class Unicode::DisplayWidth
def self.decompress_index(index, level)
def self.decompress_index(index, level) index.flat_map{ |value| if level > 0 if value.instance_of?(Array) value[15] ||= nil decompress_index(value, level - 1) else decompress_index([value] * 16, level - 1) end else if value.instance_of?(Array) value[15] ||= nil value else [value] * 16 end end } end
def self.emoji_extra_width_of(string, ambiguous = 1, overwrite = {}, _ = {})
def self.emoji_extra_width_of(string, ambiguous = 1, overwrite = {}, _ = {}) require "unicode/emoji" extra_width = 0 modifier_regex = /[#{ Unicode::Emoji::EMOJI_MODIFIERS.pack("U*") }]/ zwj_regex = /(?<=#{ [Unicode::Emoji::ZWJ].pack("U") })./ string.scan(Unicode::Emoji::REGEX){ |emoji| extra_width += 2 * emoji.scan(modifier_regex).size emoji.scan(zwj_regex){ |zwj_succ| extra_width += self.of(zwj_succ, ambiguous, overwrite) } } extra_width end
def self.of(string, ambiguous = 1, overwrite = {}, options = {})
def self.of(string, ambiguous = 1, overwrite = {}, options = {}) if overwrite.empty? # Optimization for ASCII-only strings without certain control symbols if string.ascii_only? if string.match?(ASCII_NON_ZERO_REGEX) res = string.gsub(ASCII_NON_ZERO_REGEX, "").size - string.count("\b") res < 0 ? 0 : res else string.size end else width_no_overwrite(string, ambiguous, options) end else width_all_features(string, ambiguous, overwrite, options) end end
def self.width_all_features(string, ambiguous, overwrite, options)
def self.width_all_features(string, ambiguous, overwrite, options) # Sum of all chars widths res = string.codepoints.sum{ |codepoint| next overwrite[codepoint] if overwrite[codepoint] if codepoint > 15 && codepoint < 161 # very common next 1 elsif codepoint < 0x1001 width = FIRST_4096[codepoint] else width = INDEX depth = INITIAL_DEPTH while (width = width[codepoint / depth]).instance_of? Array codepoint %= depth depth /= 16 end end width == :A ? ambiguous : (width || 1) } # Substract emoji error res -= emoji_extra_width_of(string, ambiguous, overwrite) if options[:emoji] # Return result + prevent negative lengths res < 0 ? 0 : res end
def self.width_no_overwrite(string, ambiguous, options = {})
def self.width_no_overwrite(string, ambiguous, options = {}) # Sum of all chars widths res = string.codepoints.sum{ |codepoint| if codepoint > 15 && codepoint < 161 # very common next 1 elsif codepoint < 0x1001 width = FIRST_4096[codepoint] else width = INDEX depth = INITIAL_DEPTH while (width = width[codepoint / depth]).instance_of? Array codepoint %= depth depth /= 16 end end width == :A ? ambiguous : (width || 1) } # Substract emoji error res -= emoji_extra_width_of(string, ambiguous) if options[:emoji] # Return result + prevent negative lengths res < 0 ? 0 : res end
def get_config(**kwargs)
def get_config(**kwargs) [ kwargs[:ambiguous] || @ambiguous, kwargs[:overwrite] || @overwrite, { emoji: kwargs[:emoji] || @emoji }, ] end
def initialize(ambiguous: 1, overwrite: {}, emoji: false)
def initialize(ambiguous: 1, overwrite: {}, emoji: false) @ambiguous = ambiguous @overwrite = overwrite @emoji = emoji end
def of(string, **kwargs)
def of(string, **kwargs) self.class.of(string, *get_config(**kwargs)) end