2017-07-09 08:06:36 -04:00
|
|
|
# frozen_string_literal: true
|
2017-07-10 09:39:13 -04:00
|
|
|
|
2017-10-21 09:11:29 -04:00
|
|
|
require "active_support/json"
|
|
|
|
require "active_support/core_ext/string/access"
|
|
|
|
require "active_support/core_ext/string/behavior"
|
2020-05-24 16:14:44 -04:00
|
|
|
require "active_support/core_ext/symbol/starts_ends_with"
|
2017-10-21 09:11:29 -04:00
|
|
|
require "active_support/core_ext/module/delegation"
|
2008-09-21 11:21:30 -04:00
|
|
|
|
|
|
|
module ActiveSupport #:nodoc:
|
|
|
|
module Multibyte #:nodoc:
|
2012-09-14 23:54:39 -04:00
|
|
|
# Chars enables you to work transparently with UTF-8 encoding in the Ruby
|
|
|
|
# String class without having extensive knowledge about the encoding. A
|
|
|
|
# Chars object accepts a string upon initialization and proxies String
|
|
|
|
# methods in an encoding safe manner. All the normal String methods are also
|
|
|
|
# implemented on the proxy.
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
2012-09-14 23:54:39 -04:00
|
|
|
# String methods are proxied through the Chars object, and can be accessed
|
|
|
|
# through the +mb_chars+ method. Methods which would normally return a
|
|
|
|
# String object now return a Chars object so methods can be chained.
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
2018-10-12 11:40:29 -04:00
|
|
|
# 'The Perfect String '.mb_chars.downcase.strip
|
2017-08-01 19:07:37 -04:00
|
|
|
# # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
2012-09-14 23:54:39 -04:00
|
|
|
# Chars objects are perfectly interchangeable with String objects as long as
|
|
|
|
# no explicit class checks are made. If certain methods do explicitly check
|
|
|
|
# the class, call +to_s+ before you pass chars objects to them.
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
2012-09-14 23:54:39 -04:00
|
|
|
# bad.explicit_checking_method 'T'.mb_chars.downcase.to_s
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
2012-09-14 23:54:39 -04:00
|
|
|
# The default Chars implementation assumes that the encoding of the string
|
|
|
|
# is UTF-8, if you want to handle different encodings you can write your own
|
|
|
|
# multibyte string handler and configure it through
|
2008-09-21 11:21:30 -04:00
|
|
|
# ActiveSupport::Multibyte.proxy_class.
|
|
|
|
#
|
|
|
|
# class CharsForUTF32
|
|
|
|
# def size
|
|
|
|
# @wrapped_string.size / 4
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# def self.accepts?(string)
|
|
|
|
# string.length % 4 == 0
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
|
|
|
|
class Chars
|
2012-01-05 15:15:36 -05:00
|
|
|
include Comparable
|
2008-09-21 11:21:30 -04:00
|
|
|
attr_reader :wrapped_string
|
|
|
|
alias to_s wrapped_string
|
|
|
|
alias to_str wrapped_string
|
|
|
|
|
2019-07-29 00:33:03 -04:00
|
|
|
delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
|
2012-01-05 13:43:06 -05:00
|
|
|
|
2011-12-20 12:22:21 -05:00
|
|
|
# Creates a new Chars instance by wrapping _string_.
|
|
|
|
def initialize(string)
|
|
|
|
@wrapped_string = string
|
|
|
|
@wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# Forward all undefined methods to the wrapped string.
|
|
|
|
def method_missing(method, *args, &block)
|
2013-09-20 01:37:41 -04:00
|
|
|
result = @wrapped_string.__send__(method, *args, &block)
|
2020-05-23 20:23:46 -04:00
|
|
|
if method.end_with?("!")
|
2012-01-05 09:51:10 -05:00
|
|
|
self if result
|
2008-09-21 11:21:30 -04:00
|
|
|
else
|
2008-09-21 11:29:22 -04:00
|
|
|
result.kind_of?(String) ? chars(result) : result
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
end
|
2008-09-21 11:29:22 -04:00
|
|
|
|
2012-09-14 23:54:39 -04:00
|
|
|
# Returns +true+ if _obj_ responds to the given method. Private methods
|
|
|
|
# are included in the search only if the optional second parameter
|
|
|
|
# evaluates to +true+.
|
2012-05-05 02:34:18 -04:00
|
|
|
def respond_to_missing?(method, include_private)
|
|
|
|
@wrapped_string.respond_to?(method, include_private)
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
2012-09-14 23:54:39 -04:00
|
|
|
# Returns +true+ when the proxy class can handle the string. Returns
|
|
|
|
# +false+ otherwise.
|
2008-09-21 11:21:30 -04:00
|
|
|
def self.consumes?(string)
|
2018-10-15 02:29:56 -04:00
|
|
|
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
|
|
ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be
|
|
|
|
removed from Rails 6.1. Use string.is_utf8? instead.
|
|
|
|
MSG
|
|
|
|
|
2012-01-05 14:21:59 -05:00
|
|
|
string.encoding == Encoding::UTF_8
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
2008-05-31 17:54:17 -04:00
|
|
|
|
2012-09-14 23:54:39 -04:00
|
|
|
# Works just like <tt>String#split</tt>, with the exception that the items
|
|
|
|
# in the resulting list are Chars instances instead of String. This makes
|
|
|
|
# chaining methods easier.
|
2009-08-09 21:57:25 -04:00
|
|
|
#
|
2010-07-29 20:30:04 -04:00
|
|
|
# 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"]
|
2010-05-10 09:46:37 -04:00
|
|
|
def split(*args)
|
2012-05-28 16:32:14 -04:00
|
|
|
@wrapped_string.split(*args).map { |i| self.class.new(i) }
|
2009-08-09 21:57:25 -04:00
|
|
|
end
|
|
|
|
|
2015-05-26 03:53:05 -04:00
|
|
|
# Works like <tt>String#slice!</tt>, but returns an instance of
|
2016-10-26 17:13:15 -04:00
|
|
|
# Chars, or +nil+ if the string was not modified. The string will not be
|
2015-10-20 18:02:31 -04:00
|
|
|
# modified if the range given is out of bounds
|
2015-05-25 13:02:52 -04:00
|
|
|
#
|
|
|
|
# string = 'Welcome'
|
|
|
|
# string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c">
|
|
|
|
# string # => 'Welome'
|
|
|
|
# string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo">
|
|
|
|
# string # => 'me'
|
2012-01-05 09:40:01 -05:00
|
|
|
def slice!(*args)
|
2015-06-29 16:20:50 -04:00
|
|
|
string_sliced = @wrapped_string.slice!(*args)
|
2015-10-20 18:02:31 -04:00
|
|
|
if string_sliced
|
|
|
|
chars(string_sliced)
|
|
|
|
end
|
2012-01-05 09:40:01 -05:00
|
|
|
end
|
|
|
|
|
2008-09-21 11:28:05 -04:00
|
|
|
# Reverses all characters in the string.
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
2010-07-29 20:30:04 -04:00
|
|
|
# 'Café'.mb_chars.reverse.to_s # => 'éfaC'
|
2008-09-21 11:21:30 -04:00
|
|
|
def reverse
|
2018-10-16 16:36:30 -04:00
|
|
|
chars(@wrapped_string.scan(/\X/).reverse.join)
|
2006-10-03 19:45:32 -04:00
|
|
|
end
|
2009-08-31 15:16:22 -04:00
|
|
|
|
2012-09-14 23:54:39 -04:00
|
|
|
# Limits the byte size of the string to a number of bytes without breaking
|
|
|
|
# characters. Usable when the storage for a string is limited for some
|
|
|
|
# reason.
|
2009-11-01 11:18:27 -05:00
|
|
|
#
|
2011-07-13 05:27:14 -04:00
|
|
|
# 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
|
2009-11-01 11:18:27 -05:00
|
|
|
def limit(limit)
|
2019-09-02 21:24:49 -04:00
|
|
|
chars(@wrapped_string.truncate_bytes(limit, omission: nil))
|
2009-11-01 11:18:27 -05:00
|
|
|
end
|
|
|
|
|
2010-05-28 15:36:22 -04:00
|
|
|
# Capitalizes the first letter of every word, when possible.
|
|
|
|
#
|
2017-08-01 19:07:37 -04:00
|
|
|
# "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró"
|
|
|
|
# "日本語".mb_chars.titleize.to_s # => "日本語"
|
2010-05-28 15:36:22 -04:00
|
|
|
def titleize
|
2018-10-08 04:57:18 -04:00
|
|
|
chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase })
|
2010-05-28 15:36:22 -04:00
|
|
|
end
|
|
|
|
alias_method :titlecase, :titleize
|
|
|
|
|
2012-09-14 23:54:39 -04:00
|
|
|
# Returns the KC normalization of the string by default. NFKC is
|
|
|
|
# considered the best normalization form for passing strings to databases
|
|
|
|
# and validations.
|
2008-09-21 11:21:30 -04:00
|
|
|
#
|
|
|
|
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
|
|
|
|
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
|
2010-05-10 09:46:37 -04:00
|
|
|
# ActiveSupport::Multibyte::Unicode.default_normalization_form
|
|
|
|
def normalize(form = nil)
|
2018-10-12 11:40:29 -04:00
|
|
|
form ||= Unicode.default_normalization_form
|
|
|
|
|
|
|
|
# See https://www.unicode.org/reports/tr15, Table 1
|
|
|
|
if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form]
|
|
|
|
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
|
|
ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
|
|
|
|
removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead.
|
|
|
|
MSG
|
|
|
|
|
|
|
|
send(:unicode_normalize, alias_form)
|
|
|
|
else
|
|
|
|
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
|
|
ActiveSupport::Multibyte::Chars#normalize is deprecated and will be
|
|
|
|
removed from Rails 6.1. Use #unicode_normalize instead.
|
|
|
|
MSG
|
|
|
|
|
|
|
|
raise ArgumentError, "#{form} is not a valid normalization variant", caller
|
|
|
|
end
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# Performs canonical decomposition on all the characters.
|
|
|
|
#
|
2010-07-29 20:30:04 -04:00
|
|
|
# 'é'.length # => 2
|
|
|
|
# 'é'.mb_chars.decompose.to_s.length # => 3
|
2008-09-21 11:21:30 -04:00
|
|
|
def decompose
|
2016-08-06 11:58:50 -04:00
|
|
|
chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack("U*"))
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# Performs composition on all the characters.
|
|
|
|
#
|
2010-07-29 20:30:04 -04:00
|
|
|
# 'é'.length # => 3
|
|
|
|
# 'é'.mb_chars.compose.to_s.length # => 2
|
2008-09-21 11:21:30 -04:00
|
|
|
def compose
|
2016-08-06 11:58:50 -04:00
|
|
|
chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack("U*"))
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# Returns the number of grapheme clusters in the string.
|
|
|
|
#
|
2010-07-29 20:30:04 -04:00
|
|
|
# 'क्षि'.mb_chars.length # => 4
|
2012-01-05 15:25:40 -05:00
|
|
|
# 'क्षि'.mb_chars.grapheme_length # => 3
|
|
|
|
def grapheme_length
|
2018-10-16 16:36:30 -04:00
|
|
|
@wrapped_string.scan(/\X/).length
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
2012-09-14 23:54:39 -04:00
|
|
|
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
|
|
|
|
# resulting in a valid UTF-8 string.
|
2010-04-07 15:21:41 -04:00
|
|
|
#
|
2012-09-14 23:54:39 -04:00
|
|
|
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
|
|
|
|
# encoding is entirely CP1252 or ISO-8859-1.
|
2010-04-07 15:21:41 -04:00
|
|
|
def tidy_bytes(force = false)
|
2010-05-10 09:46:37 -04:00
|
|
|
chars(Unicode.tidy_bytes(@wrapped_string, force))
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
|
2012-01-31 17:01:00 -05:00
|
|
|
def as_json(options = nil) #:nodoc:
|
|
|
|
to_s.as_json(options)
|
|
|
|
end
|
|
|
|
|
2018-10-11 05:00:46 -04:00
|
|
|
%w(reverse tidy_bytes).each do |method|
|
2012-01-05 10:25:27 -05:00
|
|
|
define_method("#{method}!") do |*args|
|
|
|
|
@wrapped_string = send(method, *args).to_s
|
|
|
|
self
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-12-17 03:13:50 -05:00
|
|
|
private
|
|
|
|
def chars(string)
|
2008-09-21 11:28:05 -04:00
|
|
|
self.class.new(string)
|
2008-09-21 11:21:30 -04:00
|
|
|
end
|
|
|
|
end
|
2006-10-03 19:45:32 -04:00
|
|
|
end
|
2008-10-21 13:33:40 -04:00
|
|
|
end
|