actionview 6.0.6.1 → 6.1.0.rc1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -325
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/action_view/base.rb +20 -51
  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 +8 -6
  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/sanitize_helper.rb +2 -2
  23. data/lib/action_view/helpers/tag_helper.rb +96 -52
  24. data/lib/action_view/helpers/tags/base.rb +9 -5
  25. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  26. data/lib/action_view/helpers/tags/date_select.rb +2 -2
  27. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -1
  28. data/lib/action_view/helpers/tags/label.rb +4 -0
  29. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  30. data/lib/action_view/helpers/tags/select.rb +1 -1
  31. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  32. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  33. data/lib/action_view/helpers/text_helper.rb +1 -1
  34. data/lib/action_view/helpers/translation_helper.rb +88 -53
  35. data/lib/action_view/helpers/url_helper.rb +107 -13
  36. data/lib/action_view/layouts.rb +3 -2
  37. data/lib/action_view/log_subscriber.rb +26 -10
  38. data/lib/action_view/lookup_context.rb +3 -18
  39. data/lib/action_view/path_set.rb +0 -3
  40. data/lib/action_view/railtie.rb +35 -46
  41. data/lib/action_view/renderer/abstract_renderer.rb +93 -14
  42. data/lib/action_view/renderer/collection_renderer.rb +192 -0
  43. data/lib/action_view/renderer/object_renderer.rb +34 -0
  44. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +25 -26
  45. data/lib/action_view/renderer/partial_renderer.rb +20 -282
  46. data/lib/action_view/renderer/renderer.rb +44 -1
  47. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -1
  48. data/lib/action_view/renderer/template_renderer.rb +15 -12
  49. data/lib/action_view/rendering.rb +3 -1
  50. data/lib/action_view/routing_url_for.rb +1 -1
  51. data/lib/action_view/template/handlers/erb/erubi.rb +9 -7
  52. data/lib/action_view/template/handlers/erb.rb +10 -14
  53. data/lib/action_view/template/handlers.rb +0 -26
  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/template.rb +9 -49
  60. data/lib/action_view/test_case.rb +18 -25
  61. data/lib/action_view/testing/resolvers.rb +10 -31
  62. data/lib/action_view/unbound_template.rb +3 -3
  63. data/lib/action_view/view_paths.rb +34 -36
  64. data/lib/action_view.rb +4 -1
  65. data/lib/assets/compiled/rails-ujs.js +1 -1
  66. metadata +20 -18
@@ -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]
@@ -896,15 +897,16 @@ module ActionView
896
897
  end
897
898
 
898
899
  def set_default_disable_with(value, tag_options)
899
- data = tag_options.fetch("data", {})
900
+ return unless ActionView::Base.automatically_disable_submit_tag
901
+ data = tag_options["data"]
900
902
 
901
- if tag_options["data-disable-with"] == false || data["disable_with"] == false
902
- data.delete("disable_with")
903
- elsif ActionView::Base.automatically_disable_submit_tag
903
+ unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false)
904
904
  disable_with_text = tag_options["data-disable-with"]
905
- disable_with_text ||= data["disable_with"]
905
+ disable_with_text ||= data["disable_with"] if data
906
906
  disable_with_text ||= value.to_s.clone
907
907
  tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
908
+ else
909
+ data.delete("disable_with") if data
908
910
  end
909
911
 
910
912
  tag_options.delete("data-disable-with")
@@ -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
 
@@ -129,11 +129,11 @@ module ActionView
129
129
  end
130
130
 
131
131
  def sanitized_allowed_tags
132
- sanitizer_vendor.safe_list_sanitizer.allowed_tags
132
+ safe_list_sanitizer.allowed_tags
133
133
  end
134
134
 
135
135
  def sanitized_allowed_attributes
136
- sanitizer_vendor.safe_list_sanitizer.allowed_attributes
136
+ safe_list_sanitizer.allowed_attributes
137
137
  end
138
138
 
139
139
  # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
@@ -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,25 +49,22 @@ module ActionView
41
49
  @view_context = view_context
42
50
  end
43
51
 
44
- def tag_string(name, content = nil, **options, &block)
45
- escape = handle_deprecated_escape_options(options)
46
- content = @view_context.capture(self, &block) if block_given?
52
+ def p(*arguments, **options, &block)
53
+ tag_string(:p, *arguments, **options, &block)
54
+ end
47
55
 
56
+ def tag_string(name, content = nil, escape_attributes: true, **options, &block)
57
+ content = @view_context.capture(self, &block) if block_given?
48
58
  if VOID_ELEMENTS.include?(name) && content.nil?
49
- "<#{name.to_s.dasherize}#{tag_options(options, escape)}>".html_safe
59
+ "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe
50
60
  else
51
- content_tag_string(name.to_s.dasherize, content || "", options, escape)
61
+ content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes)
52
62
  end
53
63
  end
54
64
 
55
65
  def content_tag_string(name, content, options, escape = true)
56
66
  tag_options = tag_options(options, escape) if options
57
-
58
- if escape
59
- name = ERB::Util.xml_name_escape(name)
60
- content = ERB::Util.unwrapped_html_escape(content)
61
- end
62
-
67
+ content = ERB::Util.unwrapped_html_escape(content) if escape
63
68
  "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
64
69
  end
65
70
 
@@ -68,13 +73,31 @@ module ActionView
68
73
  output = +""
69
74
  sep = " "
70
75
  options.each_pair do |key, value|
71
- 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)
72
84
  value.each_pair do |k, v|
73
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
+
74
97
  output << sep
75
98
  output << prefix_tag_option(key, k, v, escape)
76
99
  end
77
- elsif BOOLEAN_ATTRIBUTES.include?(key)
100
+ elsif type == :boolean
78
101
  if value
79
102
  output << sep
80
103
  output << boolean_tag_option(key)
@@ -92,14 +115,14 @@ module ActionView
92
115
  end
93
116
 
94
117
  def tag_option(key, value, escape)
95
- key = ERB::Util.xml_name_escape(key) if escape
96
-
97
- if value.is_a?(Array)
118
+ case value
119
+ when Array, Hash
120
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
98
121
  value = escape ? safe_join(value, " ") : value.join(" ")
99
122
  else
100
- 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
101
124
  end
102
- value.gsub!('"', "&quot;")
125
+ value = value.gsub('"', "&quot;") if value.include?('"')
103
126
  %(#{key}="#{value}")
104
127
  end
105
128
 
@@ -116,27 +139,6 @@ module ActionView
116
139
  true
117
140
  end
118
141
 
119
- def handle_deprecated_escape_options(options)
120
- # The option :escape_attributes has been merged into the options hash to be
121
- # able to warn when it is used, so we need to handle default values here.
122
- escape_option_provided = options.has_key?(:escape)
123
- escape_attributes_option_provided = options.has_key?(:escape_attributes)
124
-
125
- if escape_attributes_option_provided
126
- ActiveSupport::Deprecation.warn(<<~MSG)
127
- Use of the option :escape_attributes is deprecated. It currently \
128
- escapes both names and values of tags and attributes and it is \
129
- equivalent to :escape. If any of them are enabled, the escaping \
130
- is fully enabled.
131
- MSG
132
- end
133
-
134
- return true unless escape_option_provided || escape_attributes_option_provided
135
- escape_option = options.delete(:escape)
136
- escape_attributes_option = options.delete(:escape_attributes)
137
- escape_option || escape_attributes_option
138
- end
139
-
140
142
  def method_missing(called, *args, **options, &block)
141
143
  tag_string(called, *args, **options, &block)
142
144
  end
@@ -182,8 +184,8 @@ module ActionView
182
184
  # tag.input type: 'text', disabled: true
183
185
  # # => <input type="text" disabled="disabled">
184
186
  #
185
- # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
186
- # 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.
187
189
  #
188
190
  # To play nicely with JavaScript conventions, sub-attributes are dasherized.
189
191
  #
@@ -263,11 +265,13 @@ module ActionView
263
265
  #
264
266
  # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
265
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" />
266
271
  def tag(name = nil, options = nil, open = false, escape = true)
267
272
  if name.nil?
268
273
  tag_builder
269
274
  else
270
- name = ERB::Util.xml_name_escape(name) if escape
271
275
  "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
272
276
  end
273
277
  end
@@ -291,6 +295,8 @@ module ActionView
291
295
  # # => <div class="strong"><p>Hello world!</p></div>
292
296
  # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
293
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>
294
300
  # content_tag("select", options, multiple: true)
295
301
  # # => <select multiple="multiple">...options...</select>
296
302
  #
@@ -307,6 +313,24 @@ module ActionView
307
313
  end
308
314
  end
309
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
+
310
334
  # Returns a CDATA section with the given +content+. CDATA sections
311
335
  # are used to escape blocks of text containing characters which would
312
336
  # otherwise be recognized as markup. CDATA sections begin with the string
@@ -337,6 +361,26 @@ module ActionView
337
361
  end
338
362
 
339
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
+
340
384
  def tag_builder
341
385
  @tag_builder ||= TagBuilder.new(self)
342
386
  end