text: # -*- coding: utf-8 -*-
  
  # MIT License
  #
  # Copyright 2018-2021 New York University Abu Dhabi
  #
  # Permission is hereby granted, free of charge, to any person obtaining a copy
  # of this software and associated documentation files (the "Software"), to deal
  # in the Software without restriction, including without limitation the rights
  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  # copies of the Software, and to permit persons to whom the Software is
  # furnished to do so, subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be included in
  # all copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  # SOFTWARE.
  
  """Contains the CharMapper class (for mapping characters in a Unicode string to
  other strings) and custom exceptions raised by CharMapper.
  """
  
  from __future__ import absolute_import
  
  from collections import deque
  from collections.abc import Mapping
  import os
  import json
  
  from .stringutils import isunicode
  
  
  class InvalidCharMapKeyError(ValueError):
      """Exception raised when an invalid key is found in a charmap used to
      initialize :obj:`CharMapper`.
      """
  
      def __init__(self, key, message):
          super(InvalidCharMapKeyError, self).__init__(message)
          self.key = key
          self.message = message
  
      def __repr__(self):
          return 'InvalidCharMapKeyError({}, {})'.format(
              repr(self.key), repr(self.message)
          )
  
      def __str__(self):
          return self.message
  
  
  class BuiltinCharMapNotFoundError(ValueError):
      """Exception raised when a specified map name passed to
      :func:`CharMapper.builtin_mapper` is not in the list of builtin maps.
      """
  
      def __init__(self, map_name, message):
          super(BuiltinCharMapNotFoundError, self).__init__(message)
          self.map_name = map_name
          self.message = message
  
      def __repr__(self):
          return 'BuiltinCharMapNotFoundError({}, {})'.format(
              repr(self.map_name), repr(self.message)
          )
  
      def __str__(self):
          return self.message
  
  
  class CharMapper(object):
      """A class for mapping characters in a Unicode string to other strings.
  
      Args:
          charmap (:obj:`dict`): A dictionary or any other dictionary-like
              obeject (implementing collections.Mapping) mapping characters
              or range of characters to a string. Keys in the dictionary
              should be Unicode strings of length 1 or 3. Strings of length 1
              indicate a single character to be mapped, while strings of
              length 3 indicate a range. Range strings should have the format
              'a-b' where is the starting character in the range and 'b' is
              the last character in the range (inclusive). 'b' should have a
              strictly larger ordinal number than 'a'. Dictionary values
              should be either strings or `None`, where `None` indicates that
              characters are mapped to themselves. Use an empty string to
              indicate deletion.
          default (:obj:`str`, optional): The default value to map characters
              not in **charmap** to. `None` indicates that characters map to
              themselves. Defaults to `None`.
  
      Raises:
          :obj:`InvalidCharMapKeyError`: If a key in charmap is not a Unicode
              string containing either a single character or a valid
              character range.
          :obj:`TypeError`: If default or a value for a key in charmap is
              neither `None` nor a Unicode string, or if **charmap** is not a
              dictionary-like object.
      """
  
      BUILTIN_CHARMAPS = frozenset((
          'ar2bw',
          'ar2safebw',
          'ar2xmlbw',
          'ar2hsb',
          'bw2ar',
          'bw2safebw',
          'bw2xmlbw',
          'bw2hsb',
          'safebw2ar',
          'safebw2bw',
          'safebw2xmlbw',
          'safebw2hsb',
          'xmlbw2ar',
          'xmlbw2bw',
          'xmlbw2safebw',
          'xmlbw2hsb',
          'hsb2ar',
          'hsb2bw',
          'hsb2safebw',
          'hsb2xmlbw',
          'arclean',
      ))
  
      @staticmethod
      def _expand_char_map(charmap):
          """Creates a new dictionary from charmap where character ranges are
          expanded and given their own dictionary entry.
  
          Args:
              charmap (:obj:`dict`): The character map to be expanded.
  
          Raises:
              :obj:`InvalidCharMapKeyError`: If a key in **charmap** is not a
                  Unicode string containing either a single character or a valid
                  character range.
              :obj:`TypeError`: If a value for a key in **charmap** is neither
                  `None` nor a Unicode string.
          """
  
          # TODO: Implement a space efficient character map data structure
  
          new_map = {}
  
          for key in charmap.keys():
              # Check that key is a string
              if not isunicode(key):
                  raise TypeError('Expected string as key. '
                                  'Got {} instead.'.format(type(key)))
  
              # If string is one character long we can directly add it to the map
              if len(key) == 1:
                  if charmap[key] is not None and not isunicode(charmap[key]):
                      raise TypeError(
                          ('Expected a Unicode string or None value for key '
                           'value, got {} instead.').format(type(charmap[key])))
                  else:
                      new_map[key] = charmap[key]
  
              # We check if it's a range with the following rules:
              #     a) The string is 3 character long with a dash '-' in the
              #        middle.
              #     b) The first character must have a strictly smaller ordinal
              #        than the last character.
              elif len(key) == 3 and key[1] == '-':
                  if ord(key[0]) >= ord(key[2]):
                      raise InvalidCharMapKeyError(key, '')
                  else:
                      if (charmap[key] is not None
                              and not isunicode(charmap[key])):
                          raise TypeError(
                              ('Expected a Unicode string or None value for key '
                               'value, got {} instead.').format(
                                   type(charmap[key]))
                          )
                      for char in range(ord(key[0]), ord(key[2]) + 1):
                          new_map[chr(char)] = charmap[key]
  
              # Otherwise, we have an invalid map key
              else:
                  raise InvalidCharMapKeyError(
                      key, 'Invalid character or character range')
  
          return new_map
  
      def __init__(self, charmap, default=None):
          """Class constructor.
          """
  
          if isinstance(charmap, Mapping):
              self._charmap = self._expand_char_map(charmap)
          else:
              raise TypeError(
                  ('Expected a dictionary like object for charmap, got {} '
                   'instead').format(type(charmap)))
  
          if default is None or isunicode(default):
              self._default = default
          else:
              raise TypeError(
                  ('Expected a Unicode string or None value for default, got {} '
                   'instead.').format(type(default)))
  
      def __call__(self, s):
          """Alias for :func:`CharMapper.map_string`.
          """
          return self.map_string(s)
  
      @staticmethod
      def mapper_from_json(fpath):
          """Creates a :obj:`CharMapper` instance from a JSON file.
  
          Args:
              fpath (:obj:`str`): Path to JSON file.
  
          Returns:
              :obj:`CharMapper`: A new :obj:`CharMapper` instance generated from
              given JSON file.
  
          Raises:
              :obj:`InvalidCharMapKeyError`: If a key in charmap is not a Unicode
                  string containing either a single character or a valid
                  character range.
              :obj:`TypeError`: If default or a value for a key in charmap is
                  neither `None` nor a Unicode string.
              :obj:`FileNotFoundError`: If file at `fpath` doesn't exist.
              :obj:`JSONDecodeError`: If `fpath` is not a valid JSON file.
          """
  
          with open(fpath, 'r', encoding='utf-8') as charmap_fp:
              jsonstr = charmap_fp.read()
              json_dict = json.loads(jsonstr)
  
          return CharMapper(
              json_dict.get('charMap', {}),
              default=json_dict.get('default', None)
          )
  
      @staticmethod
      def builtin_mapper(map_name):
          """Creates a :obj:`CharMapper` instance from built-in mappings.
  
          Args:
              map_name (:obj:`str`): Name of built-in map.
  
          Returns:
              :obj:`CharMapper`: A new :obj:`CharMapper` instance of built-in
              map.
  
          Raises:
              :obj:`BuiltinCharMapNotFound`: If `map_name` is not in the list of
                  built-in maps.
          """
  
          if map_name not in CharMapper.BUILTIN_CHARMAPS:
              raise BuiltinCharMapNotFoundError(
                  map_name,
                  'No built in mapping with name \'{}\' '
                  'was found.'.format(map_name))
  
          try:
              charmaps_dir = os.path.join(os.path.dirname(__file__), 'charmaps')
  
          # This should never happen unless there something wrong with the
          # system or the installation.
          except Exception:  # pragma: no coverage
              raise BuiltinCharMapNotFoundError(
                  map_name,
                  'Could not create mapping with name \'{}\'.'.format(map_name))
  
          map_path = os.path.join(charmaps_dir, '{}_map.json'.format(map_name))
  
          return CharMapper.mapper_from_json(map_path)
  
      def map_string(self, s):
          """Maps each character in a given string to its corresponding value in
          the charmap.
  
          Args:
              s (:obj:`str`): A Unicode string to be mapped.
  
          Returns:
              :obj:`str`: A new Unicode string with the charmap applied.
  
          Raises:
              :obj:`TypeError`: If s is not a Unicode string.
          """
  
          if not isunicode(s):
              raise TypeError((
                  'Expected Unicode string as input, got {} instead.'
              ).format(type(s)))
  
          buff = deque()
  
          for char in s:
              transliteration = self._charmap.get(char, self._default)
              if transliteration is None:
                  buff.append(char)
              else:
                  buff.append(transliteration)
  
          return ''.join(buff)
  