# frozen_string_literal: true module TanukiEmoji # Character represents an Emoji character or sequence which can be formed by one or more Unicode code points # respectively which combined form a unique pictographic representation (known as Emoji) # # @see https://www.unicode.org/reports/tr51/ class Character IMAGE_PREFIX = 'emoji_u' IMAGE_EXTENSION = '.png' FLAG_REGEXP = /[🇦-🇿]{2}/u.freeze ALPHA_CODE_REGEXP = /:(?[_+\-a-z0-9]+):/.freeze # This denotes a "color" or "emoji" version EMOJI_VARIATION_SELECTOR = 0xFE0F # This denotes a "plain" (black/white) or "textual" version PLAIN_VARIATION_SELECTOR = 0xFE0E PLAIN_VARIATION_SELECTOR_STRING = PLAIN_VARIATION_SELECTOR.chr(Encoding::UTF_8) # Zero Width Joiner is used in sequences to indicate they should all be evaluated and displayed as a single thing ZWJ_TAG = 0x200D attr_reader :name, :codepoints, :codepoints_alternates, :alpha_code, :aliases, :ascii_aliases, :description, :category attr_accessor :unicode_version # @param [String] name # @param [String] codepoints # @param [String] alpha_code # @param [String] description # @param [String] category def initialize(name, codepoints:, alpha_code:, description:, category:) @name = self.class.format_name(name) @codepoints = codepoints @codepoints_alternates = [] @alpha_code = self.class.format_alpha_code(alpha_code) @aliases = [] @ascii_aliases = [] @description = description @category = category end # Add alternative codepoints to this character # # @param [String] codepoints def add_codepoints(codepoints) codepoints_alternates << codepoints end # Add alternative alpha_codes to this character # # @param [String] alpha_code def add_alias(alpha_code) aliases << self.class.format_alpha_code(alpha_code) end # Add alternative ASCII aliases to this character # # @param [String] ascii_string def add_ascii_alias(ascii_string) ascii_aliases << ascii_string end # Return a Hex formatted version of the Unicode code points # # @return [String] Hex formatted version of the unicode def hex unicode_to_hex(codepoints).join('-') end # Generate the image name to be used as fallback for this character # # @return [String] image name with extension def image_name # Noto doesn't ship flags as part of regular hex-named files # Flags are stored in a separate third-party folder and follow ISO-3166-1 codes # @see http://en.wikipedia.org/wiki/ISO_3166-1 return alpha_code.tr(':', '').sub('flag_', '').upcase + IMAGE_EXTENSION if flag? # Noto omits Emoji Variation Selector on their resources file names IMAGE_PREFIX + unicode_to_hex(codepoints).reject { |i| i == EMOJI_VARIATION_SELECTOR.to_s(16) }.join('_') + IMAGE_EXTENSION end # Return whether current character represents a flag or not # # @return [Boolean] whether character represents a flag or not def flag? codepoints.match?(FLAG_REGEXP) end def to_s codepoints end def inspect "#<#{self.class.name}:#{name} #{codepoints}(#{hex})>" end def ==(other) name == other.name && codepoints == other.codepoints && codepoints_alternates == other.codepoints_alternates && alpha_code == other.alpha_code && aliases == other.aliases && ascii_aliases == other.ascii_aliases && description == other.description end # Ensure alpha code is formatted with colons # # @param [String] alpha_code # @return [String] formatted alpha code def self.format_alpha_code(alpha_code) alpha_code.to_s.match?(ALPHA_CODE_REGEXP) ? alpha_code.to_s : ":#{alpha_code}:" end def self.format_name(raw_name) matched = raw_name.match(ALPHA_CODE_REGEXP) matched ? matched['alpha_text'] : raw_name end private # Return each codepoint converted to its hex value as string # # @param [String] value # @return [Array] hex value as string def unicode_to_hex(value) value.unpack('U*').map { |i| i.to_s(16).rjust(4, '0') } end end end