activesupport 5.2.8.1 → 6.1.6.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activesupport might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +426 -424
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -3
- data/lib/active_support/actionable_error.rb +48 -0
- data/lib/active_support/array_inquirer.rb +4 -2
- data/lib/active_support/backtrace_cleaner.rb +29 -3
- data/lib/active_support/benchmarkable.rb +1 -1
- data/lib/active_support/cache/file_store.rb +34 -34
- data/lib/active_support/cache/mem_cache_store.rb +39 -24
- data/lib/active_support/cache/memory_store.rb +59 -33
- data/lib/active_support/cache/null_store.rb +8 -3
- data/lib/active_support/cache/redis_cache_store.rb +72 -45
- data/lib/active_support/cache/strategy/local_cache.rb +41 -26
- data/lib/active_support/cache.rb +148 -78
- data/lib/active_support/callbacks.rb +81 -64
- data/lib/active_support/concern.rb +70 -3
- data/lib/active_support/concurrency/share_lock.rb +0 -1
- data/lib/active_support/configurable.rb +10 -14
- data/lib/active_support/configuration_file.rb +51 -0
- data/lib/active_support/core_ext/array/access.rb +18 -6
- data/lib/active_support/core_ext/array/conversions.rb +5 -5
- data/lib/active_support/core_ext/array/extract.rb +21 -0
- data/lib/active_support/core_ext/array.rb +1 -1
- data/lib/active_support/core_ext/benchmark.rb +2 -2
- data/lib/active_support/core_ext/class/attribute.rb +32 -47
- data/lib/active_support/core_ext/class/subclasses.rb +17 -38
- data/lib/active_support/core_ext/date/calculations.rb +6 -5
- data/lib/active_support/core_ext/date/conversions.rb +2 -1
- data/lib/active_support/core_ext/date_and_time/calculations.rb +37 -47
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
- data/lib/active_support/core_ext/date_and_time/zones.rb +0 -1
- data/lib/active_support/core_ext/date_time/calculations.rb +1 -1
- data/lib/active_support/core_ext/date_time/conversions.rb +0 -1
- data/lib/active_support/core_ext/digest/uuid.rb +1 -0
- data/lib/active_support/core_ext/enumerable.rb +171 -75
- data/lib/active_support/core_ext/hash/conversions.rb +3 -3
- data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
- data/lib/active_support/core_ext/hash/except.rb +2 -2
- data/lib/active_support/core_ext/hash/keys.rb +1 -30
- data/lib/active_support/core_ext/hash/slice.rb +6 -27
- data/lib/active_support/core_ext/hash.rb +1 -2
- data/lib/active_support/core_ext/integer/multiple.rb +1 -1
- data/lib/active_support/core_ext/kernel.rb +0 -1
- data/lib/active_support/core_ext/load_error.rb +1 -1
- data/lib/active_support/core_ext/marshal.rb +2 -0
- data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
- data/lib/active_support/core_ext/module/attribute_accessors.rb +30 -39
- data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +17 -19
- data/lib/active_support/core_ext/module/concerning.rb +8 -2
- data/lib/active_support/core_ext/module/delegation.rb +76 -33
- data/lib/active_support/core_ext/module/introspection.rb +16 -15
- data/lib/active_support/core_ext/module/redefine_method.rb +8 -17
- data/lib/active_support/core_ext/module.rb +0 -1
- data/lib/active_support/core_ext/name_error.rb +29 -2
- data/lib/active_support/core_ext/numeric/conversions.rb +129 -129
- data/lib/active_support/core_ext/numeric.rb +0 -1
- data/lib/active_support/core_ext/object/blank.rb +1 -2
- data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
- data/lib/active_support/core_ext/object/duplicable.rb +7 -114
- data/lib/active_support/core_ext/object/json.rb +14 -2
- data/lib/active_support/core_ext/object/try.rb +17 -7
- data/lib/active_support/core_ext/object/with_options.rb +1 -1
- data/lib/active_support/core_ext/range/compare_range.rb +34 -13
- data/lib/active_support/core_ext/range/conversions.rb +31 -29
- data/lib/active_support/core_ext/range/each.rb +0 -1
- data/lib/active_support/core_ext/range/include_time_with_zone.rb +8 -3
- data/lib/active_support/core_ext/regexp.rb +8 -5
- data/lib/active_support/core_ext/securerandom.rb +23 -3
- data/lib/active_support/core_ext/string/access.rb +5 -16
- data/lib/active_support/core_ext/string/conversions.rb +1 -0
- data/lib/active_support/core_ext/string/filters.rb +42 -1
- data/lib/active_support/core_ext/string/inflections.rb +45 -6
- data/lib/active_support/core_ext/string/inquiry.rb +1 -0
- data/lib/active_support/core_ext/string/multibyte.rb +6 -5
- data/lib/active_support/core_ext/string/output_safety.rb +70 -13
- data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
- data/lib/active_support/core_ext/string/strip.rb +3 -1
- data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
- data/lib/active_support/core_ext/symbol.rb +3 -0
- data/lib/active_support/core_ext/time/calculations.rb +53 -3
- data/lib/active_support/core_ext/time/conversions.rb +2 -0
- data/lib/active_support/core_ext/uri.rb +6 -1
- data/lib/active_support/core_ext.rb +1 -1
- data/lib/active_support/current_attributes/test_helper.rb +13 -0
- data/lib/active_support/current_attributes.rb +16 -2
- data/lib/active_support/dependencies/zeitwerk_integration.rb +120 -0
- data/lib/active_support/dependencies.rb +109 -34
- data/lib/active_support/deprecation/behaviors.rb +16 -3
- data/lib/active_support/deprecation/disallowed.rb +56 -0
- data/lib/active_support/deprecation/instance_delegator.rb +0 -1
- data/lib/active_support/deprecation/method_wrappers.rb +18 -23
- data/lib/active_support/deprecation/proxy_wrappers.rb +29 -6
- data/lib/active_support/deprecation/reporting.rb +50 -7
- data/lib/active_support/deprecation.rb +6 -1
- data/lib/active_support/descendants_tracker.rb +59 -9
- data/lib/active_support/digest.rb +2 -0
- data/lib/active_support/duration/iso8601_parser.rb +2 -4
- data/lib/active_support/duration/iso8601_serializer.rb +18 -14
- data/lib/active_support/duration.rb +82 -33
- data/lib/active_support/encrypted_configuration.rb +0 -4
- data/lib/active_support/encrypted_file.rb +22 -4
- data/lib/active_support/environment_inquirer.rb +20 -0
- data/lib/active_support/evented_file_update_checker.rb +82 -117
- data/lib/active_support/execution_wrapper.rb +2 -1
- data/lib/active_support/file_update_checker.rb +0 -1
- data/lib/active_support/fork_tracker.rb +64 -0
- data/lib/active_support/gem_version.rb +3 -3
- data/lib/active_support/hash_with_indifferent_access.rb +70 -42
- data/lib/active_support/i18n.rb +1 -0
- data/lib/active_support/i18n_railtie.rb +15 -8
- data/lib/active_support/inflector/inflections.rb +2 -7
- data/lib/active_support/inflector/methods.rb +49 -58
- data/lib/active_support/inflector/transliterate.rb +47 -18
- data/lib/active_support/json/decoding.rb +25 -26
- data/lib/active_support/json/encoding.rb +11 -3
- data/lib/active_support/key_generator.rb +1 -33
- data/lib/active_support/lazy_load_hooks.rb +5 -2
- data/lib/active_support/locale/en.rb +33 -0
- data/lib/active_support/locale/en.yml +7 -3
- data/lib/active_support/log_subscriber.rb +39 -9
- data/lib/active_support/logger.rb +2 -17
- data/lib/active_support/logger_silence.rb +11 -19
- data/lib/active_support/logger_thread_safe_level.rb +50 -6
- data/lib/active_support/message_encryptor.rb +8 -13
- data/lib/active_support/message_verifier.rb +10 -10
- data/lib/active_support/messages/metadata.rb +11 -2
- data/lib/active_support/messages/rotation_configuration.rb +2 -1
- data/lib/active_support/messages/rotator.rb +10 -9
- data/lib/active_support/multibyte/chars.rb +10 -68
- data/lib/active_support/multibyte/unicode.rb +15 -327
- data/lib/active_support/notifications/fanout.rb +116 -16
- data/lib/active_support/notifications/instrumenter.rb +71 -9
- data/lib/active_support/notifications.rb +72 -8
- data/lib/active_support/number_helper/number_converter.rb +5 -6
- data/lib/active_support/number_helper/number_to_currency_converter.rb +4 -9
- data/lib/active_support/number_helper/number_to_delimited_converter.rb +3 -2
- data/lib/active_support/number_helper/number_to_human_converter.rb +4 -3
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +4 -3
- data/lib/active_support/number_helper/number_to_percentage_converter.rb +3 -1
- data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
- data/lib/active_support/number_helper/number_to_rounded_converter.rb +12 -7
- data/lib/active_support/number_helper/rounding_helper.rb +12 -28
- data/lib/active_support/number_helper.rb +38 -12
- data/lib/active_support/option_merger.rb +22 -3
- data/lib/active_support/ordered_hash.rb +1 -1
- data/lib/active_support/ordered_options.rb +13 -3
- data/lib/active_support/parameter_filter.rb +133 -0
- data/lib/active_support/per_thread_registry.rb +2 -1
- data/lib/active_support/rails.rb +1 -10
- data/lib/active_support/railtie.rb +23 -1
- data/lib/active_support/reloader.rb +4 -5
- data/lib/active_support/rescuable.rb +4 -4
- data/lib/active_support/secure_compare_rotator.rb +51 -0
- data/lib/active_support/security_utils.rb +19 -12
- data/lib/active_support/string_inquirer.rb +4 -3
- data/lib/active_support/subscriber.rb +72 -28
- data/lib/active_support/tagged_logging.rb +42 -8
- data/lib/active_support/test_case.rb +91 -0
- data/lib/active_support/testing/assertions.rb +30 -9
- data/lib/active_support/testing/deprecation.rb +0 -1
- data/lib/active_support/testing/file_fixtures.rb +2 -0
- data/lib/active_support/testing/isolation.rb +2 -2
- data/lib/active_support/testing/method_call_assertions.rb +28 -1
- data/lib/active_support/testing/parallelization/server.rb +78 -0
- data/lib/active_support/testing/parallelization/worker.rb +100 -0
- data/lib/active_support/testing/parallelization.rb +51 -0
- data/lib/active_support/testing/stream.rb +1 -2
- data/lib/active_support/testing/time_helpers.rb +47 -12
- data/lib/active_support/time_with_zone.rb +81 -47
- data/lib/active_support/values/time_zone.rb +34 -17
- data/lib/active_support/xml_mini/jdom.rb +2 -3
- data/lib/active_support/xml_mini/libxml.rb +2 -2
- data/lib/active_support/xml_mini/libxmlsax.rb +4 -4
- data/lib/active_support/xml_mini/nokogiri.rb +2 -2
- data/lib/active_support/xml_mini/nokogirisax.rb +3 -3
- data/lib/active_support/xml_mini/rexml.rb +10 -3
- data/lib/active_support/xml_mini.rb +2 -10
- data/lib/active_support.rb +14 -1
- metadata +54 -27
- data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -9
- data/lib/active_support/core_ext/hash/compact.rb +0 -29
- data/lib/active_support/core_ext/hash/transform_values.rb +0 -32
- data/lib/active_support/core_ext/kernel/agnostics.rb +0 -13
- data/lib/active_support/core_ext/module/reachable.rb +0 -11
- data/lib/active_support/core_ext/numeric/inquiry.rb +0 -28
- data/lib/active_support/core_ext/range/include_range.rb +0 -3
- data/lib/active_support/values/unicode_tables.dat +0 -0
@@ -71,7 +71,7 @@ module ActiveSupport
|
|
71
71
|
# @verifier.generate(parcel, expires_in: 1.month)
|
72
72
|
# @verifier.generate(doowad, expires_at: Time.now.end_of_year)
|
73
73
|
#
|
74
|
-
# Then the messages can be verified and returned
|
74
|
+
# Then the messages can be verified and returned up to the expire time.
|
75
75
|
# Thereafter, the +verified+ method returns +nil+ while +verify+ raises
|
76
76
|
# <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
|
77
77
|
#
|
@@ -103,11 +103,11 @@ module ActiveSupport
|
|
103
103
|
|
104
104
|
class InvalidSignature < StandardError; end
|
105
105
|
|
106
|
-
def initialize(secret,
|
106
|
+
def initialize(secret, digest: nil, serializer: nil)
|
107
107
|
raise ArgumentError, "Secret should not be nil." unless secret
|
108
108
|
@secret = secret
|
109
|
-
@digest =
|
110
|
-
@serializer =
|
109
|
+
@digest = digest || "SHA1"
|
110
|
+
@serializer = serializer || Marshal
|
111
111
|
end
|
112
112
|
|
113
113
|
# Checks if a signed message could have been generated by signing an object
|
@@ -122,7 +122,7 @@ module ActiveSupport
|
|
122
122
|
def valid_message?(signed_message)
|
123
123
|
return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
|
124
124
|
|
125
|
-
data, digest = signed_message.split("--"
|
125
|
+
data, digest = signed_message.split("--")
|
126
126
|
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
|
127
127
|
end
|
128
128
|
|
@@ -150,7 +150,7 @@ module ActiveSupport
|
|
150
150
|
def verified(signed_message, purpose: nil, **)
|
151
151
|
if valid_message?(signed_message)
|
152
152
|
begin
|
153
|
-
data = signed_message.split("--"
|
153
|
+
data = signed_message.split("--")[0]
|
154
154
|
message = Messages::Metadata.verify(decode(data), purpose)
|
155
155
|
@serializer.load(message) if message
|
156
156
|
rescue ArgumentError => argument_error
|
@@ -172,14 +172,14 @@ module ActiveSupport
|
|
172
172
|
#
|
173
173
|
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
|
174
174
|
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
|
175
|
-
def verify(*args)
|
176
|
-
verified(*args) || raise(InvalidSignature)
|
175
|
+
def verify(*args, **options)
|
176
|
+
verified(*args, **options) || raise(InvalidSignature)
|
177
177
|
end
|
178
178
|
|
179
179
|
# Generates a signed message for the provided value.
|
180
180
|
#
|
181
|
-
# The message is signed with the +MessageVerifier+'s secret.
|
182
|
-
#
|
181
|
+
# The message is signed with the +MessageVerifier+'s secret.
|
182
|
+
# Returns Base64-encoded message joined with the generated signature.
|
183
183
|
#
|
184
184
|
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
|
185
185
|
# verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
|
@@ -6,7 +6,8 @@ module ActiveSupport
|
|
6
6
|
module Messages #:nodoc:
|
7
7
|
class Metadata #:nodoc:
|
8
8
|
def initialize(message, expires_at = nil, purpose = nil)
|
9
|
-
@message, @
|
9
|
+
@message, @purpose = message, purpose
|
10
|
+
@expires_at = expires_at.is_a?(String) ? parse_expires_at(expires_at) : expires_at
|
10
11
|
end
|
11
12
|
|
12
13
|
def as_json(options = {})
|
@@ -64,7 +65,15 @@ module ActiveSupport
|
|
64
65
|
end
|
65
66
|
|
66
67
|
def fresh?
|
67
|
-
@expires_at.nil? || Time.now.utc <
|
68
|
+
@expires_at.nil? || Time.now.utc < @expires_at
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_expires_at(expires_at)
|
72
|
+
if ActiveSupport.use_standard_json_time_format
|
73
|
+
Time.iso8601(expires_at)
|
74
|
+
else
|
75
|
+
Time.parse(expires_at)
|
76
|
+
end
|
68
77
|
end
|
69
78
|
end
|
70
79
|
end
|
@@ -3,11 +3,12 @@
|
|
3
3
|
module ActiveSupport
|
4
4
|
module Messages
|
5
5
|
module Rotator # :nodoc:
|
6
|
-
def initialize(
|
7
|
-
super
|
6
|
+
def initialize(*secrets, on_rotation: nil, **options)
|
7
|
+
super(*secrets, **options)
|
8
8
|
|
9
9
|
@options = options
|
10
10
|
@rotations = []
|
11
|
+
@on_rotation = on_rotation
|
11
12
|
end
|
12
13
|
|
13
14
|
def rotate(*secrets, **options)
|
@@ -17,28 +18,28 @@ module ActiveSupport
|
|
17
18
|
module Encryptor
|
18
19
|
include Rotator
|
19
20
|
|
20
|
-
def decrypt_and_verify(*args, on_rotation:
|
21
|
+
def decrypt_and_verify(*args, on_rotation: @on_rotation, **options)
|
21
22
|
super
|
22
23
|
rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature
|
23
|
-
run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, options) } || raise
|
24
|
+
run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, **options) } || raise
|
24
25
|
end
|
25
26
|
|
26
27
|
private
|
27
28
|
def build_rotation(secret = @secret, sign_secret = @sign_secret, options)
|
28
|
-
self.class.new(secret, sign_secret, options)
|
29
|
+
self.class.new(secret, sign_secret, **options)
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
33
|
module Verifier
|
33
34
|
include Rotator
|
34
35
|
|
35
|
-
def verified(*args, on_rotation:
|
36
|
-
super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, options) }
|
36
|
+
def verified(*args, on_rotation: @on_rotation, **options)
|
37
|
+
super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, **options) }
|
37
38
|
end
|
38
39
|
|
39
40
|
private
|
40
41
|
def build_rotation(secret = @secret, options)
|
41
|
-
self.class.new(secret, options)
|
42
|
+
self.class.new(secret, **options)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
@@ -46,7 +47,7 @@ module ActiveSupport
|
|
46
47
|
def run_rotations(on_rotation)
|
47
48
|
@rotations.find do |rotation|
|
48
49
|
if message = yield(rotation) rescue next
|
49
|
-
on_rotation
|
50
|
+
on_rotation&.call
|
50
51
|
return message
|
51
52
|
end
|
52
53
|
end
|
@@ -3,8 +3,8 @@
|
|
3
3
|
require "active_support/json"
|
4
4
|
require "active_support/core_ext/string/access"
|
5
5
|
require "active_support/core_ext/string/behavior"
|
6
|
+
require "active_support/core_ext/symbol/starts_ends_with"
|
6
7
|
require "active_support/core_ext/module/delegation"
|
7
|
-
require "active_support/core_ext/regexp"
|
8
8
|
|
9
9
|
module ActiveSupport #:nodoc:
|
10
10
|
module Multibyte #:nodoc:
|
@@ -18,7 +18,7 @@ module ActiveSupport #:nodoc:
|
|
18
18
|
# through the +mb_chars+ method. Methods which would normally return a
|
19
19
|
# String object now return a Chars object so methods can be chained.
|
20
20
|
#
|
21
|
-
# 'The Perfect String '.mb_chars.downcase.strip
|
21
|
+
# 'The Perfect String '.mb_chars.downcase.strip
|
22
22
|
# # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
|
23
23
|
#
|
24
24
|
# Chars objects are perfectly interchangeable with String objects as long as
|
@@ -49,7 +49,7 @@ module ActiveSupport #:nodoc:
|
|
49
49
|
alias to_s wrapped_string
|
50
50
|
alias to_str wrapped_string
|
51
51
|
|
52
|
-
delegate :<=>, :=~, :acts_like_string?, to: :wrapped_string
|
52
|
+
delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
|
53
53
|
|
54
54
|
# Creates a new Chars instance by wrapping _string_.
|
55
55
|
def initialize(string)
|
@@ -60,7 +60,7 @@ module ActiveSupport #:nodoc:
|
|
60
60
|
# Forward all undefined methods to the wrapped string.
|
61
61
|
def method_missing(method, *args, &block)
|
62
62
|
result = @wrapped_string.__send__(method, *args, &block)
|
63
|
-
if
|
63
|
+
if method.end_with?("!")
|
64
64
|
self if result
|
65
65
|
else
|
66
66
|
result.kind_of?(String) ? chars(result) : result
|
@@ -74,12 +74,6 @@ module ActiveSupport #:nodoc:
|
|
74
74
|
@wrapped_string.respond_to?(method, include_private)
|
75
75
|
end
|
76
76
|
|
77
|
-
# Returns +true+ when the proxy class can handle the string. Returns
|
78
|
-
# +false+ otherwise.
|
79
|
-
def self.consumes?(string)
|
80
|
-
string.encoding == Encoding::UTF_8
|
81
|
-
end
|
82
|
-
|
83
77
|
# Works just like <tt>String#split</tt>, with the exception that the items
|
84
78
|
# in the resulting list are Chars instances instead of String. This makes
|
85
79
|
# chaining methods easier.
|
@@ -109,7 +103,7 @@ module ActiveSupport #:nodoc:
|
|
109
103
|
#
|
110
104
|
# 'Café'.mb_chars.reverse.to_s # => 'éfaC'
|
111
105
|
def reverse
|
112
|
-
chars(
|
106
|
+
chars(@wrapped_string.scan(/\X/).reverse.join)
|
113
107
|
end
|
114
108
|
|
115
109
|
# Limits the byte size of the string to a number of bytes without breaking
|
@@ -118,35 +112,7 @@ module ActiveSupport #:nodoc:
|
|
118
112
|
#
|
119
113
|
# 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
|
120
114
|
def limit(limit)
|
121
|
-
|
122
|
-
end
|
123
|
-
|
124
|
-
# Converts characters in the string to uppercase.
|
125
|
-
#
|
126
|
-
# 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
|
127
|
-
def upcase
|
128
|
-
chars Unicode.upcase(@wrapped_string)
|
129
|
-
end
|
130
|
-
|
131
|
-
# Converts characters in the string to lowercase.
|
132
|
-
#
|
133
|
-
# 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
|
134
|
-
def downcase
|
135
|
-
chars Unicode.downcase(@wrapped_string)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Converts characters in the string to the opposite case.
|
139
|
-
#
|
140
|
-
# 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN"
|
141
|
-
def swapcase
|
142
|
-
chars Unicode.swapcase(@wrapped_string)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Converts the first character to uppercase and the remainder to lowercase.
|
146
|
-
#
|
147
|
-
# 'über'.mb_chars.capitalize.to_s # => "Über"
|
148
|
-
def capitalize
|
149
|
-
(slice(0) || chars("")).upcase + (slice(1..-1) || chars("")).downcase
|
115
|
+
chars(@wrapped_string.truncate_bytes(limit, omission: nil))
|
150
116
|
end
|
151
117
|
|
152
118
|
# Capitalizes the first letter of every word, when possible.
|
@@ -154,21 +120,10 @@ module ActiveSupport #:nodoc:
|
|
154
120
|
# "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró"
|
155
121
|
# "日本語".mb_chars.titleize.to_s # => "日本語"
|
156
122
|
def titleize
|
157
|
-
chars(downcase.to_s.gsub(/\b('?\S)/u) {
|
123
|
+
chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase })
|
158
124
|
end
|
159
125
|
alias_method :titlecase, :titleize
|
160
126
|
|
161
|
-
# Returns the KC normalization of the string by default. NFKC is
|
162
|
-
# considered the best normalization form for passing strings to databases
|
163
|
-
# and validations.
|
164
|
-
#
|
165
|
-
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
|
166
|
-
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
|
167
|
-
# ActiveSupport::Multibyte::Unicode.default_normalization_form
|
168
|
-
def normalize(form = nil)
|
169
|
-
chars(Unicode.normalize(@wrapped_string, form))
|
170
|
-
end
|
171
|
-
|
172
127
|
# Performs canonical decomposition on all the characters.
|
173
128
|
#
|
174
129
|
# 'é'.length # => 2
|
@@ -190,7 +145,7 @@ module ActiveSupport #:nodoc:
|
|
190
145
|
# 'क्षि'.mb_chars.length # => 4
|
191
146
|
# 'क्षि'.mb_chars.grapheme_length # => 3
|
192
147
|
def grapheme_length
|
193
|
-
|
148
|
+
@wrapped_string.scan(/\X/).length
|
194
149
|
end
|
195
150
|
|
196
151
|
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
|
@@ -206,27 +161,14 @@ module ActiveSupport #:nodoc:
|
|
206
161
|
to_s.as_json(options)
|
207
162
|
end
|
208
163
|
|
209
|
-
%w(
|
164
|
+
%w(reverse tidy_bytes).each do |method|
|
210
165
|
define_method("#{method}!") do |*args|
|
211
|
-
@wrapped_string =
|
166
|
+
@wrapped_string = public_send(method, *args).to_s
|
212
167
|
self
|
213
168
|
end
|
214
169
|
end
|
215
170
|
|
216
171
|
private
|
217
|
-
|
218
|
-
def translate_offset(byte_offset)
|
219
|
-
return nil if byte_offset.nil?
|
220
|
-
return 0 if @wrapped_string == ""
|
221
|
-
|
222
|
-
begin
|
223
|
-
@wrapped_string.byteslice(0...byte_offset).unpack("U*").length
|
224
|
-
rescue ArgumentError
|
225
|
-
byte_offset -= 1
|
226
|
-
retry
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
172
|
def chars(string)
|
231
173
|
self.class.new(string)
|
232
174
|
end
|
@@ -5,215 +5,33 @@ module ActiveSupport
|
|
5
5
|
module Unicode
|
6
6
|
extend self
|
7
7
|
|
8
|
-
# A list of all available normalization forms.
|
9
|
-
# See http://www.unicode.org/reports/tr15/tr15-29.html for more
|
10
|
-
# information about normalization.
|
11
|
-
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
|
12
|
-
|
13
8
|
# The Unicode version that is supported by the implementation
|
14
|
-
UNICODE_VERSION = "
|
15
|
-
|
16
|
-
# The default normalization used for operations that require
|
17
|
-
# normalization. It can be set to any of the normalizations
|
18
|
-
# in NORMALIZATION_FORMS.
|
19
|
-
#
|
20
|
-
# ActiveSupport::Multibyte::Unicode.default_normalization_form = :c
|
21
|
-
attr_accessor :default_normalization_form
|
22
|
-
@default_normalization_form = :kc
|
23
|
-
|
24
|
-
# Hangul character boundaries and properties
|
25
|
-
HANGUL_SBASE = 0xAC00
|
26
|
-
HANGUL_LBASE = 0x1100
|
27
|
-
HANGUL_VBASE = 0x1161
|
28
|
-
HANGUL_TBASE = 0x11A7
|
29
|
-
HANGUL_LCOUNT = 19
|
30
|
-
HANGUL_VCOUNT = 21
|
31
|
-
HANGUL_TCOUNT = 28
|
32
|
-
HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
|
33
|
-
HANGUL_SCOUNT = 11172
|
34
|
-
HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
|
35
|
-
|
36
|
-
# Detect whether the codepoint is in a certain character class. Returns
|
37
|
-
# +true+ when it's in the specified character class and +false+ otherwise.
|
38
|
-
# Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>,
|
39
|
-
# <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>.
|
40
|
-
#
|
41
|
-
# Primarily used by the grapheme cluster support.
|
42
|
-
def in_char_class?(codepoint, classes)
|
43
|
-
classes.detect { |c| database.boundary[c] === codepoint } ? true : false
|
44
|
-
end
|
45
|
-
|
46
|
-
# Unpack the string at grapheme boundaries. Returns a list of character
|
47
|
-
# lists.
|
48
|
-
#
|
49
|
-
# Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]]
|
50
|
-
# Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]]
|
51
|
-
def unpack_graphemes(string)
|
52
|
-
codepoints = string.codepoints.to_a
|
53
|
-
unpacked = []
|
54
|
-
pos = 0
|
55
|
-
marker = 0
|
56
|
-
eoc = codepoints.length
|
57
|
-
while (pos < eoc)
|
58
|
-
pos += 1
|
59
|
-
previous = codepoints[pos - 1]
|
60
|
-
current = codepoints[pos]
|
61
|
-
|
62
|
-
# See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
|
63
|
-
should_break =
|
64
|
-
if pos == eoc
|
65
|
-
true
|
66
|
-
# GB3. CR X LF
|
67
|
-
elsif previous == database.boundary[:cr] && current == database.boundary[:lf]
|
68
|
-
false
|
69
|
-
# GB4. (Control|CR|LF) ÷
|
70
|
-
elsif previous && in_char_class?(previous, [:control, :cr, :lf])
|
71
|
-
true
|
72
|
-
# GB5. ÷ (Control|CR|LF)
|
73
|
-
elsif in_char_class?(current, [:control, :cr, :lf])
|
74
|
-
true
|
75
|
-
# GB6. L X (L|V|LV|LVT)
|
76
|
-
elsif database.boundary[:l] === previous && in_char_class?(current, [:l, :v, :lv, :lvt])
|
77
|
-
false
|
78
|
-
# GB7. (LV|V) X (V|T)
|
79
|
-
elsif in_char_class?(previous, [:lv, :v]) && in_char_class?(current, [:v, :t])
|
80
|
-
false
|
81
|
-
# GB8. (LVT|T) X (T)
|
82
|
-
elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current
|
83
|
-
false
|
84
|
-
# GB9. X (Extend | ZWJ)
|
85
|
-
elsif in_char_class?(current, [:extend, :zwj])
|
86
|
-
false
|
87
|
-
# GB9a. X SpacingMark
|
88
|
-
elsif database.boundary[:spacingmark] === current
|
89
|
-
false
|
90
|
-
# GB9b. Prepend X
|
91
|
-
elsif database.boundary[:prepend] === previous
|
92
|
-
false
|
93
|
-
# GB10. (E_Base | EBG) Extend* X E_Modifier
|
94
|
-
elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current
|
95
|
-
false
|
96
|
-
# GB11. ZWJ X (Glue_After_Zwj | EBG)
|
97
|
-
elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz])
|
98
|
-
false
|
99
|
-
# GB12. ^ (RI RI)* RI X RI
|
100
|
-
# GB13. [^RI] (RI RI)* RI X RI
|
101
|
-
elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even?
|
102
|
-
false
|
103
|
-
# GB999. Any ÷ Any
|
104
|
-
else
|
105
|
-
true
|
106
|
-
end
|
9
|
+
UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
|
107
10
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
end
|
113
|
-
unpacked
|
11
|
+
def default_normalization_form
|
12
|
+
ActiveSupport::Deprecation.warn(
|
13
|
+
"ActiveSupport::Multibyte::Unicode.default_normalization_form is deprecated and will be removed in Rails 7.0."
|
14
|
+
)
|
114
15
|
end
|
115
16
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
unpacked.flatten.pack("U*")
|
121
|
-
end
|
122
|
-
|
123
|
-
# Re-order codepoints so the string becomes canonical.
|
124
|
-
def reorder_characters(codepoints)
|
125
|
-
length = codepoints.length - 1
|
126
|
-
pos = 0
|
127
|
-
while pos < length do
|
128
|
-
cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos + 1]]
|
129
|
-
if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0)
|
130
|
-
codepoints[pos..pos + 1] = cp2.code, cp1.code
|
131
|
-
pos += (pos > 0 ? -1 : 1)
|
132
|
-
else
|
133
|
-
pos += 1
|
134
|
-
end
|
135
|
-
end
|
136
|
-
codepoints
|
17
|
+
def default_normalization_form=(_)
|
18
|
+
ActiveSupport::Deprecation.warn(
|
19
|
+
"ActiveSupport::Multibyte::Unicode.default_normalization_form= is deprecated and will be removed in Rails 7.0."
|
20
|
+
)
|
137
21
|
end
|
138
22
|
|
139
23
|
# Decompose composed characters to the decomposed form.
|
140
24
|
def decompose(type, codepoints)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
ncp = [] # new codepoints
|
146
|
-
ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT
|
147
|
-
ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
|
148
|
-
tindex = sindex % HANGUL_TCOUNT
|
149
|
-
ncp << (HANGUL_TBASE + tindex) unless tindex == 0
|
150
|
-
decomposed.concat ncp
|
151
|
-
# if the codepoint is decomposable in with the current decomposition type
|
152
|
-
elsif (ncp = database.codepoints[cp].decomp_mapping) && (!database.codepoints[cp].decomp_type || type == :compatibility)
|
153
|
-
decomposed.concat decompose(type, ncp.dup)
|
154
|
-
else
|
155
|
-
decomposed << cp
|
156
|
-
end
|
25
|
+
if type == :compatibility
|
26
|
+
codepoints.pack("U*").unicode_normalize(:nfkd).codepoints
|
27
|
+
else
|
28
|
+
codepoints.pack("U*").unicode_normalize(:nfd).codepoints
|
157
29
|
end
|
158
30
|
end
|
159
31
|
|
160
32
|
# Compose decomposed characters to the composed form.
|
161
33
|
def compose(codepoints)
|
162
|
-
|
163
|
-
eoa = codepoints.length - 1
|
164
|
-
starter_pos = 0
|
165
|
-
starter_char = codepoints[0]
|
166
|
-
previous_combining_class = -1
|
167
|
-
while pos < eoa
|
168
|
-
pos += 1
|
169
|
-
lindex = starter_char - HANGUL_LBASE
|
170
|
-
# -- Hangul
|
171
|
-
if 0 <= lindex && lindex < HANGUL_LCOUNT
|
172
|
-
vindex = codepoints[starter_pos + 1] - HANGUL_VBASE rescue vindex = -1
|
173
|
-
if 0 <= vindex && vindex < HANGUL_VCOUNT
|
174
|
-
tindex = codepoints[starter_pos + 2] - HANGUL_TBASE rescue tindex = -1
|
175
|
-
if 0 <= tindex && tindex < HANGUL_TCOUNT
|
176
|
-
j = starter_pos + 2
|
177
|
-
eoa -= 2
|
178
|
-
else
|
179
|
-
tindex = 0
|
180
|
-
j = starter_pos + 1
|
181
|
-
eoa -= 1
|
182
|
-
end
|
183
|
-
codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE
|
184
|
-
end
|
185
|
-
starter_pos += 1
|
186
|
-
starter_char = codepoints[starter_pos]
|
187
|
-
# -- Other characters
|
188
|
-
else
|
189
|
-
current_char = codepoints[pos]
|
190
|
-
current = database.codepoints[current_char]
|
191
|
-
if current.combining_class > previous_combining_class
|
192
|
-
if ref = database.composition_map[starter_char]
|
193
|
-
composition = ref[current_char]
|
194
|
-
else
|
195
|
-
composition = nil
|
196
|
-
end
|
197
|
-
unless composition.nil?
|
198
|
-
codepoints[starter_pos] = composition
|
199
|
-
starter_char = composition
|
200
|
-
codepoints.delete_at pos
|
201
|
-
eoa -= 1
|
202
|
-
pos -= 1
|
203
|
-
previous_combining_class = -1
|
204
|
-
else
|
205
|
-
previous_combining_class = current.combining_class
|
206
|
-
end
|
207
|
-
else
|
208
|
-
previous_combining_class = current.combining_class
|
209
|
-
end
|
210
|
-
if current.combining_class == 0
|
211
|
-
starter_pos = pos
|
212
|
-
starter_char = codepoints[pos]
|
213
|
-
end
|
214
|
-
end
|
215
|
-
end
|
216
|
-
codepoints
|
34
|
+
codepoints.pack("U*").unicode_normalize(:nfc).codepoints
|
217
35
|
end
|
218
36
|
|
219
37
|
# Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
|
@@ -224,7 +42,7 @@ module ActiveSupport
|
|
224
42
|
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
|
225
43
|
# encoding is entirely CP1252 or ISO-8859-1.
|
226
44
|
def tidy_bytes(string, force = false)
|
227
|
-
return string if string.empty?
|
45
|
+
return string if string.empty? || string.ascii_only?
|
228
46
|
return recode_windows1252_chars(string) if force
|
229
47
|
string.scrub { |bad| recode_windows1252_chars(bad) }
|
230
48
|
end
|
@@ -255,140 +73,10 @@ module ActiveSupport
|
|
255
73
|
end
|
256
74
|
end
|
257
75
|
|
258
|
-
# Returns the KC normalization of the string by default. NFKC is
|
259
|
-
# considered the best normalization form for passing strings to databases
|
260
|
-
# and validations.
|
261
|
-
#
|
262
|
-
# * <tt>string</tt> - The string to perform normalization on.
|
263
|
-
# * <tt>form</tt> - The form you want to normalize in. Should be one of
|
264
|
-
# the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>.
|
265
|
-
# Default is ActiveSupport::Multibyte::Unicode.default_normalization_form.
|
266
|
-
def normalize(string, form = nil)
|
267
|
-
form ||= @default_normalization_form
|
268
|
-
# See http://www.unicode.org/reports/tr15, Table 1
|
269
|
-
codepoints = string.codepoints.to_a
|
270
|
-
case form
|
271
|
-
when :d
|
272
|
-
reorder_characters(decompose(:canonical, codepoints))
|
273
|
-
when :c
|
274
|
-
compose(reorder_characters(decompose(:canonical, codepoints)))
|
275
|
-
when :kd
|
276
|
-
reorder_characters(decompose(:compatibility, codepoints))
|
277
|
-
when :kc
|
278
|
-
compose(reorder_characters(decompose(:compatibility, codepoints)))
|
279
|
-
else
|
280
|
-
raise ArgumentError, "#{form} is not a valid normalization variant", caller
|
281
|
-
end.pack("U*".freeze)
|
282
|
-
end
|
283
|
-
|
284
|
-
def downcase(string)
|
285
|
-
apply_mapping string, :lowercase_mapping
|
286
|
-
end
|
287
|
-
|
288
|
-
def upcase(string)
|
289
|
-
apply_mapping string, :uppercase_mapping
|
290
|
-
end
|
291
|
-
|
292
|
-
def swapcase(string)
|
293
|
-
apply_mapping string, :swapcase_mapping
|
294
|
-
end
|
295
|
-
|
296
|
-
# Holds data about a codepoint in the Unicode database.
|
297
|
-
class Codepoint
|
298
|
-
attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
|
299
|
-
|
300
|
-
# Initializing Codepoint object with default values
|
301
|
-
def initialize
|
302
|
-
@combining_class = 0
|
303
|
-
@uppercase_mapping = 0
|
304
|
-
@lowercase_mapping = 0
|
305
|
-
end
|
306
|
-
|
307
|
-
def swapcase_mapping
|
308
|
-
uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
|
309
|
-
end
|
310
|
-
end
|
311
|
-
|
312
|
-
# Holds static data from the Unicode database.
|
313
|
-
class UnicodeDatabase
|
314
|
-
ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
|
315
|
-
|
316
|
-
attr_writer(*ATTRIBUTES)
|
317
|
-
|
318
|
-
def initialize
|
319
|
-
@codepoints = Hash.new(Codepoint.new)
|
320
|
-
@composition_exclusion = []
|
321
|
-
@composition_map = {}
|
322
|
-
@boundary = {}
|
323
|
-
@cp1252 = {}
|
324
|
-
end
|
325
|
-
|
326
|
-
# Lazy load the Unicode database so it's only loaded when it's actually used
|
327
|
-
ATTRIBUTES.each do |attr_name|
|
328
|
-
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
329
|
-
def #{attr_name} # def codepoints
|
330
|
-
load # load
|
331
|
-
@#{attr_name} # @codepoints
|
332
|
-
end # end
|
333
|
-
EOS
|
334
|
-
end
|
335
|
-
|
336
|
-
# Loads the Unicode database and returns all the internal objects of
|
337
|
-
# UnicodeDatabase.
|
338
|
-
def load
|
339
|
-
begin
|
340
|
-
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, "rb") { |f| Marshal.load f.read }
|
341
|
-
rescue => e
|
342
|
-
raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
|
343
|
-
end
|
344
|
-
|
345
|
-
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
|
346
|
-
@boundary.each_key do |k|
|
347
|
-
@boundary[k].instance_eval do
|
348
|
-
def ===(other)
|
349
|
-
detect { |i| i === other } ? true : false
|
350
|
-
end
|
351
|
-
end if @boundary[k].kind_of?(Array)
|
352
|
-
end
|
353
|
-
|
354
|
-
# define attr_reader methods for the instance variables
|
355
|
-
class << self
|
356
|
-
attr_reader(*ATTRIBUTES)
|
357
|
-
end
|
358
|
-
end
|
359
|
-
|
360
|
-
# Returns the directory in which the data files are stored.
|
361
|
-
def self.dirname
|
362
|
-
File.expand_path("../values", __dir__)
|
363
|
-
end
|
364
|
-
|
365
|
-
# Returns the filename for the data file for this version.
|
366
|
-
def self.filename
|
367
|
-
File.expand_path File.join(dirname, "unicode_tables.dat")
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
76
|
private
|
372
|
-
|
373
|
-
def apply_mapping(string, mapping)
|
374
|
-
database.codepoints
|
375
|
-
string.each_codepoint.map do |codepoint|
|
376
|
-
cp = database.codepoints[codepoint]
|
377
|
-
if cp && (ncp = cp.send(mapping)) && ncp > 0
|
378
|
-
ncp
|
379
|
-
else
|
380
|
-
codepoint
|
381
|
-
end
|
382
|
-
end.pack("U*")
|
383
|
-
end
|
384
|
-
|
385
77
|
def recode_windows1252_chars(string)
|
386
78
|
string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
|
387
79
|
end
|
388
|
-
|
389
|
-
def database
|
390
|
-
@database ||= UnicodeDatabase.new
|
391
|
-
end
|
392
80
|
end
|
393
81
|
end
|
394
82
|
end
|