actionview 6.0.3.1 → 6.1.0.rc2

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +160 -209
  3. data/MIT-LICENSE +1 -1
  4. data/lib/action_view.rb +4 -1
  5. data/lib/action_view/base.rb +21 -52
  6. data/lib/action_view/cache_expiry.rb +1 -2
  7. data/lib/action_view/context.rb +0 -1
  8. data/lib/action_view/dependency_tracker.rb +10 -4
  9. data/lib/action_view/digestor.rb +3 -2
  10. data/lib/action_view/gem_version.rb +3 -3
  11. data/lib/action_view/helpers/asset_tag_helper.rb +40 -15
  12. data/lib/action_view/helpers/asset_url_helper.rb +6 -4
  13. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  14. data/lib/action_view/helpers/cache_helper.rb +10 -16
  15. data/lib/action_view/helpers/date_helper.rb +4 -4
  16. data/lib/action_view/helpers/form_helper.rb +59 -17
  17. data/lib/action_view/helpers/form_options_helper.rb +7 -16
  18. data/lib/action_view/helpers/form_tag_helper.rb +2 -1
  19. data/lib/action_view/helpers/javascript_helper.rb +3 -3
  20. data/lib/action_view/helpers/number_helper.rb +6 -6
  21. data/lib/action_view/helpers/rendering_helper.rb +11 -3
  22. data/lib/action_view/helpers/tag_helper.rb +92 -17
  23. data/lib/action_view/helpers/tags/base.rb +9 -5
  24. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  25. data/lib/action_view/helpers/tags/date_select.rb +2 -2
  26. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -1
  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 +1 -1
  33. data/lib/action_view/helpers/translation_helper.rb +94 -49
  34. data/lib/action_view/helpers/url_helper.rb +107 -13
  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 +35 -46
  40. data/lib/action_view/renderer/abstract_renderer.rb +93 -14
  41. data/lib/action_view/renderer/collection_renderer.rb +192 -0
  42. data/lib/action_view/renderer/object_renderer.rb +34 -0
  43. data/lib/action_view/renderer/partial_renderer.rb +20 -282
  44. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +25 -26
  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.rb +9 -49
  51. data/lib/action_view/template/handlers.rb +0 -26
  52. data/lib/action_view/template/handlers/erb.rb +10 -14
  53. data/lib/action_view/template/handlers/erb/erubi.rb +9 -7
  54. data/lib/action_view/template/html.rb +1 -11
  55. data/lib/action_view/template/raw_file.rb +0 -3
  56. data/lib/action_view/template/renderable.rb +24 -0
  57. data/lib/action_view/template/resolver.rb +82 -40
  58. data/lib/action_view/template/text.rb +0 -3
  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. metadata +17 -14
@@ -197,8 +197,8 @@ module ActionView
197
197
  # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
198
198
  # See <tt>Kernel.sprintf</tt> for documentation on format sequences.
199
199
  # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
200
- # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is "" (i.e. nothing).
201
- # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is "" (i.e. nothing).
200
+ # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is " : ".
201
+ # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is " &mdash; ".
202
202
  # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if
203
203
  # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
204
204
  # the current selected year minus 5.
@@ -1053,7 +1053,7 @@ module ActionView
1053
1053
  select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes]
1054
1054
 
1055
1055
  select_html = +"\n"
1056
- select_html << content_tag("option", "", value: "") + "\n" if @options[:include_blank]
1056
+ select_html << content_tag("option", "", value: "", label: " ") + "\n" if @options[:include_blank]
1057
1057
  select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
1058
1058
  select_html << select_options_as_html
1059
1059
 
@@ -1138,7 +1138,7 @@ module ActionView
1138
1138
  first_visible = order.find { |type| !@options[:"discard_#{type}"] }
1139
1139
  order.reverse_each do |type|
1140
1140
  separator = separator(type) unless type == first_visible # don't add before first visible field
1141
- select.insert(0, separator.to_s + send("select_#{type}").to_s)
1141
+ select.insert(0, separator.to_s + public_send("select_#{type}").to_s)
1142
1142
  end
1143
1143
  select.html_safe
1144
1144
  end
@@ -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
@@ -888,7 +889,7 @@ module ActionView
888
889
  #
889
890
  # Now, when you use a form element with the <tt>_destroy</tt> parameter,
890
891
  # with a value that evaluates to +true+, you will destroy the associated
891
- # model (eg. 1, '1', true, or 'true'):
892
+ # model (e.g. 1, '1', true, or 'true'):
892
893
  #
893
894
  # <%= form_for @person do |person_form| %>
894
895
  # ...
@@ -977,7 +978,7 @@ module ActionView
977
978
  # This will allow you to specify which models to destroy in the
978
979
  # attributes hash by adding a form element for the <tt>_destroy</tt>
979
980
  # parameter with a value that evaluates to +true+
980
- # (eg. 1, '1', true, or 'true'):
981
+ # (e.g. 1, '1', true, or 'true'):
981
982
  #
982
983
  # <%= form_for @person do |person_form| %>
983
984
  # ...
@@ -1110,6 +1111,16 @@ module ActionView
1110
1111
  # label(:post, :privacy, "Public Post", value: "public")
1111
1112
  # # => <label for="post_privacy_public">Public Post</label>
1112
1113
  #
1114
+ # label(:post, :cost) do |translation|
1115
+ # content_tag(:span, translation, class: "cost_label")
1116
+ # end
1117
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
1118
+ #
1119
+ # label(:post, :cost) do |builder|
1120
+ # content_tag(:span, builder.translation, class: "cost_label")
1121
+ # end
1122
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
1123
+ #
1113
1124
  # label(:post, :terms) do
1114
1125
  # raw('Accept <a href="/terms">Terms</a>.')
1115
1126
  # end
@@ -1668,8 +1679,8 @@ module ActionView
1668
1679
 
1669
1680
  convert_to_legacy_options(@options)
1670
1681
 
1671
- if @object_name.to_s.match(/\[\]$/)
1672
- if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
1682
+ if @object_name&.end_with?("[]")
1683
+ if (object ||= @template.instance_variable_get("@#{@object_name[0..-3]}")) && object.respond_to?(:to_param)
1673
1684
  @auto_index = object.to_param
1674
1685
  else
1675
1686
  raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
@@ -1792,7 +1803,7 @@ module ActionView
1792
1803
  # Wraps ActionView::Helpers::FormHelper#time_field for form builders:
1793
1804
  #
1794
1805
  # <%= form_with model: @user do |f| %>
1795
- # <%= f.time_field :borned_at %>
1806
+ # <%= f.time_field :born_at %>
1796
1807
  # <% end %>
1797
1808
  #
1798
1809
  # Please refer to the documentation of the base helper for details.
@@ -1904,8 +1915,8 @@ module ActionView
1904
1915
  (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector|
1905
1916
  class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
1906
1917
  def #{selector}(method, options = {}) # def text_field(method, options = {})
1907
- @template.send( # @template.send(
1908
- #{selector.inspect}, # "text_field",
1918
+ @template.public_send( # @template.public_send(
1919
+ #{selector.inspect}, # :text_field,
1909
1920
  @object_name, # @object_name,
1910
1921
  method, # method,
1911
1922
  objectify_options(options)) # objectify_options(options))
@@ -2038,7 +2049,7 @@ module ActionView
2038
2049
  #
2039
2050
  # Now, when you use a form element with the <tt>_destroy</tt> parameter,
2040
2051
  # with a value that evaluates to +true+, you will destroy the associated
2041
- # model (eg. 1, '1', true, or 'true'):
2052
+ # model (e.g. 1, '1', true, or 'true'):
2042
2053
  #
2043
2054
  # <%= form_for @person do |person_form| %>
2044
2055
  # ...
@@ -2127,7 +2138,7 @@ module ActionView
2127
2138
  # This will allow you to specify which models to destroy in the
2128
2139
  # attributes hash by adding a form element for the <tt>_destroy</tt>
2129
2140
  # parameter with a value that evaluates to +true+
2130
- # (eg. 1, '1', true, or 'true'):
2141
+ # (e.g. 1, '1', true, or 'true'):
2131
2142
  #
2132
2143
  # <%= form_for @person do |person_form| %>
2133
2144
  # ...
@@ -2174,15 +2185,14 @@ module ActionView
2174
2185
  index = if options.has_key?(:index)
2175
2186
  options[:index]
2176
2187
  elsif defined?(@auto_index)
2177
- object_name = object_name.to_s.sub(/\[\]$/, "")
2188
+ object_name = object_name.to_s.delete_suffix("[]")
2178
2189
  @auto_index
2179
2190
  end
2180
2191
 
2181
2192
  record_name = if index
2182
2193
  "#{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}"
2194
+ elsif record_name.end_with?("[]")
2195
+ "#{object_name}[#{record_name[0..-3]}][#{record_object.id}]"
2186
2196
  else
2187
2197
  "#{object_name}[#{record_name}]"
2188
2198
  end
@@ -2245,6 +2255,24 @@ module ActionView
2245
2255
  # label(:privacy, "Public Post", value: "public")
2246
2256
  # # => <label for="post_privacy_public">Public Post</label>
2247
2257
  #
2258
+ # label(:cost) do |translation|
2259
+ # content_tag(:span, translation, class: "cost_label")
2260
+ # end
2261
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
2262
+ #
2263
+ # label(:cost) do |builder|
2264
+ # content_tag(:span, builder.translation, class: "cost_label")
2265
+ # end
2266
+ # # => <label for="post_cost"><span class="cost_label">Total cost</span></label>
2267
+ #
2268
+ # label(:cost) do |builder|
2269
+ # content_tag(:span, builder.translation, class: [
2270
+ # "cost_label",
2271
+ # ("error_label" if builder.object.errors.include?(:cost))
2272
+ # ])
2273
+ # end
2274
+ # # => <label for="post_cost"><span class="cost_label error_label">Total cost</span></label>
2275
+ #
2248
2276
  # label(:terms) do
2249
2277
  # raw('Accept <a href="/terms">Terms</a>.')
2250
2278
  # end
@@ -2468,10 +2496,22 @@ module ActionView
2468
2496
  # # <strong>Ask me!</strong>
2469
2497
  # # </button>
2470
2498
  #
2499
+ # button do |text|
2500
+ # content_tag(:strong, text)
2501
+ # end
2502
+ # # => <button name='button' type='submit'>
2503
+ # # <strong>Create post</strong>
2504
+ # # </button>
2505
+ #
2471
2506
  def button(value = nil, options = {}, &block)
2472
2507
  value, options = nil, value if value.is_a?(Hash)
2473
2508
  value ||= submit_default_value
2474
- @template.button_tag(value, options, &block)
2509
+
2510
+ if block_given?
2511
+ value = @template.capture { yield(value) }
2512
+ end
2513
+
2514
+ @template.button_tag(value, options)
2475
2515
  end
2476
2516
 
2477
2517
  def emitted_hidden_id? # :nodoc:
@@ -2480,7 +2520,9 @@ module ActionView
2480
2520
 
2481
2521
  private
2482
2522
  def objectify_options(options)
2483
- @default_options.merge(options.merge(object: @object))
2523
+ result = @default_options.merge(options)
2524
+ result[:object] = @object
2525
+ result
2484
2526
  end
2485
2527
 
2486
2528
  def submit_default_value
@@ -2515,9 +2557,9 @@ module ActionView
2515
2557
  association = convert_to_model(association)
2516
2558
 
2517
2559
  if association.respond_to?(:persisted?)
2518
- association = [association] if @object.send(association_name).respond_to?(:to_ary)
2560
+ association = [association] if @object.public_send(association_name).respond_to?(:to_ary)
2519
2561
  elsif !association.respond_to?(:to_ary)
2520
- association = @object.send(association_name)
2562
+ association = @object.public_send(association_name)
2521
2563
  end
2522
2564
 
2523
2565
  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]
@@ -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,6 +49,10 @@ 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, escape_attributes: true, **options, &block)
45
57
  content = @view_context.capture(self, &block) if block_given?
46
58
  if VOID_ELEMENTS.include?(name) && content.nil?
@@ -61,13 +73,31 @@ module ActionView
61
73
  output = +""
62
74
  sep = " "
63
75
  options.each_pair do |key, value|
64
- if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
76
+ type = TAG_TYPES[key]
77
+ if type == :data && value.is_a?(Hash)
78
+ value.each_pair do |k, v|
79
+ next if v.nil?
80
+ output << sep
81
+ output << prefix_tag_option(key, k, v, escape)
82
+ end
83
+ elsif type == :aria && value.is_a?(Hash)
65
84
  value.each_pair do |k, v|
66
85
  next if v.nil?
86
+
87
+ case v
88
+ when Array, Hash
89
+ tokens = TagHelper.build_tag_values(v)
90
+ next if tokens.none?
91
+
92
+ v = safe_join(tokens, " ")
93
+ else
94
+ v = v.to_s
95
+ end
96
+
67
97
  output << sep
68
98
  output << prefix_tag_option(key, k, v, escape)
69
99
  end
70
- elsif BOOLEAN_ATTRIBUTES.include?(key)
100
+ elsif type == :boolean
71
101
  if value
72
102
  output << sep
73
103
  output << boolean_tag_option(key)
@@ -85,12 +115,14 @@ module ActionView
85
115
  end
86
116
 
87
117
  def tag_option(key, value, escape)
88
- if value.is_a?(Array)
118
+ case value
119
+ when Array, Hash
120
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
89
121
  value = escape ? safe_join(value, " ") : value.join(" ")
90
122
  else
91
- value = escape ? ERB::Util.unwrapped_html_escape(value).dup : value.to_s.dup
123
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
92
124
  end
93
- value.gsub!('"', "&quot;")
125
+ value = value.gsub('"', "&quot;") if value.include?('"')
94
126
  %(#{key}="#{value}")
95
127
  end
96
128
 
@@ -152,8 +184,8 @@ module ActionView
152
184
  # tag.input type: 'text', disabled: true
153
185
  # # => <input type="text" disabled="disabled">
154
186
  #
155
- # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
156
- # pointing to a hash of sub-attributes.
187
+ # HTML5 <tt>data-*</tt> and <tt>aria-*</tt> attributes can be set with a
188
+ # single +data+ or +aria+ key pointing to a hash of sub-attributes.
157
189
  #
158
190
  # To play nicely with JavaScript conventions, sub-attributes are dasherized.
159
191
  #
@@ -233,6 +265,9 @@ module ActionView
233
265
  #
234
266
  # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
235
267
  # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
268
+ #
269
+ # tag("div", class: { highlight: current_user.admin? })
270
+ # # => <div class="highlight" />
236
271
  def tag(name = nil, options = nil, open = false, escape = true)
237
272
  if name.nil?
238
273
  tag_builder
@@ -260,6 +295,8 @@ module ActionView
260
295
  # # => <div class="strong"><p>Hello world!</p></div>
261
296
  # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
262
297
  # # => <div class="strong highlight">Hello world!</div>
298
+ # content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
299
+ # # => <div class="strong highlight">Hello world!</div>
263
300
  # content_tag("select", options, multiple: true)
264
301
  # # => <select multiple="multiple">...options...</select>
265
302
  #
@@ -276,6 +313,24 @@ module ActionView
276
313
  end
277
314
  end
278
315
 
316
+ # Returns a string of tokens built from +args+.
317
+ #
318
+ # ==== Examples
319
+ # token_list("foo", "bar")
320
+ # # => "foo bar"
321
+ # token_list("foo", "foo bar")
322
+ # # => "foo bar"
323
+ # token_list({ foo: true, bar: false })
324
+ # # => "foo"
325
+ # token_list(nil, false, 123, "", "foo", { bar: true })
326
+ # # => "123 foo bar"
327
+ def token_list(*args)
328
+ tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
329
+
330
+ safe_join(tokens, " ")
331
+ end
332
+ alias_method :class_names, :token_list
333
+
279
334
  # Returns a CDATA section with the given +content+. CDATA sections
280
335
  # are used to escape blocks of text containing characters which would
281
336
  # otherwise be recognized as markup. CDATA sections begin with the string
@@ -306,6 +361,26 @@ module ActionView
306
361
  end
307
362
 
308
363
  private
364
+ def build_tag_values(*args)
365
+ tag_values = []
366
+
367
+ args.each do |tag_value|
368
+ case tag_value
369
+ when Hash
370
+ tag_value.each do |key, val|
371
+ tag_values << key.to_s if val && key.present?
372
+ end
373
+ when Array
374
+ tag_values.concat build_tag_values(*tag_value)
375
+ else
376
+ tag_values << tag_value.to_s if tag_value.present?
377
+ end
378
+ end
379
+
380
+ tag_values
381
+ end
382
+ module_function :build_tag_values
383
+
309
384
  def tag_builder
310
385
  @tag_builder ||= TagBuilder.new(self)
311
386
  end