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
@@ -11,6 +11,7 @@ require "active_support/core_ext/module/attribute_accessors"
11
11
  require "active_support/core_ext/hash/slice"
12
12
  require "active_support/core_ext/string/output_safety"
13
13
  require "active_support/core_ext/string/inflections"
14
+ require "active_support/core_ext/symbol/starts_ends_with"
14
15
 
15
16
  module ActionView
16
17
  # = Action View Form Helpers
@@ -185,8 +186,7 @@ module ActionView
185
186
  # get the authenticity token from the <tt>meta</tt> tag, so embedding is
186
187
  # unnecessary unless you support browsers without JavaScript.
187
188
  # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive
188
- # JavaScript drivers to control the submit behavior. By default this
189
- # behavior is an ajax submit.
189
+ # JavaScript drivers to control the submit behavior.
190
190
  # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name
191
191
  # utf8 is not output.
192
192
  # * <tt>:html</tt> - Optional HTML attributes for the form tag.
@@ -322,10 +322,8 @@ module ActionView
322
322
  # remote: true
323
323
  #
324
324
  # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its
325
- # behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular
326
- # POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor.
327
- # Even though it's using JavaScript to serialize the form elements, the form submission will work just like
328
- # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>).
325
+ # behavior. The form submission will work just like a regular submission as viewed by the receiving
326
+ # side (all elements available in <tt>params</tt>).
329
327
  #
330
328
  # Example:
331
329
  #
@@ -535,11 +533,6 @@ module ActionView
535
533
  # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt>
536
534
  # respectively.
537
535
  #
538
- # By default +form_with+ attaches the <tt>data-remote</tt> attribute
539
- # submitting the form via an XMLHTTPRequest in the background if an
540
- # Unobtrusive JavaScript driver, like rails-ujs, is used. See the
541
- # <tt>:local</tt> option for more.
542
- #
543
536
  # For ease of comparison the examples above left out the submit button,
544
537
  # as well as the auto generated hidden fields that enable UTF-8 support
545
538
  # and adds an authenticity token needed for cross site request forgery
@@ -611,8 +604,10 @@ module ActionView
611
604
  # This is helpful when fragment-caching the form. Remote forms
612
605
  # get the authenticity token from the <tt>meta</tt> tag, so embedding is
613
606
  # unnecessary unless you support browsers without JavaScript.
614
- # * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs.
615
- # Disable remote submits with <tt>local: true</tt>.
607
+ # * <tt>:local</tt> - By default form submits via typical HTTP requests.
608
+ # Enable remote and unobtrusive XHRs submits with <tt>local: false</tt>.
609
+ # Remote forms may be enabled by default by setting
610
+ # <tt>config.action_view.form_with_generates_remote_forms = true</tt>.
616
611
  # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
617
612
  # utf8 is not output.
618
613
  # * <tt>:builder</tt> - Override the object used to build the form.
@@ -888,7 +883,7 @@ module ActionView
888
883
  #
889
884
  # Now, when you use a form element with the <tt>_destroy</tt> parameter,
890
885
  # with a value that evaluates to +true+, you will destroy the associated
891
- # model (eg. 1, '1', true, or 'true'):
886
+ # model (e.g. 1, '1', true, or 'true'):
892
887
  #
893
888
  # <%= form_for @person do |person_form| %>
894
889
  # ...
@@ -977,7 +972,7 @@ module ActionView
977
972
  # This will allow you to specify which models to destroy in the
978
973
  # attributes hash by adding a form element for the <tt>_destroy</tt>
979
974
  # parameter with a value that evaluates to +true+
980
- # (eg. 1, '1', true, or 'true'):
975
+ # (e.g. 1, '1', true, or 'true'):
981
976
  #
982
977
  # <%= form_for @person do |person_form| %>
983
978
  # ...
@@ -1110,6 +1105,16 @@ module ActionView
1110
1105
  # label(:post, :privacy, "Public Post", value: "public")
1111
1106
  # # => <label for="post_privacy_public">Public Post</label>
1112
1107
  #
1108
+ # label(:post, :cost) do |translation|
1109
+ # content_tag(:span, translation, class: "cost_label")
1110
+ # end
1111
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
1112
+ #
1113
+ # label(:post, :cost) do |builder|
1114
+ # content_tag(:span, builder.translation, class: "cost_label")
1115
+ # end
1116
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
1117
+ #
1113
1118
  # label(:post, :terms) do
1114
1119
  # raw('Accept <a href="/terms">Terms</a>.')
1115
1120
  # end
@@ -1668,8 +1673,8 @@ module ActionView
1668
1673
 
1669
1674
  convert_to_legacy_options(@options)
1670
1675
 
1671
- if @object_name.to_s.match(/\[\]$/)
1672
- if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
1676
+ if @object_name&.end_with?("[]")
1677
+ if (object ||= @template.instance_variable_get("@#{@object_name[0..-3]}")) && object.respond_to?(:to_param)
1673
1678
  @auto_index = object.to_param
1674
1679
  else
1675
1680
  raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
@@ -1792,7 +1797,7 @@ module ActionView
1792
1797
  # Wraps ActionView::Helpers::FormHelper#time_field for form builders:
1793
1798
  #
1794
1799
  # <%= form_with model: @user do |f| %>
1795
- # <%= f.time_field :borned_at %>
1800
+ # <%= f.time_field :born_at %>
1796
1801
  # <% end %>
1797
1802
  #
1798
1803
  # Please refer to the documentation of the base helper for details.
@@ -1904,8 +1909,8 @@ module ActionView
1904
1909
  (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector|
1905
1910
  class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
1906
1911
  def #{selector}(method, options = {}) # def text_field(method, options = {})
1907
- @template.send( # @template.send(
1908
- #{selector.inspect}, # "text_field",
1912
+ @template.public_send( # @template.public_send(
1913
+ #{selector.inspect}, # :text_field,
1909
1914
  @object_name, # @object_name,
1910
1915
  method, # method,
1911
1916
  objectify_options(options)) # objectify_options(options))
@@ -2038,7 +2043,7 @@ module ActionView
2038
2043
  #
2039
2044
  # Now, when you use a form element with the <tt>_destroy</tt> parameter,
2040
2045
  # with a value that evaluates to +true+, you will destroy the associated
2041
- # model (eg. 1, '1', true, or 'true'):
2046
+ # model (e.g. 1, '1', true, or 'true'):
2042
2047
  #
2043
2048
  # <%= form_for @person do |person_form| %>
2044
2049
  # ...
@@ -2127,7 +2132,7 @@ module ActionView
2127
2132
  # This will allow you to specify which models to destroy in the
2128
2133
  # attributes hash by adding a form element for the <tt>_destroy</tt>
2129
2134
  # parameter with a value that evaluates to +true+
2130
- # (eg. 1, '1', true, or 'true'):
2135
+ # (e.g. 1, '1', true, or 'true'):
2131
2136
  #
2132
2137
  # <%= form_for @person do |person_form| %>
2133
2138
  # ...
@@ -2174,15 +2179,14 @@ module ActionView
2174
2179
  index = if options.has_key?(:index)
2175
2180
  options[:index]
2176
2181
  elsif defined?(@auto_index)
2177
- object_name = object_name.to_s.sub(/\[\]$/, "")
2182
+ object_name = object_name.to_s.delete_suffix("[]")
2178
2183
  @auto_index
2179
2184
  end
2180
2185
 
2181
2186
  record_name = if index
2182
2187
  "#{object_name}[#{index}][#{record_name}]"
2183
- elsif record_name.to_s.end_with?("[]")
2184
- record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]")
2185
- "#{object_name}#{record_name}"
2188
+ elsif record_name.end_with?("[]")
2189
+ "#{object_name}[#{record_name[0..-3]}][#{record_object.id}]"
2186
2190
  else
2187
2191
  "#{object_name}[#{record_name}]"
2188
2192
  end
@@ -2245,6 +2249,24 @@ module ActionView
2245
2249
  # label(:privacy, "Public Post", value: "public")
2246
2250
  # # => <label for="post_privacy_public">Public Post</label>
2247
2251
  #
2252
+ # label(:cost) do |translation|
2253
+ # content_tag(:span, translation, class: "cost_label")
2254
+ # end
2255
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
2256
+ #
2257
+ # label(:cost) do |builder|
2258
+ # content_tag(:span, builder.translation, class: "cost_label")
2259
+ # end
2260
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
2261
+ #
2262
+ # label(:cost) do |builder|
2263
+ # content_tag(:span, builder.translation, class: [
2264
+ # "cost_label",
2265
+ # ("error_label" if builder.object.errors.include?(:cost))
2266
+ # ])
2267
+ # end
2268
+ # # => <label for="post_cost"><span class="cost_label error_label">Total cost</span></label>
2269
+ #
2248
2270
  # label(:terms) do
2249
2271
  # raw('Accept <a href="/terms">Terms</a>.')
2250
2272
  # end
@@ -2468,10 +2490,22 @@ module ActionView
2468
2490
  # # <strong>Ask me!</strong>
2469
2491
  # # </button>
2470
2492
  #
2493
+ # button do |text|
2494
+ # content_tag(:strong, text)
2495
+ # end
2496
+ # # => <button name='button' type='submit'>
2497
+ # # <strong>Create post</strong>
2498
+ # # </button>
2499
+ #
2471
2500
  def button(value = nil, options = {}, &block)
2472
2501
  value, options = nil, value if value.is_a?(Hash)
2473
2502
  value ||= submit_default_value
2474
- @template.button_tag(value, options, &block)
2503
+
2504
+ if block_given?
2505
+ value = @template.capture { yield(value) }
2506
+ end
2507
+
2508
+ @template.button_tag(value, options)
2475
2509
  end
2476
2510
 
2477
2511
  def emitted_hidden_id? # :nodoc:
@@ -2480,7 +2514,9 @@ module ActionView
2480
2514
 
2481
2515
  private
2482
2516
  def objectify_options(options)
2483
- @default_options.merge(options.merge(object: @object))
2517
+ result = @default_options.merge(options)
2518
+ result[:object] = @object
2519
+ result
2484
2520
  end
2485
2521
 
2486
2522
  def submit_default_value
@@ -2515,9 +2551,9 @@ module ActionView
2515
2551
  association = convert_to_model(association)
2516
2552
 
2517
2553
  if association.respond_to?(:persisted?)
2518
- association = [association] if @object.send(association_name).respond_to?(:to_ary)
2554
+ association = [association] if @object.public_send(association_name).respond_to?(:to_ary)
2519
2555
  elsif !association.respond_to?(:to_ary)
2520
- association = @object.send(association_name)
2556
+ association = @object.public_send(association_name)
2521
2557
  end
2522
2558
 
2523
2559
  if association.respond_to?(:to_ary)
@@ -21,7 +21,7 @@ module ActionView
21
21
  # could become:
22
22
  #
23
23
  # <select name="post[category]" id="post_category">
24
- # <option value=""></option>
24
+ # <option value="" label=" "></option>
25
25
  # <option value="joke">joke</option>
26
26
  # <option value="poem">poem</option>
27
27
  # </select>
@@ -74,7 +74,6 @@ module ActionView
74
74
  # could become:
75
75
  #
76
76
  # <select name="post[category]" id="post_category">
77
- # <option value=""></option>
78
77
  # <option value="joke">joke</option>
79
78
  # <option value="poem">poem</option>
80
79
  # <option disabled="disabled" value="restricted">restricted</option>
@@ -112,7 +111,7 @@ module ActionView
112
111
  # would become:
113
112
  #
114
113
  # <select name="post[person_id]" id="post_person_id">
115
- # <option value=""></option>
114
+ # <option value="" label=" "></option>
116
115
  # <option value="1" selected="selected">David</option>
117
116
  # <option value="2">Eileen</option>
118
117
  # <option value="3">Rafael</option>
@@ -143,7 +142,7 @@ module ActionView
143
142
  #
144
143
  # The HTML specification says when +multiple+ parameter passed to select and all options got deselected
145
144
  # web browsers do not send any value to server. Unfortunately this introduces a gotcha:
146
- # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
145
+ # if a +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
147
146
  # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So,
148
147
  # any mass-assignment idiom like
149
148
  #
@@ -569,7 +568,7 @@ module ActionView
569
568
  # be obtained in Active Record as a value object). The +model+ parameter
570
569
  # must respond to +all+ and return an array of objects that represent time
571
570
  # zones; each object must respond to +name+. If a Regexp is given it will
572
- # attempt to match the zones using the <code>=~<code> operator.
571
+ # attempt to match the zones using <code>match?</code> method.
573
572
  #
574
573
  # NOTE: Only the option tags are returned, you have to wrap this call in
575
574
  # a regular HTML select tag.
@@ -581,7 +580,7 @@ module ActionView
581
580
 
582
581
  if priority_zones
583
582
  if priority_zones.is_a?(Regexp)
584
- priority_zones = zones.select { |z| z =~ priority_zones }
583
+ priority_zones = zones.select { |z| z.match?(priority_zones) }
585
584
  end
586
585
 
587
586
  zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
@@ -795,7 +794,7 @@ module ActionView
795
794
  def extract_values_from_collection(collection, value_method, selected)
796
795
  if selected.is_a?(Proc)
797
796
  collection.map do |element|
798
- public_or_deprecated_send(element, value_method) if selected.call(element)
797
+ element.public_send(value_method) if selected.call(element)
799
798
  end.compact
800
799
  else
801
800
  selected
@@ -803,15 +802,7 @@ module ActionView
803
802
  end
804
803
 
805
804
  def value_for_collection(item, value)
806
- value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value)
807
- end
808
-
809
- def public_or_deprecated_send(item, value)
810
- item.public_send(value)
811
- rescue NoMethodError
812
- raise unless item.respond_to?(value, true) && !item.respond_to?(value)
813
- ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})"
814
- item.send(value)
805
+ value.respond_to?(:call) ? value.call(item) : item.public_send(value)
815
806
  end
816
807
 
817
808
  def prompt_text(prompt)
@@ -4,6 +4,7 @@ require "cgi"
4
4
  require "action_view/helpers/tag_helper"
5
5
  require "active_support/core_ext/string/output_safety"
6
6
  require "active_support/core_ext/module/attribute_accessors"
7
+ require "active_support/core_ext/symbol/starts_ends_with"
7
8
 
8
9
  module ActionView
9
10
  # = Action View Form Tag Helpers
@@ -134,7 +135,7 @@ module ActionView
134
135
  # # <option selected="selected">MasterCard</option></select>
135
136
  def select_tag(name, option_tags = nil, options = {})
136
137
  option_tags ||= ""
137
- html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name
138
+ html_name = (options[:multiple] == true && !name.end_with?("[]")) ? "#{name}[]" : name
138
139
 
139
140
  if options.include?(:include_blank)
140
141
  include_blank = options[:include_blank]
@@ -240,7 +241,7 @@ module ActionView
240
241
  # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
241
242
  # # type="hidden" value="" />
242
243
  def hidden_field_tag(name, value = nil, options = {})
243
- text_field_tag(name, value, options.merge(type: :hidden))
244
+ text_field_tag(name, value, options.merge(type: :hidden, autocomplete: "off"))
244
245
  end
245
246
 
246
247
  # Creates a file upload field. If you are using file uploads then you will also need
@@ -822,7 +823,7 @@ module ActionView
822
823
  # Use raw HTML to ensure the value is written as an HTML entity; it
823
824
  # needs to be the right character regardless of which encoding the
824
825
  # browser infers.
825
- '<input name="utf8" type="hidden" value="&#x2713;" />'.html_safe
826
+ '<input name="utf8" type="hidden" value="&#x2713;" autocomplete="off" />'.html_safe
826
827
  end
827
828
 
828
829
  private
@@ -51,10 +51,10 @@ module ActionView
51
51
  # +html_options+ may be a hash of attributes for the <tt>\<script></tt>
52
52
  # tag.
53
53
  #
54
- # javascript_tag "alert('All is good')", defer: 'defer'
54
+ # javascript_tag "alert('All is good')", type: 'application/javascript'
55
55
  #
56
56
  # Returns:
57
- # <script defer="defer">
57
+ # <script type="application/javascript">
58
58
  # //<![CDATA[
59
59
  # alert('All is good')
60
60
  # //]]>
@@ -63,7 +63,7 @@ module ActionView
63
63
  # Instead of passing the content as an argument, you can also use a block
64
64
  # in which case, you pass your +html_options+ as the first parameter.
65
65
  #
66
- # <%= javascript_tag defer: 'defer' do -%>
66
+ # <%= javascript_tag type: 'application/javascript' do -%>
67
67
  # alert('All is good')
68
68
  # <% end -%>
69
69
  #
@@ -253,7 +253,7 @@ module ActionView
253
253
  end
254
254
 
255
255
  # Formats the bytes in +number+ into a more understandable
256
- # representation (e.g., giving it 1500 yields 1.5 KB). This
256
+ # representation (e.g., giving it 1500 yields 1.46 KB). This
257
257
  # method is useful for reporting file sizes to users. You can
258
258
  # customize the format in the +options+ hash.
259
259
  #
@@ -299,7 +299,7 @@ module ActionView
299
299
  end
300
300
 
301
301
  # Pretty prints (formats and approximates) a number in a way it
302
- # is more readable by humans (eg.: 1200000000 becomes "1.2
302
+ # is more readable by humans (e.g.: 1200000000 becomes "1.2
303
303
  # Billion"). This is useful for numbers that can get very large
304
304
  # (and too hard to read).
305
305
  #
@@ -307,7 +307,7 @@ module ActionView
307
307
  # size.
308
308
  #
309
309
  # You can also define your own unit-quantifier names if you want
310
- # to use other decimal units (eg.: 1500 becomes "1.5
310
+ # to use other decimal units (e.g.: 1500 becomes "1.5
311
311
  # kilometers", 0.150 becomes "150 milliliters", etc). You may
312
312
  # define a wide range of unit quantifiers, even fractional ones
313
313
  # (centi, deci, mili, etc).
@@ -425,9 +425,9 @@ module ActionView
425
425
  end
426
426
 
427
427
  def escape_units(units)
428
- Hash[units.map do |k, v|
429
- [k, ERB::Util.html_escape(v)]
430
- end]
428
+ units.transform_values do |v|
429
+ ERB::Util.html_escape(v)
430
+ end
431
431
  end
432
432
 
433
433
  def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
@@ -22,8 +22,12 @@ module ActionView
22
22
  # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt>
23
23
  # object.
24
24
  #
25
- # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
26
- # as the locals hash.
25
+ # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
26
+ #
27
+ # If an object responding to +render_in+ is passed, +render_in+ is called on the object,
28
+ # passing in the current view context.
29
+ #
30
+ # Otherwise, a partial is rendered using the second parameter as the locals hash.
27
31
  def render(options = {}, locals = {}, &block)
28
32
  case options
29
33
  when Hash
@@ -35,7 +39,11 @@ module ActionView
35
39
  end
36
40
  end
37
41
  else
38
- view_renderer.render_partial(self, partial: options, locals: locals, &block)
42
+ if options.respond_to?(:render_in)
43
+ options.render_in(self, &block)
44
+ else
45
+ view_renderer.render_partial(self, partial: options, locals: locals, &block)
46
+ end
39
47
  end
40
48
  end
41
49
 
@@ -13,19 +13,27 @@ module ActionView
13
13
  include CaptureHelper
14
14
  include OutputSafetyHelper
15
15
 
16
- BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked
17
- compact controls declare default defaultchecked
18
- defaultmuted defaultselected defer disabled
19
- enabled formnovalidate hidden indeterminate inert
20
- ismap itemscope loop multiple muted nohref
21
- noresize noshade novalidate nowrap open
22
- pauseonexit readonly required reversed scoped
23
- seamless selected sortable truespeed typemustmatch
24
- visible).to_set
16
+ BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
17
+ autoplay checked compact controls declare default
18
+ defaultchecked defaultmuted defaultselected defer
19
+ disabled enabled formnovalidate hidden indeterminate
20
+ inert ismap itemscope loop multiple muted nohref
21
+ nomodule noresize noshade novalidate nowrap open
22
+ pauseonexit playsinline readonly required reversed
23
+ scoped seamless selected sortable truespeed
24
+ typemustmatch visible).to_set
25
25
 
26
26
  BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
27
+ BOOLEAN_ATTRIBUTES.freeze
27
28
 
28
- TAG_PREFIXES = ["aria", "data", :aria, :data].to_set
29
+ ARIA_PREFIXES = ["aria", :aria].to_set.freeze
30
+ DATA_PREFIXES = ["data", :data].to_set.freeze
31
+
32
+ TAG_TYPES = {}
33
+ TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
34
+ TAG_TYPES.merge! DATA_PREFIXES.index_with(:data)
35
+ TAG_TYPES.merge! ARIA_PREFIXES.index_with(:aria)
36
+ TAG_TYPES.freeze
29
37
 
30
38
  PRE_CONTENT_STRINGS = Hash.new { "" }
31
39
  PRE_CONTENT_STRINGS[:textarea] = "\n"
@@ -41,10 +49,14 @@ module ActionView
41
49
  @view_context = view_context
42
50
  end
43
51
 
52
+ def p(*arguments, **options, &block)
53
+ tag_string(:p, *arguments, **options, &block)
54
+ end
55
+
44
56
  def tag_string(name, content = nil, **options, &block)
45
57
  escape = handle_deprecated_escape_options(options)
46
- content = @view_context.capture(self, &block) if block_given?
47
58
 
59
+ content = @view_context.capture(self, &block) if block_given?
48
60
  if VOID_ELEMENTS.include?(name) && content.nil?
49
61
  "<#{name.to_s.dasherize}#{tag_options(options, escape)}>".html_safe
50
62
  else
@@ -68,13 +80,31 @@ module ActionView
68
80
  output = +""
69
81
  sep = " "
70
82
  options.each_pair do |key, value|
71
- if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
83
+ type = TAG_TYPES[key]
84
+ if type == :data && value.is_a?(Hash)
72
85
  value.each_pair do |k, v|
73
86
  next if v.nil?
74
87
  output << sep
75
88
  output << prefix_tag_option(key, k, v, escape)
76
89
  end
77
- elsif BOOLEAN_ATTRIBUTES.include?(key)
90
+ elsif type == :aria && value.is_a?(Hash)
91
+ value.each_pair do |k, v|
92
+ next if v.nil?
93
+
94
+ case v
95
+ when Array, Hash
96
+ tokens = TagHelper.build_tag_values(v)
97
+ next if tokens.none?
98
+
99
+ v = safe_join(tokens, " ")
100
+ else
101
+ v = v.to_s
102
+ end
103
+
104
+ output << sep
105
+ output << prefix_tag_option(key, k, v, escape)
106
+ end
107
+ elsif type == :boolean
78
108
  if value
79
109
  output << sep
80
110
  output << boolean_tag_option(key)
@@ -94,12 +124,15 @@ module ActionView
94
124
  def tag_option(key, value, escape)
95
125
  key = ERB::Util.xml_name_escape(key) if escape
96
126
 
97
- if value.is_a?(Array)
127
+ case value
128
+ when Array, Hash
129
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
98
130
  value = escape ? safe_join(value, " ") : value.join(" ")
99
131
  else
100
- value = escape ? ERB::Util.unwrapped_html_escape(value).dup : value.to_s.dup
132
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
101
133
  end
102
- value.gsub!('"', "&quot;")
134
+ value = value.gsub('"', "&quot;") if value.include?('"')
135
+
103
136
  %(#{key}="#{value}")
104
137
  end
105
138
 
@@ -182,8 +215,8 @@ module ActionView
182
215
  # tag.input type: 'text', disabled: true
183
216
  # # => <input type="text" disabled="disabled">
184
217
  #
185
- # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
186
- # pointing to a hash of sub-attributes.
218
+ # HTML5 <tt>data-*</tt> and <tt>aria-*</tt> attributes can be set with a
219
+ # single +data+ or +aria+ key pointing to a hash of sub-attributes.
187
220
  #
188
221
  # To play nicely with JavaScript conventions, sub-attributes are dasherized.
189
222
  #
@@ -200,13 +233,13 @@ module ActionView
200
233
  # tag.div data: { city_state: %w( Chicago IL ) }
201
234
  # # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
202
235
  #
203
- # The generated attributes are escaped by default. This can be disabled using
204
- # +escape_attributes+.
236
+ # The generated tag names and attributes are escaped by default. This can be disabled using
237
+ # +escape+.
205
238
  #
206
239
  # tag.img src: 'open & shut.png'
207
240
  # # => <img src="open &amp; shut.png">
208
241
  #
209
- # tag.img src: 'open & shut.png', escape_attributes: false
242
+ # tag.img src: 'open & shut.png', escape: false
210
243
  # # => <img src="open & shut.png">
211
244
  #
212
245
  # The tag builder respects
@@ -263,6 +296,9 @@ module ActionView
263
296
  #
264
297
  # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
265
298
  # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
299
+ #
300
+ # tag("div", class: { highlight: current_user.admin? })
301
+ # # => <div class="highlight" />
266
302
  def tag(name = nil, options = nil, open = false, escape = true)
267
303
  if name.nil?
268
304
  tag_builder
@@ -276,7 +312,7 @@ module ActionView
276
312
  # HTML attributes by passing an attributes hash to +options+.
277
313
  # Instead of passing the content as an argument, you can also use a block
278
314
  # in which case, you pass your +options+ as the second parameter.
279
- # Set escape to false to disable attribute value escaping.
315
+ # Set escape to false to disable escaping.
280
316
  # Note: this is legacy syntax, see +tag+ method description for details.
281
317
  #
282
318
  # ==== Options
@@ -291,6 +327,8 @@ module ActionView
291
327
  # # => <div class="strong"><p>Hello world!</p></div>
292
328
  # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
293
329
  # # => <div class="strong highlight">Hello world!</div>
330
+ # content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
331
+ # # => <div class="strong highlight">Hello world!</div>
294
332
  # content_tag("select", options, multiple: true)
295
333
  # # => <select multiple="multiple">...options...</select>
296
334
  #
@@ -307,6 +345,24 @@ module ActionView
307
345
  end
308
346
  end
309
347
 
348
+ # Returns a string of tokens built from +args+.
349
+ #
350
+ # ==== Examples
351
+ # token_list("foo", "bar")
352
+ # # => "foo bar"
353
+ # token_list("foo", "foo bar")
354
+ # # => "foo bar"
355
+ # token_list({ foo: true, bar: false })
356
+ # # => "foo"
357
+ # token_list(nil, false, 123, "", "foo", { bar: true })
358
+ # # => "123 foo bar"
359
+ def token_list(*args)
360
+ tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
361
+
362
+ safe_join(tokens, " ")
363
+ end
364
+ alias_method :class_names, :token_list
365
+
310
366
  # Returns a CDATA section with the given +content+. CDATA sections
311
367
  # are used to escape blocks of text containing characters which would
312
368
  # otherwise be recognized as markup. CDATA sections begin with the string
@@ -337,6 +393,26 @@ module ActionView
337
393
  end
338
394
 
339
395
  private
396
+ def build_tag_values(*args)
397
+ tag_values = []
398
+
399
+ args.each do |tag_value|
400
+ case tag_value
401
+ when Hash
402
+ tag_value.each do |key, val|
403
+ tag_values << key.to_s if val && key.present?
404
+ end
405
+ when Array
406
+ tag_values.concat build_tag_values(*tag_value)
407
+ else
408
+ tag_values << tag_value.to_s if tag_value.present?
409
+ end
410
+ end
411
+
412
+ tag_values
413
+ end
414
+ module_function :build_tag_values
415
+
340
416
  def tag_builder
341
417
  @tag_builder ||= TagBuilder.new(self)
342
418
  end