actionview 5.2.4.4 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +221 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -3
  5. data/lib/action_view.rb +7 -2
  6. data/lib/action_view/base.rb +81 -15
  7. data/lib/action_view/buffers.rb +15 -0
  8. data/lib/action_view/cache_expiry.rb +52 -0
  9. data/lib/action_view/context.rb +5 -9
  10. data/lib/action_view/dependency_tracker.rb +10 -4
  11. data/lib/action_view/digestor.rb +15 -22
  12. data/lib/action_view/flows.rb +0 -1
  13. data/lib/action_view/gem_version.rb +4 -4
  14. data/lib/action_view/helpers.rb +0 -2
  15. data/lib/action_view/helpers/active_model_helper.rb +0 -1
  16. data/lib/action_view/helpers/asset_tag_helper.rb +63 -46
  17. data/lib/action_view/helpers/asset_url_helper.rb +9 -6
  18. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  19. data/lib/action_view/helpers/cache_helper.rb +23 -22
  20. data/lib/action_view/helpers/capture_helper.rb +4 -0
  21. data/lib/action_view/helpers/csp_helper.rb +4 -2
  22. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  23. data/lib/action_view/helpers/date_helper.rb +73 -30
  24. data/lib/action_view/helpers/form_helper.rb +305 -37
  25. data/lib/action_view/helpers/form_options_helper.rb +23 -23
  26. data/lib/action_view/helpers/form_tag_helper.rb +19 -16
  27. data/lib/action_view/helpers/javascript_helper.rb +12 -11
  28. data/lib/action_view/helpers/number_helper.rb +14 -8
  29. data/lib/action_view/helpers/output_safety_helper.rb +1 -1
  30. data/lib/action_view/helpers/rendering_helper.rb +17 -7
  31. data/lib/action_view/helpers/sanitize_helper.rb +12 -18
  32. data/lib/action_view/helpers/tag_helper.rb +98 -22
  33. data/lib/action_view/helpers/tags/base.rb +18 -11
  34. data/lib/action_view/helpers/tags/check_box.rb +0 -1
  35. data/lib/action_view/helpers/tags/collection_check_boxes.rb +0 -1
  36. data/lib/action_view/helpers/tags/collection_helpers.rb +0 -1
  37. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +0 -1
  38. data/lib/action_view/helpers/tags/color_field.rb +1 -2
  39. data/lib/action_view/helpers/tags/date_field.rb +1 -2
  40. data/lib/action_view/helpers/tags/date_select.rb +2 -3
  41. data/lib/action_view/helpers/tags/datetime_field.rb +0 -1
  42. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -2
  43. data/lib/action_view/helpers/tags/label.rb +4 -1
  44. data/lib/action_view/helpers/tags/month_field.rb +1 -2
  45. data/lib/action_view/helpers/tags/radio_button.rb +0 -1
  46. data/lib/action_view/helpers/tags/select.rb +1 -2
  47. data/lib/action_view/helpers/tags/text_field.rb +0 -1
  48. data/lib/action_view/helpers/tags/time_field.rb +1 -2
  49. data/lib/action_view/helpers/tags/translator.rb +1 -6
  50. data/lib/action_view/helpers/tags/week_field.rb +1 -2
  51. data/lib/action_view/helpers/text_helper.rb +3 -4
  52. data/lib/action_view/helpers/translation_helper.rb +93 -55
  53. data/lib/action_view/helpers/url_helper.rb +121 -27
  54. data/lib/action_view/layouts.rb +8 -10
  55. data/lib/action_view/log_subscriber.rb +30 -15
  56. data/lib/action_view/lookup_context.rb +63 -35
  57. data/lib/action_view/path_set.rb +3 -12
  58. data/lib/action_view/railtie.rb +42 -26
  59. data/lib/action_view/record_identifier.rb +2 -3
  60. data/lib/action_view/renderer/abstract_renderer.rb +142 -11
  61. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  62. data/lib/action_view/renderer/object_renderer.rb +34 -0
  63. data/lib/action_view/renderer/partial_renderer.rb +21 -273
  64. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +61 -16
  65. data/lib/action_view/renderer/renderer.rb +59 -4
  66. data/lib/action_view/renderer/streaming_template_renderer.rb +10 -8
  67. data/lib/action_view/renderer/template_renderer.rb +35 -27
  68. data/lib/action_view/rendering.rb +54 -33
  69. data/lib/action_view/routing_url_for.rb +13 -12
  70. data/lib/action_view/template.rb +66 -75
  71. data/lib/action_view/template/error.rb +30 -15
  72. data/lib/action_view/template/handlers.rb +1 -1
  73. data/lib/action_view/template/handlers/builder.rb +2 -2
  74. data/lib/action_view/template/handlers/erb.rb +16 -11
  75. data/lib/action_view/template/handlers/erb/erubi.rb +15 -9
  76. data/lib/action_view/template/handlers/html.rb +1 -1
  77. data/lib/action_view/template/handlers/raw.rb +2 -2
  78. data/lib/action_view/template/html.rb +5 -6
  79. data/lib/action_view/template/inline.rb +22 -0
  80. data/lib/action_view/template/raw_file.rb +25 -0
  81. data/lib/action_view/template/renderable.rb +24 -0
  82. data/lib/action_view/template/resolver.rb +191 -150
  83. data/lib/action_view/template/sources.rb +13 -0
  84. data/lib/action_view/template/sources/file.rb +17 -0
  85. data/lib/action_view/template/text.rb +2 -3
  86. data/lib/action_view/test_case.rb +21 -29
  87. data/lib/action_view/testing/resolvers.rb +18 -27
  88. data/lib/action_view/unbound_template.rb +31 -0
  89. data/lib/action_view/view_paths.rb +59 -38
  90. data/lib/assets/compiled/rails-ujs.js +29 -3
  91. metadata +32 -21
  92. data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -14,7 +14,6 @@ module ActionView
14
14
  end
15
15
 
16
16
  private
17
-
18
17
  def format_date(value)
19
18
  raise NotImplementedError
20
19
  end
@@ -11,9 +11,8 @@ module ActionView
11
11
  end
12
12
 
13
13
  private
14
-
15
14
  def format_date(value)
16
- value.try(:strftime, "%Y-%m-%dT%T")
15
+ value&.strftime("%Y-%m-%dT%T")
17
16
  end
18
17
  end
19
18
  end
@@ -25,6 +25,10 @@ module ActionView
25
25
 
26
26
  content
27
27
  end
28
+
29
+ def to_s
30
+ translation
31
+ end
28
32
  end
29
33
 
30
34
  def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
@@ -71,7 +75,6 @@ module ActionView
71
75
  end
72
76
 
73
77
  private
74
-
75
78
  def render_component(builder)
76
79
  builder.translation
77
80
  end
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class MonthField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%Y-%m")
9
+ value&.strftime("%Y-%m")
11
10
  end
12
11
  end
13
12
  end
@@ -23,7 +23,6 @@ module ActionView
23
23
  end
24
24
 
25
25
  private
26
-
27
26
  def checked?(value)
28
27
  value.to_s == @tag_value.to_s
29
28
  end
@@ -15,7 +15,7 @@ module ActionView
15
15
 
16
16
  def render
17
17
  option_tags_options = {
18
- selected: @options.fetch(:selected) { value },
18
+ selected: @options.fetch(:selected) { value.nil? ? "" : value },
19
19
  disabled: @options[:disabled]
20
20
  }
21
21
 
@@ -29,7 +29,6 @@ module ActionView
29
29
  end
30
30
 
31
31
  private
32
-
33
32
  # Grouped choices look like this:
34
33
  #
35
34
  # [nil, []]
@@ -24,7 +24,6 @@ module ActionView
24
24
  end
25
25
 
26
26
  private
27
-
28
27
  def field_type
29
28
  self.class.field_type
30
29
  end
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class TimeField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%T.%L")
9
+ value&.strftime("%T.%L")
11
10
  end
12
11
  end
13
12
  end
@@ -16,13 +16,8 @@ module ActionView
16
16
  translated_attribute || human_attribute_name
17
17
  end
18
18
 
19
- # TODO Change this to private once we've dropped Ruby 2.2 support.
20
- # Workaround for Ruby 2.2 "private attribute?" warning.
21
- protected
22
-
23
- attr_reader :object_name, :method_and_value, :scope, :model
24
-
25
19
  private
20
+ attr_reader :object_name, :method_and_value, :scope, :model
26
21
 
27
22
  def i18n_default
28
23
  if model
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class WeekField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%Y-W%V")
9
+ value&.strftime("%Y-W%V")
11
10
  end
12
11
  end
13
12
  end
@@ -188,7 +188,7 @@ module ActionView
188
188
 
189
189
  unless separator.empty?
190
190
  text.split(separator).each do |value|
191
- if value.match(regex)
191
+ if value.match?(regex)
192
192
  phrase = value
193
193
  break
194
194
  end
@@ -228,7 +228,7 @@ module ActionView
228
228
  # pluralize(2, 'Person', locale: :de)
229
229
  # # => 2 Personen
230
230
  def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
231
- word = if (count == 1 || count.to_s =~ /^1(\.0+)?$/)
231
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
232
232
  singular
233
233
  else
234
234
  plural || singular.pluralize(locale)
@@ -259,7 +259,7 @@ module ActionView
259
259
  # # => Once\r\nupon\r\na\r\ntime
260
260
  def word_wrap(text, line_width: 80, break_sequence: "\n")
261
261
  text.split("\n").collect! do |line|
262
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
262
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
263
263
  end * break_sequence
264
264
  end
265
265
 
@@ -426,7 +426,6 @@ module ActionView
426
426
  end
427
427
 
428
428
  private
429
-
430
429
  def next_index
431
430
  step_index(1)
432
431
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "action_view/helpers/tag_helper"
4
- require "active_support/core_ext/string/access"
5
- require "i18n/exceptions"
4
+ require "active_support/core_ext/symbol/starts_ends_with"
6
5
 
7
6
  module ActionView
8
7
  # = Action View Translation Helpers
@@ -57,74 +56,65 @@ module ActionView
57
56
  # that include HTML tags so that you know what kind of output to expect
58
57
  # when you call translate in a template and translators know which keys
59
58
  # they can provide HTML values for.
60
- def translate(key, options = {})
61
- options = options.dup
62
- has_default = options.has_key?(:default)
63
- remaining_defaults = Array(options.delete(:default)).compact
59
+ #
60
+ # To access the translated text along with the fully resolved
61
+ # translation key, <tt>translate</tt> accepts a block:
62
+ #
63
+ # <%= translate(".relative_key") do |translation, resolved_key| %>
64
+ # <span title="<%= resolved_key %>"><%= translation %></span>
65
+ # <% end %>
66
+ #
67
+ # This enables annotate translated text to be aware of the scope it was
68
+ # resolved against.
69
+ #
70
+ def translate(key, **options)
71
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
72
+ key = key&.to_s unless key.is_a?(Symbol)
64
73
 
65
- if has_default && !remaining_defaults.first.kind_of?(Symbol)
66
- options[:default] = remaining_defaults
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
67
76
  end
68
77
 
69
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
70
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
71
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
72
- if options[:raise] == false
73
- raise_error = false
74
- i18n_raise = false
75
- else
76
- raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
77
- i18n_raise = true
78
- end
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
79
80
 
80
- if html_safe_translation_key?(key)
81
- html_safe_options = options.dup
82
-
83
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
84
- unless name == :count && value.is_a?(Numeric)
85
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
86
- end
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
87
84
  end
88
85
 
89
- html_safe_options[:default] = MISSING_TRANSLATION unless html_safe_options[:default].blank?
86
+ key = scope_key_by_partial(key)
90
87
 
91
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
92
-
93
- if translation.equal?(MISSING_TRANSLATION)
94
- options[:default].first
88
+ if html_safe_translation_key?(key)
89
+ html_safe_options ||= html_escape_translation_options(options)
90
+ translated = I18n.translate(key, **html_safe_options, default: default)
91
+ break html_safe_translation(translated) unless translated.equal?(MISSING_TRANSLATION)
95
92
  else
96
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
97
- end
98
- else
99
- I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
100
- end
101
- rescue I18n::MissingTranslationData => e
102
- if remaining_defaults.present?
103
- translate remaining_defaults.shift, options.merge(default: remaining_defaults)
104
- else
105
- raise e if raise_error
106
-
107
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
108
- title = "translation missing: #{keys.join('.')}".dup
109
-
110
- interpolations = options.except(:default, :scope)
111
- if interpolations.any?
112
- title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
93
+ translated = I18n.translate(key, **options, default: default)
94
+ break translated unless translated.equal?(MISSING_TRANSLATION)
113
95
  end
114
96
 
115
- return title unless ActionView::Base.debug_missing_translation
97
+ break alternatives.first if alternatives.present? && !alternatives.first.is_a?(Symbol)
116
98
 
117
- content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
99
+ first_key ||= key
100
+ key = alternatives&.shift
118
101
  end
102
+
103
+ if key.nil? && !first_key.nil?
104
+ translation = missing_translation(first_key, options)
105
+ key = first_key
106
+ end
107
+
108
+ block_given? ? yield(translation, key) : translation
119
109
  end
120
110
  alias :t :translate
121
111
 
122
112
  # Delegates to <tt>I18n.localize</tt> with no additional functionality.
123
113
  #
124
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
114
+ # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
125
115
  # for more information.
126
- def localize(*args)
127
- I18n.localize(*args)
116
+ def localize(object, **options)
117
+ I18n.localize(object, **options)
128
118
  end
129
119
  alias :l :localize
130
120
 
@@ -132,10 +122,19 @@ module ActionView
132
122
  MISSING_TRANSLATION = Object.new
133
123
  private_constant :MISSING_TRANSLATION
134
124
 
125
+ NO_DEFAULT = [].freeze
126
+ private_constant :NO_DEFAULT
127
+
128
+ def self.i18n_option?(name)
129
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
130
+ end
131
+
135
132
  def scope_key_by_partial(key)
136
- if key.to_s.first == "."
133
+ if key&.start_with?(".")
137
134
  if @virtual_path
138
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
135
+ @_scope_key_by_partial_cache ||= {}
136
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
137
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
139
138
  else
140
139
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
141
140
  end
@@ -144,8 +143,47 @@ module ActionView
144
143
  end
145
144
  end
146
145
 
146
+ def html_escape_translation_options(options)
147
+ return options if options.empty?
148
+ html_safe_options = options.dup
149
+
150
+ options.each do |name, value|
151
+ unless TranslationHelper.i18n_option?(name) || (name == :count && value.is_a?(Numeric))
152
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
153
+ end
154
+ end
155
+
156
+ html_safe_options
157
+ end
158
+
147
159
  def html_safe_translation_key?(key)
148
- /(\b|_|\.)html$/.match?(key.to_s)
160
+ /(?:_|\b)html\z/.match?(key)
161
+ end
162
+
163
+ def html_safe_translation(translation)
164
+ if translation.respond_to?(:map)
165
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
166
+ else
167
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
168
+ end
169
+ end
170
+
171
+ def missing_translation(key, options)
172
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
173
+
174
+ title = +"translation missing: #{keys.join(".")}"
175
+
176
+ options.each do |name, value|
177
+ unless name == :scope
178
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
179
+ end
180
+ end
181
+
182
+ if ActionView::Base.debug_missing_translation
183
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
184
+ else
185
+ title
186
+ end
149
187
  end
150
188
  end
151
189
  end
@@ -45,7 +45,7 @@ module ActionView
45
45
  def _back_url # :nodoc:
46
46
  _filtered_referrer || "javascript:history.back()"
47
47
  end
48
- protected :_back_url
48
+ private :_back_url
49
49
 
50
50
  def _filtered_referrer # :nodoc:
51
51
  if controller.respond_to?(:request)
@@ -56,12 +56,12 @@ module ActionView
56
56
  end
57
57
  rescue URI::InvalidURIError
58
58
  end
59
- protected :_filtered_referrer
59
+ private :_filtered_referrer
60
60
 
61
61
  # Creates an anchor element of the given +name+ using a URL created by the set of +options+.
62
62
  # See the valid options in the documentation for +url_for+. It's also possible to
63
- # pass a String instead of an options hash, which generates an anchor element that uses the
64
- # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
63
+ # pass a \String instead of an options hash, which generates an anchor element that uses the
64
+ # value of the \String as the href for the link. Using a <tt>:back</tt> \Symbol instead
65
65
  # of an options hash will generate a link to the referrer (a JavaScript back link
66
66
  # will be used in place of a referrer if none exists). If +nil+ is passed as the name
67
67
  # the value of the link itself will become the name.
@@ -177,7 +177,7 @@ module ActionView
177
177
  # # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a>
178
178
  #
179
179
  # link_to "Nonsense search", searches_path(foo: "bar", baz: "quux")
180
- # # => <a href="/searches?foo=bar&amp;baz=quux">Nonsense search</a>
180
+ # # => <a href="/searches?foo=bar&baz=quux">Nonsense search</a>
181
181
  #
182
182
  # The only option specific to +link_to+ (<tt>:method</tt>) is used as follows:
183
183
  #
@@ -200,9 +200,9 @@ module ActionView
200
200
  html_options = convert_options_to_data_attributes(options, html_options)
201
201
 
202
202
  url = url_for(options)
203
- html_options["href".freeze] ||= url
203
+ html_options["href"] ||= url
204
204
 
205
- content_tag("a".freeze, name || url, html_options, &block)
205
+ content_tag("a", name || url, html_options, &block)
206
206
  end
207
207
 
208
208
  # Generates a form containing a single button that submits to the URL created
@@ -226,7 +226,7 @@ module ActionView
226
226
  # The +options+ hash accepts the same options as +url_for+.
227
227
  #
228
228
  # There are a few special +html_options+:
229
- # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
229
+ # * <tt>:method</tt> - \Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
230
230
  # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
231
231
  # * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
232
232
  # * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -235,7 +235,7 @@ module ActionView
235
235
  # * <tt>:form</tt> - This hash will be form attributes
236
236
  # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
237
237
  # be placed
238
- # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form.
238
+ # * <tt>:params</tt> - \Hash of parameters to be rendered as hidden fields within the form.
239
239
  #
240
240
  # ==== Data attributes
241
241
  #
@@ -290,7 +290,7 @@ module ActionView
290
290
  #
291
291
  #
292
292
  # <%= button_to('Destroy', 'http://www.example.com',
293
- # method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
293
+ # method: :delete, remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
294
294
  # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
295
295
  # # <input name='_method' value='delete' type='hidden' />
296
296
  # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
@@ -308,7 +308,7 @@ module ActionView
308
308
  params = html_options.delete("params")
309
309
 
310
310
  method = html_options.delete("method").to_s
311
- method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".freeze.html_safe
311
+ method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe
312
312
 
313
313
  form_method = method == "get" ? "get" : "post"
314
314
  form_options = html_options.delete("form") || {}
@@ -321,7 +321,7 @@ module ActionView
321
321
  request_method = method.empty? ? "post" : method
322
322
  token_tag(nil, form_options: { action: url, method: request_method })
323
323
  else
324
- "".freeze
324
+ ""
325
325
  end
326
326
 
327
327
  html_options = convert_options_to_data_attributes(options, html_options)
@@ -412,8 +412,7 @@ module ActionView
412
412
  # Creates a link tag of the given +name+ using a URL created by the set of
413
413
  # +options+ if +condition+ is true, otherwise only the name is
414
414
  # returned. To specialize the default behavior, you can pass a block that
415
- # accepts the name or the full argument list for +link_to_unless+ (see the examples
416
- # in +link_to_unless+).
415
+ # accepts the name or the full argument list for +link_to_if+.
417
416
  #
418
417
  # ==== Examples
419
418
  # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
@@ -487,12 +486,12 @@ module ActionView
487
486
  option = html_options.delete(item).presence || next
488
487
  "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
489
488
  }.compact
490
- extras = extras.empty? ? "".freeze : "?" + extras.join("&")
489
+ extras = extras.empty? ? "" : "?" + extras.join("&")
491
490
 
492
491
  encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
493
492
  html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
494
493
 
495
- content_tag("a".freeze, name || email_address, html_options, &block)
494
+ content_tag("a", name || email_address, html_options, &block)
496
495
  end
497
496
 
498
497
  # True if the current request URI was generated by the given +options+.
@@ -550,14 +549,14 @@ module ActionView
550
549
  return false unless request.get? || request.head?
551
550
 
552
551
  check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
553
- url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
552
+ url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
554
553
 
555
554
  # We ignore any extra parameters in the request_uri if the
556
- # submitted url doesn't have any either. This lets the function
555
+ # submitted URL doesn't have any either. This lets the function
557
556
  # work with things like ?order=asc
558
557
  # the behaviour can be disabled with check_parameters: true
559
558
  request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
560
- request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
559
+ request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
561
560
 
562
561
  if url_string.start_with?("/") && url_string != "/"
563
562
  url_string.chomp!("/")
@@ -571,30 +570,125 @@ module ActionView
571
570
  end
572
571
  end
573
572
 
573
+ # Creates an SMS anchor link tag to the specified +phone_number+, which is
574
+ # also used as the name of the link unless +name+ is specified. Additional
575
+ # HTML attributes for the link can be passed in +html_options+.
576
+ #
577
+ # When clicked, an SMS message is prepopulated with the passed phone number
578
+ # and optional +body+ value.
579
+ #
580
+ # +sms_to+ has a +body+ option for customizing the SMS message itself by
581
+ # passing special keys to +html_options+.
582
+ #
583
+ # ==== Options
584
+ # * <tt>:body</tt> - Preset the body of the message.
585
+ #
586
+ # ==== Examples
587
+ # sms_to "5155555785"
588
+ # # => <a href="sms:5155555785;">5155555785</a>
589
+ #
590
+ # sms_to "5155555785", "Text me"
591
+ # # => <a href="sms:5155555785;">Text me</a>
592
+ #
593
+ # sms_to "5155555785", "Text me",
594
+ # body: "Hello Jim I have a question about your product."
595
+ # # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
596
+ #
597
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
598
+ #
599
+ # <%= sms_to "5155555785" do %>
600
+ # <strong>Text me:</strong>
601
+ # <% end %>
602
+ # # => <a href="sms:5155555785;">
603
+ # <strong>Text me:</strong>
604
+ # </a>
605
+ def sms_to(phone_number, name = nil, html_options = {}, &block)
606
+ html_options, name = name, nil if block_given?
607
+ html_options = (html_options || {}).stringify_keys
608
+
609
+ extras = %w{ body }.map! { |item|
610
+ option = html_options.delete(item).presence || next
611
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
612
+ }.compact
613
+ extras = extras.empty? ? "" : "?&" + extras.join("&")
614
+
615
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
616
+ html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
617
+
618
+ content_tag("a", name || phone_number, html_options, &block)
619
+ end
620
+
621
+ # Creates a TEL anchor link tag to the specified +phone_number+, which is
622
+ # also used as the name of the link unless +name+ is specified. Additional
623
+ # HTML attributes for the link can be passed in +html_options+.
624
+ #
625
+ # When clicked, the default app to make calls is opened, and it
626
+ # is prepopulated with the passed phone number and optional
627
+ # +country_code+ value.
628
+ #
629
+ # +phone_to+ has an optional +country_code+ option which automatically adds the country
630
+ # code as well as the + sign in the phone numer that gets prepopulated,
631
+ # for example if +country_code: "01"+ +\+01+ will be prepended to the
632
+ # phone numer, by passing special keys to +html_options+.
633
+ #
634
+ # ==== Options
635
+ # * <tt>:country_code</tt> - Prepends the country code to the number
636
+ #
637
+ # ==== Examples
638
+ # phone_to "1234567890"
639
+ # # => <a href="tel:1234567890">1234567890</a>
640
+ #
641
+ # phone_to "1234567890", "Phone me"
642
+ # # => <a href="tel:134567890">Phone me</a>
643
+ #
644
+ # phone_to "1234567890", "Phone me", country_code: "01"
645
+ # # => <a href="tel:+015155555785">Phone me</a>
646
+ #
647
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
648
+ #
649
+ # <%= phone_to "1234567890" do %>
650
+ # <strong>Phone me:</strong>
651
+ # <% end %>
652
+ # # => <a href="tel:1234567890">
653
+ # <strong>Phone me:</strong>
654
+ # </a>
655
+ def phone_to(phone_number, name = nil, html_options = {}, &block)
656
+ html_options, name = name, nil if block_given?
657
+ html_options = (html_options || {}).stringify_keys
658
+
659
+ country_code = html_options.delete("country_code").presence
660
+ country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}"
661
+
662
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
663
+ html_options["href"] = "tel:#{country_code}#{encoded_phone_number}"
664
+
665
+ content_tag("a", name || phone_number, html_options, &block)
666
+ end
667
+
574
668
  private
575
669
  def convert_options_to_data_attributes(options, html_options)
576
670
  if html_options
577
671
  html_options = html_options.stringify_keys
578
- html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)
672
+ html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)
579
673
 
580
- method = html_options.delete("method".freeze)
674
+ method = html_options.delete("method")
581
675
 
582
676
  add_method_to_attributes!(html_options, method) if method
583
677
 
584
678
  html_options
585
679
  else
586
- link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {}
680
+ link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
587
681
  end
588
682
  end
589
683
 
590
684
  def link_to_remote_options?(options)
591
685
  if options.is_a?(Hash)
592
- options.delete("remote".freeze) || options.delete(:remote)
686
+ options.delete("remote") || options.delete(:remote)
593
687
  end
594
688
  end
595
689
 
596
690
  def add_method_to_attributes!(html_options, method)
597
- if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
691
+ if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
598
692
  if html_options["rel"].blank?
599
693
  html_options["rel"] = "nofollow"
600
694
  else
@@ -618,11 +712,11 @@ module ActionView
618
712
  end
619
713
 
620
714
  def token_tag(token = nil, form_options: {})
621
- if token != false && protect_against_forgery?
715
+ if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
622
716
  token ||= form_authenticity_token(form_options: form_options)
623
717
  tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
624
718
  else
625
- "".freeze
719
+ ""
626
720
  end
627
721
  end
628
722
 
@@ -636,7 +730,7 @@ module ActionView
636
730
  # to_form_params(name: 'David', nationality: 'Danish')
637
731
  # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}]
638
732
  #
639
- # to_form_params(country: {name: 'Denmark'})
733
+ # to_form_params(country: { name: 'Denmark' })
640
734
  # # => [{name: 'country[name]', value: 'Denmark'}]
641
735
  #
642
736
  # to_form_params(countries: ['Denmark', 'Sweden']})