activesupport 5.2.4 → 6.0.2.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.

Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -395
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -2
  5. data/lib/active_support.rb +2 -1
  6. data/lib/active_support/actionable_error.rb +48 -0
  7. data/lib/active_support/backtrace_cleaner.rb +28 -1
  8. data/lib/active_support/cache.rb +45 -23
  9. data/lib/active_support/cache/file_store.rb +22 -22
  10. data/lib/active_support/cache/mem_cache_store.rb +5 -0
  11. data/lib/active_support/cache/memory_store.rb +9 -2
  12. data/lib/active_support/cache/null_store.rb +5 -0
  13. data/lib/active_support/cache/redis_cache_store.rb +37 -10
  14. data/lib/active_support/callbacks.rb +16 -5
  15. data/lib/active_support/concern.rb +24 -1
  16. data/lib/active_support/configurable.rb +7 -11
  17. data/lib/active_support/core_ext/array.rb +1 -1
  18. data/lib/active_support/core_ext/array/access.rb +18 -6
  19. data/lib/active_support/core_ext/array/extract.rb +21 -0
  20. data/lib/active_support/core_ext/array/prepend_and_append.rb +2 -6
  21. data/lib/active_support/core_ext/class/attribute.rb +11 -16
  22. data/lib/active_support/core_ext/class/subclasses.rb +1 -1
  23. data/lib/active_support/core_ext/date/calculations.rb +6 -5
  24. data/lib/active_support/core_ext/date_and_time/calculations.rb +24 -47
  25. data/lib/active_support/core_ext/date_time/calculations.rb +1 -1
  26. data/lib/active_support/core_ext/enumerable.rb +97 -73
  27. data/lib/active_support/core_ext/hash.rb +1 -2
  28. data/lib/active_support/core_ext/hash/compact.rb +2 -26
  29. data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
  30. data/lib/active_support/core_ext/hash/except.rb +1 -1
  31. data/lib/active_support/core_ext/hash/keys.rb +0 -29
  32. data/lib/active_support/core_ext/hash/slice.rb +3 -25
  33. data/lib/active_support/core_ext/hash/transform_values.rb +2 -29
  34. data/lib/active_support/core_ext/integer/multiple.rb +1 -1
  35. data/lib/active_support/core_ext/kernel.rb +0 -1
  36. data/lib/active_support/core_ext/load_error.rb +1 -1
  37. data/lib/active_support/core_ext/module.rb +0 -1
  38. data/lib/active_support/core_ext/module/attribute_accessors.rb +7 -10
  39. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +13 -19
  40. data/lib/active_support/core_ext/module/delegation.rb +33 -7
  41. data/lib/active_support/core_ext/module/introspection.rb +37 -13
  42. data/lib/active_support/core_ext/module/reachable.rb +1 -6
  43. data/lib/active_support/core_ext/module/redefine_method.rb +8 -17
  44. data/lib/active_support/core_ext/numeric.rb +0 -1
  45. data/lib/active_support/core_ext/numeric/conversions.rb +124 -128
  46. data/lib/active_support/core_ext/numeric/inquiry.rb +2 -25
  47. data/lib/active_support/core_ext/object/blank.rb +1 -2
  48. data/lib/active_support/core_ext/object/duplicable.rb +7 -114
  49. data/lib/active_support/core_ext/object/json.rb +1 -0
  50. data/lib/active_support/core_ext/object/try.rb +15 -7
  51. data/lib/active_support/core_ext/object/with_options.rb +1 -1
  52. data/lib/active_support/core_ext/range/compare_range.rb +28 -13
  53. data/lib/active_support/core_ext/range/conversions.rb +31 -29
  54. data/lib/active_support/core_ext/range/include_range.rb +6 -0
  55. data/lib/active_support/core_ext/range/include_time_with_zone.rb +2 -2
  56. data/lib/active_support/core_ext/regexp.rb +0 -4
  57. data/lib/active_support/core_ext/securerandom.rb +23 -3
  58. data/lib/active_support/core_ext/string/access.rb +8 -0
  59. data/lib/active_support/core_ext/string/filters.rb +42 -1
  60. data/lib/active_support/core_ext/string/inflections.rb +7 -2
  61. data/lib/active_support/core_ext/string/multibyte.rb +4 -3
  62. data/lib/active_support/core_ext/string/output_safety.rb +63 -5
  63. data/lib/active_support/core_ext/string/strip.rb +3 -1
  64. data/lib/active_support/core_ext/time/calculations.rb +31 -2
  65. data/lib/active_support/core_ext/uri.rb +1 -0
  66. data/lib/active_support/current_attributes.rb +8 -0
  67. data/lib/active_support/dependencies.rb +69 -16
  68. data/lib/active_support/dependencies/zeitwerk_integration.rb +118 -0
  69. data/lib/active_support/deprecation.rb +1 -1
  70. data/lib/active_support/deprecation/behaviors.rb +1 -1
  71. data/lib/active_support/deprecation/method_wrappers.rb +8 -20
  72. data/lib/active_support/deprecation/proxy_wrappers.rb +24 -5
  73. data/lib/active_support/descendants_tracker.rb +56 -9
  74. data/lib/active_support/duration.rb +6 -5
  75. data/lib/active_support/duration/iso8601_parser.rb +2 -3
  76. data/lib/active_support/duration/iso8601_serializer.rb +3 -4
  77. data/lib/active_support/encrypted_configuration.rb +0 -4
  78. data/lib/active_support/encrypted_file.rb +2 -1
  79. data/lib/active_support/evented_file_update_checker.rb +39 -9
  80. data/lib/active_support/execution_wrapper.rb +1 -0
  81. data/lib/active_support/gem_version.rb +4 -4
  82. data/lib/active_support/hash_with_indifferent_access.rb +22 -18
  83. data/lib/active_support/i18n.rb +1 -0
  84. data/lib/active_support/i18n_railtie.rb +13 -1
  85. data/lib/active_support/inflector/inflections.rb +1 -4
  86. data/lib/active_support/inflector/methods.rb +15 -27
  87. data/lib/active_support/inflector/transliterate.rb +47 -18
  88. data/lib/active_support/json/decoding.rb +23 -23
  89. data/lib/active_support/json/encoding.rb +6 -2
  90. data/lib/active_support/key_generator.rb +0 -32
  91. data/lib/active_support/lazy_load_hooks.rb +5 -1
  92. data/lib/active_support/locale/en.rb +31 -0
  93. data/lib/active_support/log_subscriber.rb +31 -8
  94. data/lib/active_support/logger.rb +0 -15
  95. data/lib/active_support/logger_silence.rb +28 -12
  96. data/lib/active_support/logger_thread_safe_level.rb +26 -4
  97. data/lib/active_support/message_encryptor.rb +3 -5
  98. data/lib/active_support/message_verifier.rb +3 -3
  99. data/lib/active_support/multibyte/chars.rb +29 -48
  100. data/lib/active_support/multibyte/unicode.rb +44 -281
  101. data/lib/active_support/notifications.rb +41 -4
  102. data/lib/active_support/notifications/fanout.rb +98 -13
  103. data/lib/active_support/notifications/instrumenter.rb +80 -8
  104. data/lib/active_support/number_helper.rb +7 -0
  105. data/lib/active_support/number_helper/number_to_currency_converter.rb +2 -2
  106. data/lib/active_support/number_helper/number_to_delimited_converter.rb +3 -1
  107. data/lib/active_support/number_helper/number_to_human_converter.rb +3 -1
  108. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -1
  109. data/lib/active_support/number_helper/number_to_percentage_converter.rb +3 -1
  110. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -0
  111. data/lib/active_support/number_helper/number_to_rounded_converter.rb +5 -3
  112. data/lib/active_support/ordered_hash.rb +1 -1
  113. data/lib/active_support/ordered_options.rb +1 -1
  114. data/lib/active_support/parameter_filter.rb +129 -0
  115. data/lib/active_support/rails.rb +0 -6
  116. data/lib/active_support/reloader.rb +4 -5
  117. data/lib/active_support/security_utils.rb +1 -1
  118. data/lib/active_support/subscriber.rb +65 -26
  119. data/lib/active_support/tagged_logging.rb +13 -4
  120. data/lib/active_support/test_case.rb +91 -0
  121. data/lib/active_support/testing/assertions.rb +15 -1
  122. data/lib/active_support/testing/deprecation.rb +0 -1
  123. data/lib/active_support/testing/file_fixtures.rb +2 -0
  124. data/lib/active_support/testing/isolation.rb +2 -2
  125. data/lib/active_support/testing/method_call_assertions.rb +28 -1
  126. data/lib/active_support/testing/parallelization.rb +128 -0
  127. data/lib/active_support/testing/stream.rb +1 -1
  128. data/lib/active_support/testing/time_helpers.rb +7 -7
  129. data/lib/active_support/time_with_zone.rb +15 -5
  130. data/lib/active_support/values/time_zone.rb +12 -7
  131. data/lib/active_support/xml_mini.rb +2 -9
  132. data/lib/active_support/xml_mini/jdom.rb +2 -2
  133. data/lib/active_support/xml_mini/libxml.rb +2 -2
  134. data/lib/active_support/xml_mini/libxmlsax.rb +4 -4
  135. data/lib/active_support/xml_mini/nokogiri.rb +2 -2
  136. data/lib/active_support/xml_mini/nokogirisax.rb +3 -3
  137. data/lib/active_support/xml_mini/rexml.rb +2 -2
  138. metadata +30 -8
  139. data/lib/active_support/core_ext/kernel/agnostics.rb +0 -13
  140. data/lib/active_support/values/unicode_tables.dat +0 -0
@@ -52,16 +52,17 @@ module ActiveSupport
52
52
  @pid = Process.pid
53
53
  @boot_mutex = Mutex.new
54
54
 
55
- if (@dtw = directories_to_watch).any?
55
+ dtw = directories_to_watch
56
+ @dtw, @missing = dtw.partition(&:exist?)
57
+
58
+ if @dtw.any?
56
59
  # Loading listen triggers warnings. These are originated by a legit
57
60
  # usage of attr_* macros for private attributes, but adds a lot of noise
58
61
  # to our test suite. Thus, we lazy load it and disable warnings locally.
59
62
  silence_warnings do
60
- begin
61
- require "listen"
62
- rescue LoadError => e
63
- raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
64
- end
63
+ require "listen"
64
+ rescue LoadError => e
65
+ raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
65
66
  end
66
67
  end
67
68
  boot!
@@ -75,6 +76,19 @@ module ActiveSupport
75
76
  @updated.make_true
76
77
  end
77
78
  end
79
+
80
+ if @missing.any?(&:exist?)
81
+ @boot_mutex.synchronize do
82
+ appeared, @missing = @missing.partition(&:exist?)
83
+ shutdown!
84
+
85
+ @dtw += appeared
86
+ boot!
87
+
88
+ @updated.make_true
89
+ end
90
+ end
91
+
78
92
  @updated.true?
79
93
  end
80
94
 
@@ -93,7 +107,21 @@ module ActiveSupport
93
107
 
94
108
  private
95
109
  def boot!
96
- Listen.to(*@dtw, &method(:changed)).start
110
+ normalize_dirs!
111
+
112
+ unless @dtw.empty?
113
+ Listen.to(*@dtw, &method(:changed)).start
114
+ end
115
+ end
116
+
117
+ def shutdown!
118
+ Listen.stop
119
+ end
120
+
121
+ def normalize_dirs!
122
+ @dirs.transform_keys! do |dir|
123
+ dir.exist? ? dir.realpath : dir
124
+ end
97
125
  end
98
126
 
99
127
  def changed(modified, added, removed)
@@ -113,7 +141,9 @@ module ActiveSupport
113
141
  ext = @ph.normalize_extension(file.extname)
114
142
 
115
143
  file.dirname.ascend do |dir|
116
- if @dirs.fetch(dir, []).include?(ext)
144
+ matching = @dirs[dir]
145
+
146
+ if matching && (matching.empty? || matching.include?(ext))
117
147
  break true
118
148
  elsif dir == @lcsp || dir.root?
119
149
  break false
@@ -123,7 +153,7 @@ module ActiveSupport
123
153
  end
124
154
 
125
155
  def directories_to_watch
126
- dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) }
156
+ dtw = @files.map(&:dirname) + @dirs.keys
127
157
  dtw.compact!
128
158
  dtw.uniq!
129
159
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/callbacks"
4
+ require "concurrent/hash"
4
5
 
5
6
  module ActiveSupport
6
7
  class ExecutionWrapper
@@ -7,10 +7,10 @@ module ActiveSupport
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 4
13
- PRE = nil
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 2
13
+ PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support/core_ext/hash/keys"
4
4
  require "active_support/core_ext/hash/reverse_merge"
5
+ require "active_support/core_ext/hash/except"
5
6
 
6
7
  module ActiveSupport
7
8
  # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
@@ -190,20 +191,18 @@ module ActiveSupport
190
191
  super(convert_key(key), *extras)
191
192
  end
192
193
 
193
- if Hash.new.respond_to?(:dig)
194
- # Same as <tt>Hash#dig</tt> where the key passed as argument can be
195
- # either a string or a symbol:
196
- #
197
- # counters = ActiveSupport::HashWithIndifferentAccess.new
198
- # counters[:foo] = { bar: 1 }
199
- #
200
- # counters.dig('foo', 'bar') # => 1
201
- # counters.dig(:foo, :bar) # => 1
202
- # counters.dig(:zoo) # => nil
203
- def dig(*args)
204
- args[0] = convert_key(args[0]) if args.size > 0
205
- super(*args)
206
- end
194
+ # Same as <tt>Hash#dig</tt> where the key passed as argument can be
195
+ # either a string or a symbol:
196
+ #
197
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
198
+ # counters[:foo] = { bar: 1 }
199
+ #
200
+ # counters.dig('foo', 'bar') # => 1
201
+ # counters.dig(:foo, :bar) # => 1
202
+ # counters.dig(:zoo) # => nil
203
+ def dig(*args)
204
+ args[0] = convert_key(args[0]) if args.size > 0
205
+ super(*args)
207
206
  end
208
207
 
209
208
  # Same as <tt>Hash#default</tt> where the key passed as argument can be
@@ -226,8 +225,8 @@ module ActiveSupport
226
225
  # hash[:a] = 'x'
227
226
  # hash[:b] = 'y'
228
227
  # hash.values_at('a', 'b') # => ["x", "y"]
229
- def values_at(*indices)
230
- indices.collect { |key| self[convert_key(key)] }
228
+ def values_at(*keys)
229
+ super(*keys.map { |key| convert_key(key) })
231
230
  end
232
231
 
233
232
  # Returns an array of the values at the specified indices, but also
@@ -240,8 +239,8 @@ module ActiveSupport
240
239
  # hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
241
240
  # hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
242
241
  def fetch_values(*indices, &block)
243
- indices.collect { |key| fetch(key, &block) }
244
- end if Hash.method_defined?(:fetch_values)
242
+ super(*indices.map { |key| convert_key(key) }, &block)
243
+ end
245
244
 
246
245
  # Returns a shallow copy of the hash.
247
246
  #
@@ -294,6 +293,11 @@ module ActiveSupport
294
293
  super(convert_key(key))
295
294
  end
296
295
 
296
+ def except(*keys)
297
+ slice(*self.keys - keys.map { |key| convert_key(key) })
298
+ end
299
+ alias_method :without, :except
300
+
297
301
  def stringify_keys!; self end
298
302
  def deep_stringify_keys!; self end
299
303
  def stringify_keys; dup end
@@ -13,3 +13,4 @@ require "active_support/lazy_load_hooks"
13
13
 
14
14
  ActiveSupport.run_load_hooks(:i18n)
15
15
  I18n.load_path << File.expand_path("locale/en.yml", __dir__)
16
+ I18n.load_path << File.expand_path("locale/en.rb", __dir__)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support"
4
- require "active_support/file_update_checker"
5
4
  require "active_support/core_ext/array/wrap"
6
5
 
7
6
  # :enddoc:
@@ -13,6 +12,10 @@ module I18n
13
12
  config.i18n.load_path = []
14
13
  config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
15
14
 
15
+ if I18n.respond_to?(:eager_load!)
16
+ config.eager_load_namespaces << I18n
17
+ end
18
+
16
19
  # Set the i18n configuration after initialization since a lot of
17
20
  # configuration is still usually done in application initializers.
18
21
  config.after_initialize do |app|
@@ -92,6 +95,15 @@ module I18n
92
95
  end
93
96
 
94
97
  if args.empty? || args.first.is_a?(Hash)
98
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
99
+ Using I18n fallbacks with an empty `defaults` sets the defaults to
100
+ include the `default_locale`. This behavior will change in Rails 6.1.
101
+ If you desire the default locale to be included in the defaults, please
102
+ explicitly configure it with `config.i18n.fallbacks.defaults =
103
+ [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale,
104
+ {...}]`. If you want to opt-in to the new behavior, use
105
+ `config.i18n.fallbacks.defaults = [nil, {...}]`.
106
+ MSG
95
107
  args.unshift I18n.default_locale
96
108
  end
97
109
 
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent/map"
4
- require "active_support/core_ext/array/prepend_and_append"
5
- require "active_support/core_ext/regexp"
6
4
  require "active_support/i18n"
7
5
  require "active_support/deprecation"
8
6
 
@@ -67,8 +65,7 @@ module ActiveSupport
67
65
  @__instance__[locale] ||= new
68
66
  end
69
67
 
70
- attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
71
- deprecate :acronym_regex
68
+ attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms
72
69
 
73
70
  attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
74
71
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/inflections"
4
- require "active_support/core_ext/regexp"
5
4
 
6
5
  module ActiveSupport
7
6
  # The Inflector transforms words from singular to plural, class names to table
@@ -74,7 +73,7 @@ module ActiveSupport
74
73
  string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase }
75
74
  end
76
75
  string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
77
- string.gsub!("/".freeze, "::".freeze)
76
+ string.gsub!("/", "::")
78
77
  string
79
78
  end
80
79
 
@@ -91,11 +90,11 @@ module ActiveSupport
91
90
  # camelize(underscore('SSLError')) # => "SslError"
92
91
  def underscore(camel_cased_word)
93
92
  return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
94
- word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze)
95
- word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_'.freeze }#{$2.downcase}" }
96
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
97
- word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
98
- word.tr!("-".freeze, "_".freeze)
93
+ word = camel_cased_word.to_s.gsub("::", "/")
94
+ word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
95
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
96
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
97
+ word.tr!("-", "_")
99
98
  word.downcase!
100
99
  word
101
100
  end
@@ -131,11 +130,11 @@ module ActiveSupport
131
130
 
132
131
  inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
133
132
 
134
- result.sub!(/\A_+/, "".freeze)
133
+ result.sub!(/\A_+/, "")
135
134
  unless keep_id_suffix
136
- result.sub!(/_id\z/, "".freeze)
135
+ result.sub!(/_id\z/, "")
137
136
  end
138
- result.tr!("_".freeze, " ".freeze)
137
+ result.tr!("_", " ")
139
138
 
140
139
  result.gsub!(/([a-z\d]*)/i) do |match|
141
140
  "#{inflections.acronyms[match.downcase] || match.downcase}"
@@ -200,14 +199,14 @@ module ActiveSupport
200
199
  # classify('calculus') # => "Calculus"
201
200
  def classify(table_name)
202
201
  # strip out any leading schema name
203
- camelize(singularize(table_name.to_s.sub(/.*\./, "".freeze)))
202
+ camelize(singularize(table_name.to_s.sub(/.*\./, "")))
204
203
  end
205
204
 
206
205
  # Replaces underscores with dashes in the string.
207
206
  #
208
207
  # dasherize('puni_puni') # => "puni-puni"
209
208
  def dasherize(underscored_word)
210
- underscored_word.tr("_".freeze, "-".freeze)
209
+ underscored_word.tr("_", "-")
211
210
  end
212
211
 
213
212
  # Removes the module part from the expression in the string.
@@ -270,7 +269,7 @@ module ActiveSupport
270
269
  # NameError is raised when the name is not in CamelCase or the constant is
271
270
  # unknown.
272
271
  def constantize(camel_cased_word)
273
- names = camel_cased_word.split("::".freeze)
272
+ names = camel_cased_word.split("::")
274
273
 
275
274
  # Trigger a built-in NameError exception including the ill-formed constant in the message.
276
275
  Object.const_get(camel_cased_word) if names.empty?
@@ -343,18 +342,7 @@ module ActiveSupport
343
342
  # ordinal(-11) # => "th"
344
343
  # ordinal(-1021) # => "st"
345
344
  def ordinal(number)
346
- abs_number = number.to_i.abs
347
-
348
- if (11..13).include?(abs_number % 100)
349
- "th"
350
- else
351
- case abs_number % 10
352
- when 1; "st"
353
- when 2; "nd"
354
- when 3; "rd"
355
- else "th"
356
- end
357
- end
345
+ I18n.translate("number.nth.ordinals", number: number)
358
346
  end
359
347
 
360
348
  # Turns a number into an ordinal string used to denote the position in an
@@ -367,7 +355,7 @@ module ActiveSupport
367
355
  # ordinalize(-11) # => "-11th"
368
356
  # ordinalize(-1021) # => "-1021st"
369
357
  def ordinalize(number)
370
- "#{number}#{ordinal(number)}"
358
+ I18n.translate("number.nth.ordinalized", number: number)
371
359
  end
372
360
 
373
361
  private
@@ -378,7 +366,7 @@ module ActiveSupport
378
366
  # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
379
367
  # const_regexp("::") # => "::"
380
368
  def const_regexp(camel_cased_word)
381
- parts = camel_cased_word.split("::".freeze)
369
+ parts = camel_cased_word.split("::")
382
370
 
383
371
  return Regexp.escape(camel_cased_word) if parts.blank?
384
372
 
@@ -51,20 +51,45 @@ module ActiveSupport
51
51
  #
52
52
  # Now you can have different transliterations for each locale:
53
53
  #
54
- # I18n.locale = :en
55
- # transliterate('Jürgen')
54
+ # transliterate('Jürgen', locale: :en)
56
55
  # # => "Jurgen"
57
56
  #
58
- # I18n.locale = :de
59
- # transliterate('Jürgen')
57
+ # transliterate('Jürgen', locale: :de)
60
58
  # # => "Juergen"
61
- def transliterate(string, replacement = "?".freeze)
59
+ #
60
+ # Transliteration is restricted to UTF-8, US-ASCII and GB18030 strings
61
+ # Other encodings will raise an ArgumentError.
62
+ def transliterate(string, replacement = "?", locale: nil)
63
+ string = string.dup if string.frozen?
62
64
  raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
63
65
 
64
- I18n.transliterate(
65
- ActiveSupport::Multibyte::Unicode.normalize(
66
- ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
67
- replacement: replacement)
66
+ allowed_encodings = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::GB18030]
67
+ raise ArgumentError, "Can not transliterate strings with #{string.encoding} encoding" unless allowed_encodings.include?(string.encoding)
68
+
69
+ input_encoding = string.encoding
70
+
71
+ # US-ASCII is a subset of UTF-8 so we'll force encoding as UTF-8 if
72
+ # US-ASCII is given. This way we can let tidy_bytes handle the string
73
+ # in the same way as we do for UTF-8
74
+ string.force_encoding(Encoding::UTF_8) if string.encoding == Encoding::US_ASCII
75
+
76
+ # GB18030 is Unicode compatible but is not a direct mapping so needs to be
77
+ # transcoded. Using invalid/undef :replace will result in loss of data in
78
+ # the event of invalid characters, but since tidy_bytes will replace
79
+ # invalid/undef with a "?" we're safe to do the same beforehand
80
+ string.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) if string.encoding == Encoding::GB18030
81
+
82
+ transliterated = I18n.transliterate(
83
+ ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
84
+ replacement: replacement,
85
+ locale: locale
86
+ )
87
+
88
+ # Restore the string encoding of the input if it was not UTF-8.
89
+ # Apply invalid/undef :replace as tidy_bytes does
90
+ transliterated.encode!(input_encoding, invalid: :replace, undef: :replace) if input_encoding != transliterated.encoding
91
+
92
+ transliterated
68
93
  end
69
94
 
70
95
  # Replaces special characters in a string so that it may be used as part of
@@ -75,8 +100,8 @@ module ActiveSupport
75
100
  #
76
101
  # To use a custom separator, override the +separator+ argument.
77
102
  #
78
- # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
79
- # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
103
+ # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
104
+ # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
80
105
  #
81
106
  # To preserve the case of the characters in a string, use the +preserve_case+ argument.
82
107
  #
@@ -85,19 +110,23 @@ module ActiveSupport
85
110
  #
86
111
  # It preserves dashes and underscores unless they are used as separators:
87
112
  #
88
- # parameterize("^très|Jolie__ ") # => "tres-jolie__"
89
- # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
90
- # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
113
+ # parameterize("^très|Jolie__ ") # => "tres-jolie__"
114
+ # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
115
+ # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
91
116
  #
92
- def parameterize(string, separator: "-", preserve_case: false)
117
+ # If the optional parameter +locale+ is specified,
118
+ # the word will be parameterized as a word of that language.
119
+ # By default, this parameter is set to <tt>nil</tt> and it will use
120
+ # the configured <tt>I18n.locale<tt>.
121
+ def parameterize(string, separator: "-", preserve_case: false, locale: nil)
93
122
  # Replace accented chars with their ASCII equivalents.
94
- parameterized_string = transliterate(string)
123
+ parameterized_string = transliterate(string, locale: locale)
95
124
 
96
125
  # Turn unwanted chars into the separator.
97
126
  parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
98
127
 
99
128
  unless separator.nil? || separator.empty?
100
- if separator == "-".freeze
129
+ if separator == "-"
101
130
  re_duplicate_separator = /-{2,}/
102
131
  re_leading_trailing_separator = /^-|-$/i
103
132
  else
@@ -108,7 +137,7 @@ module ActiveSupport
108
137
  # No more than one of the separator in a row.
109
138
  parameterized_string.gsub!(re_duplicate_separator, separator)
110
139
  # Remove leading/trailing separator.
111
- parameterized_string.gsub!(re_leading_trailing_separator, "".freeze)
140
+ parameterized_string.gsub!(re_leading_trailing_separator, "")
112
141
  end
113
142
 
114
143
  parameterized_string.downcase! unless preserve_case