actionview 6.0.6.1 → 6.1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +238 -220
  3. data/MIT-LICENSE +1 -2
  4. data/lib/action_view/base.rb +18 -49
  5. data/lib/action_view/cache_expiry.rb +1 -2
  6. data/lib/action_view/dependency_tracker.rb +10 -4
  7. data/lib/action_view/digestor.rb +3 -2
  8. data/lib/action_view/gem_version.rb +3 -3
  9. data/lib/action_view/helpers/asset_tag_helper.rb +57 -17
  10. data/lib/action_view/helpers/asset_url_helper.rb +6 -4
  11. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  12. data/lib/action_view/helpers/cache_helper.rb +10 -16
  13. data/lib/action_view/helpers/date_helper.rb +6 -5
  14. data/lib/action_view/helpers/form_helper.rb +66 -30
  15. data/lib/action_view/helpers/form_options_helper.rb +7 -16
  16. data/lib/action_view/helpers/form_tag_helper.rb +4 -3
  17. data/lib/action_view/helpers/javascript_helper.rb +3 -3
  18. data/lib/action_view/helpers/number_helper.rb +6 -6
  19. data/lib/action_view/helpers/rendering_helper.rb +11 -3
  20. data/lib/action_view/helpers/tag_helper.rb +98 -22
  21. data/lib/action_view/helpers/tags/base.rb +10 -6
  22. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  23. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  24. data/lib/action_view/helpers/tags/date_select.rb +2 -2
  25. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -1
  26. data/lib/action_view/helpers/tags/hidden_field.rb +4 -0
  27. data/lib/action_view/helpers/tags/label.rb +4 -0
  28. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  29. data/lib/action_view/helpers/tags/select.rb +1 -1
  30. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  31. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  32. data/lib/action_view/helpers/text_helper.rb +2 -2
  33. data/lib/action_view/helpers/translation_helper.rb +88 -50
  34. data/lib/action_view/helpers/url_helper.rb +136 -24
  35. data/lib/action_view/layouts.rb +3 -2
  36. data/lib/action_view/log_subscriber.rb +26 -10
  37. data/lib/action_view/lookup_context.rb +3 -18
  38. data/lib/action_view/path_set.rb +0 -3
  39. data/lib/action_view/railtie.rb +39 -46
  40. data/lib/action_view/renderer/abstract_renderer.rb +93 -14
  41. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  42. data/lib/action_view/renderer/object_renderer.rb +34 -0
  43. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +25 -26
  44. data/lib/action_view/renderer/partial_renderer.rb +20 -282
  45. data/lib/action_view/renderer/renderer.rb +44 -1
  46. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -1
  47. data/lib/action_view/renderer/template_renderer.rb +15 -12
  48. data/lib/action_view/rendering.rb +3 -1
  49. data/lib/action_view/routing_url_for.rb +1 -1
  50. data/lib/action_view/template/handlers/erb/erubi.rb +9 -7
  51. data/lib/action_view/template/handlers/erb.rb +10 -14
  52. data/lib/action_view/template/handlers.rb +0 -26
  53. data/lib/action_view/template/html.rb +1 -11
  54. data/lib/action_view/template/raw_file.rb +0 -3
  55. data/lib/action_view/template/renderable.rb +24 -0
  56. data/lib/action_view/template/resolver.rb +82 -40
  57. data/lib/action_view/template/text.rb +0 -3
  58. data/lib/action_view/template.rb +9 -49
  59. data/lib/action_view/test_case.rb +18 -25
  60. data/lib/action_view/testing/resolvers.rb +10 -31
  61. data/lib/action_view/unbound_template.rb +3 -3
  62. data/lib/action_view/view_paths.rb +34 -36
  63. data/lib/action_view.rb +4 -1
  64. data/lib/assets/compiled/rails-ujs.js +38 -7
  65. metadata +15 -12
@@ -105,7 +105,7 @@ module ActionView
105
105
  end
106
106
 
107
107
  def tag_name(multiple = false, index = nil)
108
- # a little duplication to construct less strings
108
+ # a little duplication to construct fewer strings
109
109
  case
110
110
  when @object_name.empty?
111
111
  "#{sanitized_method_name}#{multiple ? "[]" : ""}"
@@ -117,7 +117,7 @@ module ActionView
117
117
  end
118
118
 
119
119
  def tag_id(index = nil)
120
- # a little duplication to construct less strings
120
+ # a little duplication to construct fewer strings
121
121
  case
122
122
  when @object_name.empty?
123
123
  sanitized_method_name.dup
@@ -129,11 +129,11 @@ module ActionView
129
129
  end
130
130
 
131
131
  def sanitized_object_name
132
- @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
132
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
133
133
  end
134
134
 
135
135
  def sanitized_method_name
136
- @sanitized_method_name ||= @method_name.sub(/\?$/, "")
136
+ @sanitized_method_name ||= @method_name.delete_suffix("?")
137
137
  end
138
138
 
139
139
  def sanitized_value(value)
@@ -153,7 +153,7 @@ module ActionView
153
153
  select = content_tag("select", add_options(option_tags, options, value), html_options)
154
154
 
155
155
  if html_options["multiple"] && options.fetch(:include_hidden, true)
156
- tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
156
+ tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "", autocomplete: "off") + select
157
157
  else
158
158
  select
159
159
  end
@@ -166,8 +166,11 @@ module ActionView
166
166
 
167
167
  def add_options(option_tags, options, value = nil)
168
168
  if options[:include_blank]
169
- option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags
169
+ content = (options[:include_blank] if options[:include_blank].is_a?(String))
170
+ label = (" " unless content)
171
+ option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
170
172
  end
173
+
171
174
  if value.blank? && options[:prompt]
172
175
  tag_options = { value: "" }.tap do |prompt_opts|
173
176
  prompt_opts[:disabled] = true if options[:disabled] == ""
@@ -175,6 +178,7 @@ module ActionView
175
178
  end
176
179
  option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
177
180
  end
181
+
178
182
  option_tags
179
183
  end
180
184
 
@@ -57,7 +57,7 @@ module ActionView
57
57
  end
58
58
 
59
59
  def hidden_field_for_checkbox(options)
60
- @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
60
+ @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value, "autocomplete" => "off")) : "".html_safe
61
61
  end
62
62
  end
63
63
  end
@@ -6,7 +6,7 @@ module ActionView
6
6
  class DateField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%Y-%m-%d")
9
+ value&.strftime("%Y-%m-%d")
10
10
  end
11
11
  end
12
12
  end
@@ -13,7 +13,7 @@ module ActionView
13
13
  end
14
14
 
15
15
  def render
16
- error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
16
+ error_wrapping(datetime_selector(@options, @html_options).public_send("select_#{select_type}").html_safe)
17
17
  end
18
18
 
19
19
  class << self
@@ -58,7 +58,7 @@ module ActionView
58
58
  time = Time.current
59
59
 
60
60
  [:year, :month, :day, :hour, :min, :sec].each do |key|
61
- default[key] ||= time.send(key)
61
+ default[key] ||= time.public_send(key)
62
62
  end
63
63
 
64
64
  Time.utc(
@@ -12,7 +12,7 @@ module ActionView
12
12
 
13
13
  private
14
14
  def format_date(value)
15
- value.try(:strftime, "%Y-%m-%dT%T")
15
+ value&.strftime("%Y-%m-%dT%T")
16
16
  end
17
17
  end
18
18
  end
@@ -4,6 +4,10 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class HiddenField < TextField # :nodoc:
7
+ def render
8
+ @options[:autocomplete] = "off"
9
+ super
10
+ end
7
11
  end
8
12
  end
9
13
  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)
@@ -6,7 +6,7 @@ module ActionView
6
6
  class MonthField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%Y-%m")
9
+ value&.strftime("%Y-%m")
10
10
  end
11
11
  end
12
12
  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
 
@@ -6,7 +6,7 @@ module ActionView
6
6
  class TimeField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%T.%L")
9
+ value&.strftime("%T.%L")
10
10
  end
11
11
  end
12
12
  end
@@ -6,7 +6,7 @@ module ActionView
6
6
  class WeekField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%Y-W%V")
9
+ value&.strftime("%Y-W%V")
10
10
  end
11
11
  end
12
12
  end
@@ -105,7 +105,7 @@ module ActionView
105
105
  # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
106
106
  # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
107
107
  # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
108
- # '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+
108
+ # <tt><mark>\1</mark></tt>) or passing a block that receives each matched term. By default +text+
109
109
  # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
110
110
  # for <tt>:sanitize</tt> will turn sanitizing off.
111
111
  #
@@ -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)
@@ -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,64 +56,58 @@ 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.
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
+ #
60
70
  def translate(key, **options)
61
- if options.has_key?(:default)
62
- remaining_defaults = Array.wrap(options.delete(:default)).compact
63
- options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
64
- end
71
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
72
+ key = key&.to_s unless key.is_a?(Symbol)
65
73
 
66
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
67
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
68
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
69
- if options[:raise] == false
70
- raise_error = false
71
- i18n_raise = false
72
- else
73
- raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
74
- i18n_raise = true
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
75
76
  end
76
77
 
77
- if html_safe_translation_key?(key)
78
- html_safe_options = options.dup
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
79
80
 
80
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
81
- unless name == :count && value.is_a?(Numeric)
82
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
83
- end
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
84
84
  end
85
85
 
86
- html_safe_options[:default] = MISSING_TRANSLATION unless html_safe_options[:default].blank?
87
-
88
- translation = I18n.translate(scope_key_by_partial(key), **html_safe_options.merge(raise: i18n_raise))
86
+ key = scope_key_by_partial(key)
89
87
 
90
- if translation.equal?(MISSING_TRANSLATION)
91
- options[:default].first
92
- elsif translation.respond_to?(:map)
93
- translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
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)
94
92
  else
95
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
93
+ translated = I18n.translate(key, **options, default: default)
94
+ break translated unless translated.equal?(MISSING_TRANSLATION)
96
95
  end
97
- else
98
- I18n.translate(scope_key_by_partial(key), **options.merge(raise: i18n_raise))
99
- end
100
- rescue I18n::MissingTranslationData => e
101
- if remaining_defaults.present?
102
- translate remaining_defaults.shift, **options.merge(default: remaining_defaults)
103
- else
104
- raise e if raise_error
105
-
106
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
107
- title = +"translation missing: #{keys.join('.')}"
108
-
109
- interpolations = options.except(:default, :scope)
110
- if interpolations.any?
111
- title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
96
+
97
+ if alternatives.present? && !alternatives.first.is_a?(Symbol)
98
+ break alternatives.first && I18n.translate(**options, default: alternatives)
112
99
  end
113
100
 
114
- return title unless ActionView::Base.debug_missing_translation
101
+ first_key ||= key
102
+ key = alternatives&.shift
103
+ end
115
104
 
116
- content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
105
+ if key.nil? && !first_key.nil?
106
+ translation = missing_translation(first_key, options)
107
+ key = first_key
117
108
  end
109
+
110
+ block_given? ? yield(translation, key) : translation
118
111
  end
119
112
  alias :t :translate
120
113
 
@@ -131,13 +124,19 @@ module ActionView
131
124
  MISSING_TRANSLATION = Object.new
132
125
  private_constant :MISSING_TRANSLATION
133
126
 
127
+ NO_DEFAULT = [].freeze
128
+ private_constant :NO_DEFAULT
129
+
130
+ def self.i18n_option?(name)
131
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
132
+ end
133
+
134
134
  def scope_key_by_partial(key)
135
- stringified_key = key.to_s
136
- if stringified_key.first == "."
135
+ if key&.start_with?(".")
137
136
  if @virtual_path
138
137
  @_scope_key_by_partial_cache ||= {}
139
138
  @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
140
- "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}"
139
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
141
140
  else
142
141
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
143
142
  end
@@ -146,8 +145,47 @@ module ActionView
146
145
  end
147
146
  end
148
147
 
148
+ def html_escape_translation_options(options)
149
+ return options if options.empty?
150
+ html_safe_options = options.dup
151
+
152
+ options.each do |name, value|
153
+ unless TranslationHelper.i18n_option?(name) || (name == :count && value.is_a?(Numeric))
154
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
155
+ end
156
+ end
157
+
158
+ html_safe_options
159
+ end
160
+
149
161
  def html_safe_translation_key?(key)
150
- /(?:_|\b)html\z/.match?(key.to_s)
162
+ /(?:_|\b)html\z/.match?(key)
163
+ end
164
+
165
+ def html_safe_translation(translation)
166
+ if translation.respond_to?(:map)
167
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
168
+ else
169
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
170
+ end
171
+ end
172
+
173
+ def missing_translation(key, options)
174
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
175
+
176
+ title = +"translation missing: #{keys.join(".")}"
177
+
178
+ options.each do |name, value|
179
+ unless name == :scope
180
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
181
+ end
182
+ end
183
+
184
+ if ActionView::Base.debug_missing_translation
185
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
186
+ else
187
+ title
188
+ end
151
189
  end
152
190
  end
153
191
  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
  #
@@ -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?' />
@@ -337,7 +337,8 @@ module ActionView
337
337
  inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
338
338
  if params
339
339
  to_form_params(params).each do |param|
340
- inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
340
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value],
341
+ autocomplete: "off")
341
342
  end
342
343
  end
343
344
  content_tag("form", inner_tags, form_options)
@@ -412,8 +413,7 @@ module ActionView
412
413
  # Creates a link tag of the given +name+ using a URL created by the set of
413
414
  # +options+ if +condition+ is true, otherwise only the name is
414
415
  # 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+).
416
+ # accepts the name or the full argument list for +link_to_if+.
417
417
  #
418
418
  # ==== Examples
419
419
  # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
@@ -540,7 +540,7 @@ module ActionView
540
540
  #
541
541
  # We can also pass in the symbol arguments instead of strings.
542
542
  #
543
- def current_page?(options, check_parameters: false)
543
+ def current_page?(options = nil, check_parameters: false, **options_as_kwargs)
544
544
  unless request
545
545
  raise "You cannot use helpers that need to determine the current " \
546
546
  "page unless your view context provides a Request object " \
@@ -549,28 +549,135 @@ module ActionView
549
549
 
550
550
  return false unless request.get? || request.head?
551
551
 
552
+ options ||= options_as_kwargs
552
553
  check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
553
- url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
554
+ url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
554
555
 
555
556
  # We ignore any extra parameters in the request_uri if the
556
557
  # submitted URL doesn't have any either. This lets the function
557
558
  # work with things like ?order=asc
558
559
  # the behaviour can be disabled with check_parameters: true
559
560
  request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
560
- request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
561
+ request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
561
562
 
562
- if url_string.start_with?("/") && url_string != "/"
563
- url_string.chomp!("/")
564
- request_uri.chomp!("/")
563
+ if %r{^\w+://}.match?(url_string)
564
+ request_uri = +"#{request.protocol}#{request.host_with_port}#{request_uri}"
565
565
  end
566
566
 
567
- if %r{^\w+://}.match?(url_string)
568
- url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
569
- else
570
- url_string == request_uri
567
+ remove_trailing_slash!(url_string)
568
+ remove_trailing_slash!(request_uri)
569
+
570
+ url_string == request_uri
571
+ end
572
+
573
+ if RUBY_VERSION.start_with?("2.7")
574
+ using Module.new {
575
+ refine UrlHelper do
576
+ alias :_current_page? :current_page?
577
+ end
578
+ }
579
+
580
+ def current_page?(*args) # :nodoc:
581
+ options = args.pop
582
+ options.is_a?(Hash) ? _current_page?(*args, **options) : _current_page?(*args, options)
571
583
  end
572
584
  end
573
585
 
586
+ # Creates an SMS anchor link tag to the specified +phone_number+, which is
587
+ # also used as the name of the link unless +name+ is specified. Additional
588
+ # HTML attributes for the link can be passed in +html_options+.
589
+ #
590
+ # When clicked, an SMS message is prepopulated with the passed phone number
591
+ # and optional +body+ value.
592
+ #
593
+ # +sms_to+ has a +body+ option for customizing the SMS message itself by
594
+ # passing special keys to +html_options+.
595
+ #
596
+ # ==== Options
597
+ # * <tt>:body</tt> - Preset the body of the message.
598
+ #
599
+ # ==== Examples
600
+ # sms_to "5155555785"
601
+ # # => <a href="sms:5155555785;">5155555785</a>
602
+ #
603
+ # sms_to "5155555785", "Text me"
604
+ # # => <a href="sms:5155555785;">Text me</a>
605
+ #
606
+ # sms_to "5155555785", "Text me",
607
+ # body: "Hello Jim I have a question about your product."
608
+ # # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
609
+ #
610
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
611
+ #
612
+ # <%= sms_to "5155555785" do %>
613
+ # <strong>Text me:</strong>
614
+ # <% end %>
615
+ # # => <a href="sms:5155555785;">
616
+ # <strong>Text me:</strong>
617
+ # </a>
618
+ def sms_to(phone_number, name = nil, html_options = {}, &block)
619
+ html_options, name = name, nil if block_given?
620
+ html_options = (html_options || {}).stringify_keys
621
+
622
+ extras = %w{ body }.map! { |item|
623
+ option = html_options.delete(item).presence || next
624
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
625
+ }.compact
626
+ extras = extras.empty? ? "" : "?&" + extras.join("&")
627
+
628
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
629
+ html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
630
+
631
+ content_tag("a", name || phone_number, html_options, &block)
632
+ end
633
+
634
+ # Creates a TEL anchor link tag to the specified +phone_number+, which is
635
+ # also used as the name of the link unless +name+ is specified. Additional
636
+ # HTML attributes for the link can be passed in +html_options+.
637
+ #
638
+ # When clicked, the default app to make calls is opened, and it
639
+ # is prepopulated with the passed phone number and optional
640
+ # +country_code+ value.
641
+ #
642
+ # +phone_to+ has an optional +country_code+ option which automatically adds the country
643
+ # code as well as the + sign in the phone numer that gets prepopulated,
644
+ # for example if +country_code: "01"+ +\+01+ will be prepended to the
645
+ # phone numer, by passing special keys to +html_options+.
646
+ #
647
+ # ==== Options
648
+ # * <tt>:country_code</tt> - Prepends the country code to the number
649
+ #
650
+ # ==== Examples
651
+ # phone_to "1234567890"
652
+ # # => <a href="tel:1234567890">1234567890</a>
653
+ #
654
+ # phone_to "1234567890", "Phone me"
655
+ # # => <a href="tel:134567890">Phone me</a>
656
+ #
657
+ # phone_to "1234567890", "Phone me", country_code: "01"
658
+ # # => <a href="tel:+015155555785">Phone me</a>
659
+ #
660
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
661
+ #
662
+ # <%= phone_to "1234567890" do %>
663
+ # <strong>Phone me:</strong>
664
+ # <% end %>
665
+ # # => <a href="tel:1234567890">
666
+ # <strong>Phone me:</strong>
667
+ # </a>
668
+ def phone_to(phone_number, name = nil, html_options = {}, &block)
669
+ html_options, name = name, nil if block_given?
670
+ html_options = (html_options || {}).stringify_keys
671
+
672
+ country_code = html_options.delete("country_code").presence
673
+ country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}"
674
+
675
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
676
+ html_options["href"] = "tel:#{country_code}#{encoded_phone_number}"
677
+
678
+ content_tag("a", name || phone_number, html_options, &block)
679
+ end
680
+
574
681
  private
575
682
  def convert_options_to_data_attributes(options, html_options)
576
683
  if html_options
@@ -594,7 +701,7 @@ module ActionView
594
701
  end
595
702
 
596
703
  def add_method_to_attributes!(html_options, method)
597
- if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
704
+ if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
598
705
  if html_options["rel"].blank?
599
706
  html_options["rel"] = "nofollow"
600
707
  else
@@ -620,14 +727,14 @@ module ActionView
620
727
  def token_tag(token = nil, form_options: {})
621
728
  if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
622
729
  token ||= form_authenticity_token(form_options: form_options)
623
- tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
730
+ tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token, autocomplete: "off")
624
731
  else
625
732
  ""
626
733
  end
627
734
  end
628
735
 
629
736
  def method_tag(method)
630
- tag("input", type: "hidden", name: "_method", value: method.to_s)
737
+ tag("input", type: "hidden", name: "_method", value: method.to_s, autocomplete: "off")
631
738
  end
632
739
 
633
740
  # Returns an array of hashes each containing :name and :value keys
@@ -671,6 +778,11 @@ module ActionView
671
778
 
672
779
  params.sort_by { |pair| pair[:name] }
673
780
  end
781
+
782
+ def remove_trailing_slash!(url_string)
783
+ trailing_index = (url_string.index("?") || 0) - 1
784
+ url_string[trailing_index] = "" if url_string[trailing_index] == "/"
785
+ end
674
786
  end
675
787
  end
676
788
  end