actionview 4.2.11.1 → 7.0.2.4

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -215
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +116 -43
  6. data/lib/action_view/buffers.rb +20 -3
  7. data/lib/action_view/cache_expiry.rb +66 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  10. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  11. data/lib/action_view/dependency_tracker.rb +21 -122
  12. data/lib/action_view/digestor.rb +92 -85
  13. data/lib/action_view/flows.rb +15 -16
  14. data/lib/action_view/gem_version.rb +6 -4
  15. data/lib/action_view/helpers/active_model_helper.rb +17 -12
  16. data/lib/action_view/helpers/asset_tag_helper.rb +356 -101
  17. data/lib/action_view/helpers/asset_url_helper.rb +180 -74
  18. data/lib/action_view/helpers/atom_feed_helper.rb +21 -19
  19. data/lib/action_view/helpers/cache_helper.rb +156 -43
  20. data/lib/action_view/helpers/capture_helper.rb +21 -14
  21. data/lib/action_view/helpers/controller_helper.rb +16 -5
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  24. data/lib/action_view/helpers/date_helper.rb +288 -132
  25. data/lib/action_view/helpers/debug_helper.rb +9 -6
  26. data/lib/action_view/helpers/form_helper.rb +956 -173
  27. data/lib/action_view/helpers/form_options_helper.rb +178 -97
  28. data/lib/action_view/helpers/form_tag_helper.rb +220 -101
  29. data/lib/action_view/helpers/javascript_helper.rb +33 -19
  30. data/lib/action_view/helpers/number_helper.rb +88 -63
  31. data/lib/action_view/helpers/output_safety_helper.rb +38 -6
  32. data/lib/action_view/helpers/rendering_helper.rb +21 -10
  33. data/lib/action_view/helpers/sanitize_helper.rb +31 -32
  34. data/lib/action_view/helpers/tag_helper.rb +332 -71
  35. data/lib/action_view/helpers/tags/base.rb +123 -99
  36. data/lib/action_view/helpers/tags/check_box.rb +21 -20
  37. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  38. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  39. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  40. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  41. data/lib/action_view/helpers/tags/collection_select.rb +5 -3
  42. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  43. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  44. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  45. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  46. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  48. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  49. data/lib/action_view/helpers/tags/file_field.rb +18 -0
  50. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  51. data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
  52. data/lib/action_view/helpers/tags/label.rb +7 -2
  53. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  54. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  55. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  56. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  57. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  58. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  59. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  60. data/lib/action_view/helpers/tags/select.rb +11 -10
  61. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  62. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  63. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  64. data/lib/action_view/helpers/tags/time_field.rb +12 -2
  65. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  66. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  67. data/lib/action_view/helpers/tags/translator.rb +15 -16
  68. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  69. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  70. data/lib/action_view/helpers/tags/weekday_select.rb +28 -0
  71. data/lib/action_view/helpers/tags.rb +5 -2
  72. data/lib/action_view/helpers/text_helper.rb +80 -51
  73. data/lib/action_view/helpers/translation_helper.rb +120 -69
  74. data/lib/action_view/helpers/url_helper.rb +398 -171
  75. data/lib/action_view/helpers.rb +29 -27
  76. data/lib/action_view/layouts.rb +68 -63
  77. data/lib/action_view/log_subscriber.rb +77 -10
  78. data/lib/action_view/lookup_context.rb +137 -113
  79. data/lib/action_view/model_naming.rb +4 -2
  80. data/lib/action_view/path_set.rb +28 -32
  81. data/lib/action_view/railtie.rb +74 -13
  82. data/lib/action_view/record_identifier.rb +53 -26
  83. data/lib/action_view/render_parser.rb +188 -0
  84. data/lib/action_view/renderer/abstract_renderer.rb +152 -15
  85. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  86. data/lib/action_view/renderer/object_renderer.rb +34 -0
  87. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  88. data/lib/action_view/renderer/partial_renderer.rb +51 -333
  89. data/lib/action_view/renderer/renderer.rb +68 -11
  90. data/lib/action_view/renderer/streaming_template_renderer.rb +60 -56
  91. data/lib/action_view/renderer/template_renderer.rb +87 -74
  92. data/lib/action_view/rendering.rb +73 -47
  93. data/lib/action_view/ripper_ast_parser.rb +198 -0
  94. data/lib/action_view/routing_url_for.rb +35 -24
  95. data/lib/action_view/tasks/cache_digests.rake +25 -0
  96. data/lib/action_view/template/error.rb +151 -41
  97. data/lib/action_view/template/handlers/builder.rb +12 -13
  98. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  99. data/lib/action_view/template/handlers/erb.rb +29 -89
  100. data/lib/action_view/template/handlers/html.rb +11 -0
  101. data/lib/action_view/template/handlers/raw.rb +4 -4
  102. data/lib/action_view/template/handlers.rb +14 -10
  103. data/lib/action_view/template/html.rb +12 -13
  104. data/lib/action_view/template/inline.rb +22 -0
  105. data/lib/action_view/template/raw_file.rb +25 -0
  106. data/lib/action_view/template/renderable.rb +24 -0
  107. data/lib/action_view/template/resolver.rb +139 -300
  108. data/lib/action_view/template/sources/file.rb +17 -0
  109. data/lib/action_view/template/sources.rb +13 -0
  110. data/lib/action_view/template/text.rb +10 -12
  111. data/lib/action_view/template/types.rb +28 -26
  112. data/lib/action_view/template.rb +123 -91
  113. data/lib/action_view/template_details.rb +66 -0
  114. data/lib/action_view/template_path.rb +64 -0
  115. data/lib/action_view/test_case.rb +70 -53
  116. data/lib/action_view/testing/resolvers.rb +25 -35
  117. data/lib/action_view/unbound_template.rb +57 -0
  118. data/lib/action_view/version.rb +3 -1
  119. data/lib/action_view/view_paths.rb +73 -58
  120. data/lib/action_view.rb +16 -11
  121. data/lib/assets/compiled/rails-ujs.js +746 -0
  122. metadata +52 -32
  123. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  124. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,28 +1,28 @@
1
- require 'active_support/core_ext/object/try'
2
- require 'active_support/deprecation'
3
- require 'rails-html-sanitizer'
1
+ # frozen_string_literal: true
2
+
3
+ require "rails-html-sanitizer"
4
4
 
5
5
  module ActionView
6
6
  # = Action View Sanitize Helpers
7
- module Helpers
7
+ module Helpers # :nodoc:
8
8
  # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
9
9
  # These helper methods extend Action View making them callable within your template files.
10
10
  module SanitizeHelper
11
11
  extend ActiveSupport::Concern
12
- # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted.
12
+ # Sanitizes HTML input, stripping all but known-safe tags and attributes.
13
13
  #
14
14
  # It also strips href/src attributes with unsafe protocols like
15
15
  # <tt>javascript:</tt>, while also protecting against attempts to use Unicode,
16
16
  # ASCII, and hex character references to work around these protocol filters.
17
+ # All special characters will be escaped.
17
18
  #
18
- # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML
19
+ # The default sanitizer is Rails::Html::SafeListSanitizer. See {Rails HTML
19
20
  # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
20
21
  #
21
22
  # Custom sanitization rules can also be provided.
22
23
  #
23
24
  # Please note that sanitizing user-provided text does not guarantee that the
24
- # resulting markup is valid or even well-formed. For example, the output may still
25
- # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>.
25
+ # resulting markup is valid or even well-formed.
26
26
  #
27
27
  # ==== Options
28
28
  #
@@ -39,24 +39,22 @@ module ActionView
39
39
  #
40
40
  # <%= sanitize @comment.body %>
41
41
  #
42
- # Providing custom whitelisted tags and attributes:
42
+ # Providing custom lists of permitted tags and attributes:
43
43
  #
44
44
  # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>
45
45
  #
46
46
  # Providing a custom Rails::Html scrubber:
47
47
  #
48
48
  # class CommentScrubber < Rails::Html::PermitScrubber
49
- # def allowed_node?(node)
50
- # !%w(form script comment blockquote).include?(node.name)
49
+ # def initialize
50
+ # super
51
+ # self.tags = %w( form script comment blockquote )
52
+ # self.attributes = %w( style )
51
53
  # end
52
54
  #
53
55
  # def skip_node?(node)
54
56
  # node.text?
55
57
  # end
56
- #
57
- # def scrub_attribute?(name)
58
- # name == 'style'
59
- # end
60
58
  # end
61
59
  #
62
60
  # <%= sanitize @comment.body, scrubber: CommentScrubber.new %>
@@ -81,15 +79,15 @@ module ActionView
81
79
  # config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
82
80
  # config.action_view.sanitized_allowed_attributes = ['href', 'title']
83
81
  def sanitize(html, options = {})
84
- self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe)
82
+ self.class.safe_list_sanitizer.sanitize(html, options)&.html_safe
85
83
  end
86
84
 
87
85
  # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
88
86
  def sanitize_css(style)
89
- self.class.white_list_sanitizer.sanitize_css(style)
87
+ self.class.safe_list_sanitizer.sanitize_css(style)
90
88
  end
91
89
 
92
- # Strips all HTML tags from +html+, including comments.
90
+ # Strips all HTML tags from +html+, including comments and special characters.
93
91
  #
94
92
  # strip_tags("Strip <i>these</i> tags!")
95
93
  # # => Strip these tags!
@@ -99,8 +97,11 @@ module ActionView
99
97
  #
100
98
  # strip_tags("<div id='top-bar'>Welcome to my website!</div>")
101
99
  # # => Welcome to my website!
100
+ #
101
+ # strip_tags("> A quote from Smith & Wesson")
102
+ # # => &gt; A quote from Smith &amp; Wesson
102
103
  def strip_tags(html)
103
- self.class.full_sanitizer.sanitize(html, encode_special_chars: false)
104
+ self.class.full_sanitizer.sanitize(html)
104
105
  end
105
106
 
106
107
  # Strips all link tags from +html+ leaving just the link text.
@@ -113,25 +114,26 @@ module ActionView
113
114
  #
114
115
  # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
115
116
  # # => Blog: Visit.
117
+ #
118
+ # strip_links('<<a href="https://example.org">malformed & link</a>')
119
+ # # => &lt;malformed &amp; link
116
120
  def strip_links(html)
117
121
  self.class.link_sanitizer.sanitize(html)
118
122
  end
119
123
 
120
- module ClassMethods #:nodoc:
121
- attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
124
+ module ClassMethods # :nodoc:
125
+ attr_writer :full_sanitizer, :link_sanitizer, :safe_list_sanitizer
122
126
 
123
- # Vendors the full, link and white list sanitizers.
124
- # Provided strictly for compabitility and can be removed in Rails 5.
125
127
  def sanitizer_vendor
126
128
  Rails::Html::Sanitizer
127
129
  end
128
130
 
129
131
  def sanitized_allowed_tags
130
- sanitizer_vendor.white_list_sanitizer.allowed_tags
132
+ sanitizer_vendor.safe_list_sanitizer.allowed_tags
131
133
  end
132
134
 
133
135
  def sanitized_allowed_attributes
134
- sanitizer_vendor.white_list_sanitizer.allowed_attributes
136
+ sanitizer_vendor.safe_list_sanitizer.allowed_attributes
135
137
  end
136
138
 
137
139
  # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
@@ -140,7 +142,6 @@ module ActionView
140
142
  # class Application < Rails::Application
141
143
  # config.action_view.full_sanitizer = MySpecialSanitizer.new
142
144
  # end
143
- #
144
145
  def full_sanitizer
145
146
  @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
146
147
  end
@@ -151,20 +152,18 @@ module ActionView
151
152
  # class Application < Rails::Application
152
153
  # config.action_view.link_sanitizer = MySpecialSanitizer.new
153
154
  # end
154
- #
155
155
  def link_sanitizer
156
156
  @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
157
157
  end
158
158
 
159
- # Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
159
+ # Gets the Rails::Html::SafeListSanitizer instance used by sanitize and +sanitize_css+.
160
160
  # Replace with any object that responds to +sanitize+.
161
161
  #
162
162
  # class Application < Rails::Application
163
- # config.action_view.white_list_sanitizer = MySpecialSanitizer.new
163
+ # config.action_view.safe_list_sanitizer = MySpecialSanitizer.new
164
164
  # end
165
- #
166
- def white_list_sanitizer
167
- @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new
165
+ def safe_list_sanitizer
166
+ @safe_list_sanitizer ||= sanitizer_vendor.safe_list_sanitizer.new
168
167
  end
169
168
  end
170
169
  end
@@ -1,38 +1,299 @@
1
- require 'active_support/core_ext/string/output_safety'
2
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
4
+ require "active_support/core_ext/string/output_safety"
5
+ require "set"
6
+ require "action_view/helpers/capture_helper"
7
+ require "action_view/helpers/output_safety_helper"
3
8
 
4
9
  module ActionView
5
10
  # = Action View Tag Helpers
6
- module Helpers #:nodoc:
7
- # Provides methods to generate HTML tags programmatically when you can't use
8
- # a Builder. By default, they output XHTML compliant tags.
11
+ module Helpers # :nodoc:
12
+ # Provides methods to generate HTML tags programmatically both as a modern
13
+ # HTML5 compliant builder style and legacy XHTML compliant tags.
9
14
  module TagHelper
10
- extend ActiveSupport::Concern
11
15
  include CaptureHelper
12
16
  include OutputSafetyHelper
13
17
 
14
- BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
15
- autoplay controls loop selected hidden scoped async
16
- defer reversed ismap seamless muted required
17
- autofocus novalidate formnovalidate open pubdate
18
- itemscope allowfullscreen default inert sortable
19
- truespeed typemustmatch).to_set
18
+ BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
19
+ autoplay checked compact controls declare default
20
+ defaultchecked defaultmuted defaultselected defer
21
+ disabled enabled formnovalidate hidden indeterminate
22
+ inert ismap itemscope loop multiple muted nohref
23
+ nomodule noresize noshade novalidate nowrap open
24
+ pauseonexit playsinline readonly required reversed
25
+ scoped seamless selected sortable truespeed
26
+ typemustmatch visible).to_set
27
+
28
+ BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
29
+ BOOLEAN_ATTRIBUTES.freeze
30
+
31
+ ARIA_PREFIXES = ["aria", :aria].to_set.freeze
32
+ DATA_PREFIXES = ["data", :data].to_set.freeze
33
+
34
+ TAG_TYPES = {}
35
+ TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
36
+ TAG_TYPES.merge! DATA_PREFIXES.index_with(:data)
37
+ TAG_TYPES.merge! ARIA_PREFIXES.index_with(:aria)
38
+ TAG_TYPES.freeze
39
+
40
+ PRE_CONTENT_STRINGS = Hash.new { "" }
41
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
42
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
43
+
44
+ class TagBuilder # :nodoc:
45
+ include CaptureHelper
46
+ include OutputSafetyHelper
47
+
48
+ HTML_VOID_ELEMENTS = %i(area base br col circle embed hr img input keygen link meta param source track wbr).to_set
49
+ SVG_VOID_ELEMENTS = %i(animate animateMotion animateTransform circle ellipse line path polygon polyline rect set stop use view).to_set
50
+
51
+ def initialize(view_context)
52
+ @view_context = view_context
53
+ end
54
+
55
+ # Transforms a Hash into HTML Attributes, ready to be interpolated into
56
+ # ERB.
57
+ #
58
+ # <input <%= tag.attributes(type: :text, aria: { label: "Search" }) %> >
59
+ # # => <input type="text" aria-label="Search">
60
+ def attributes(attributes)
61
+ tag_options(attributes.to_h).to_s.strip.html_safe
62
+ end
63
+
64
+ def p(*arguments, **options, &block)
65
+ tag_string(:p, *arguments, **options, &block)
66
+ end
67
+
68
+ def tag_string(name, content = nil, **options, &block)
69
+ escape = handle_deprecated_escape_options(options)
70
+
71
+ content = @view_context.capture(self, &block) if block_given?
72
+ if (HTML_VOID_ELEMENTS.include?(name) || SVG_VOID_ELEMENTS.include?(name)) && content.nil?
73
+ "<#{name.to_s.dasherize}#{tag_options(options, escape)}>".html_safe
74
+ else
75
+ content_tag_string(name.to_s.dasherize, content || "", options, escape)
76
+ end
77
+ end
78
+
79
+ def content_tag_string(name, content, options, escape = true)
80
+ tag_options = tag_options(options, escape) if options
81
+
82
+ if escape
83
+ name = ERB::Util.xml_name_escape(name)
84
+ content = ERB::Util.unwrapped_html_escape(content)
85
+ end
86
+
87
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
88
+ end
89
+
90
+ def tag_options(options, escape = true)
91
+ return if options.blank?
92
+ output = +""
93
+ sep = " "
94
+ options.each_pair do |key, value|
95
+ type = TAG_TYPES[key]
96
+ if type == :data && value.is_a?(Hash)
97
+ value.each_pair do |k, v|
98
+ next if v.nil?
99
+ output << sep
100
+ output << prefix_tag_option(key, k, v, escape)
101
+ end
102
+ elsif type == :aria && value.is_a?(Hash)
103
+ value.each_pair do |k, v|
104
+ next if v.nil?
105
+
106
+ case v
107
+ when Array, Hash
108
+ tokens = TagHelper.build_tag_values(v)
109
+ next if tokens.none?
110
+
111
+ v = safe_join(tokens, " ")
112
+ else
113
+ v = v.to_s
114
+ end
115
+
116
+ output << sep
117
+ output << prefix_tag_option(key, k, v, escape)
118
+ end
119
+ elsif type == :boolean
120
+ if value
121
+ output << sep
122
+ output << boolean_tag_option(key)
123
+ end
124
+ elsif !value.nil?
125
+ output << sep
126
+ output << tag_option(key, value, escape)
127
+ end
128
+ end
129
+ output unless output.empty?
130
+ end
131
+
132
+ def boolean_tag_option(key)
133
+ %(#{key}="#{key}")
134
+ end
20
135
 
21
- BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
136
+ def tag_option(key, value, escape)
137
+ key = ERB::Util.xml_name_escape(key) if escape
22
138
 
23
- TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
139
+ case value
140
+ when Array, Hash
141
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
142
+ value = escape ? safe_join(value, " ") : value.join(" ")
143
+ when Regexp
144
+ value = escape ? ERB::Util.unwrapped_html_escape(value.source) : value.source
145
+ else
146
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
147
+ end
148
+ value = value.gsub('"', "&quot;") if value.include?('"')
24
149
 
25
- PRE_CONTENT_STRINGS = {
26
- :textarea => "\n"
27
- }
150
+ %(#{key}="#{value}")
151
+ end
28
152
 
29
- # Returns an empty HTML tag of type +name+ which by default is XHTML
153
+ private
154
+ def prefix_tag_option(prefix, key, value, escape)
155
+ key = "#{prefix}-#{key.to_s.dasherize}"
156
+ unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
157
+ value = value.to_json
158
+ end
159
+ tag_option(key, value, escape)
160
+ end
161
+
162
+ def respond_to_missing?(*args)
163
+ true
164
+ end
165
+
166
+ def handle_deprecated_escape_options(options)
167
+ # The option :escape_attributes has been merged into the options hash to be
168
+ # able to warn when it is used, so we need to handle default values here.
169
+ escape_option_provided = options.has_key?(:escape)
170
+ escape_attributes_option_provided = options.has_key?(:escape_attributes)
171
+
172
+ if escape_attributes_option_provided
173
+ ActiveSupport::Deprecation.warn(<<~MSG)
174
+ Use of the option :escape_attributes is deprecated. It currently \
175
+ escapes both names and values of tags and attributes and it is \
176
+ equivalent to :escape. If any of them are enabled, the escaping \
177
+ is fully enabled.
178
+ MSG
179
+ end
180
+
181
+ return true unless escape_option_provided || escape_attributes_option_provided
182
+ escape_option = options.delete(:escape)
183
+ escape_attributes_option = options.delete(:escape_attributes)
184
+ escape_option || escape_attributes_option
185
+ end
186
+
187
+ def method_missing(called, *args, **options, &block)
188
+ tag_string(called, *args, **options, &block)
189
+ end
190
+ end
191
+
192
+ # Returns an HTML tag.
193
+ #
194
+ # === Building HTML tags
195
+ #
196
+ # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with:
197
+ #
198
+ # tag.<tag name>(optional content, options)
199
+ #
200
+ # where tag name can be e.g. br, div, section, article, or any tag really.
201
+ #
202
+ # ==== Passing content
203
+ #
204
+ # Tags can pass content to embed within it:
205
+ #
206
+ # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1>
207
+ #
208
+ # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div>
209
+ #
210
+ # Content can also be captured with a block, which is useful in templates:
211
+ #
212
+ # <%= tag.p do %>
213
+ # The next great American novel starts here.
214
+ # <% end %>
215
+ # # => <p>The next great American novel starts here.</p>
216
+ #
217
+ # ==== Options
218
+ #
219
+ # Use symbol keyed options to add attributes to the generated tag.
220
+ #
221
+ # tag.section class: %w( kitties puppies )
222
+ # # => <section class="kitties puppies"></section>
223
+ #
224
+ # tag.section id: dom_id(@post)
225
+ # # => <section id="<generated dom id>"></section>
226
+ #
227
+ # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+.
228
+ #
229
+ # tag.input type: 'text', disabled: true
230
+ # # => <input type="text" disabled="disabled">
231
+ #
232
+ # HTML5 <tt>data-*</tt> and <tt>aria-*</tt> attributes can be set with a
233
+ # single +data+ or +aria+ key pointing to a hash of sub-attributes.
234
+ #
235
+ # To play nicely with JavaScript conventions, sub-attributes are dasherized.
236
+ #
237
+ # tag.article data: { user_id: 123 }
238
+ # # => <article data-user-id="123"></article>
239
+ #
240
+ # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>.
241
+ #
242
+ # Data attribute values are encoded to JSON, with the exception of strings, symbols and
243
+ # BigDecimals.
244
+ # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
245
+ # from 1.4.3.
246
+ #
247
+ # tag.div data: { city_state: %w( Chicago IL ) }
248
+ # # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
249
+ #
250
+ # The generated tag names and attributes are escaped by default. This can be disabled using
251
+ # +escape+.
252
+ #
253
+ # tag.img src: 'open & shut.png'
254
+ # # => <img src="open &amp; shut.png">
255
+ #
256
+ # tag.img src: 'open & shut.png', escape: false
257
+ # # => <img src="open & shut.png">
258
+ #
259
+ # The tag builder respects
260
+ # {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements]
261
+ # if no content is passed, and omits closing tags for those elements.
262
+ #
263
+ # # A standard element:
264
+ # tag.div # => <div></div>
265
+ #
266
+ # # A void element:
267
+ # tag.br # => <br>
268
+ #
269
+ # === Building HTML attributes
270
+ #
271
+ # Transforms a Hash into HTML attributes, ready to be interpolated into
272
+ # ERB. Includes or omits boolean attributes based on their truthiness.
273
+ # Transforms keys nested within
274
+ # <tt>aria:</tt> or <tt>data:</tt> objects into `aria-` and `data-`
275
+ # prefixed attributes:
276
+ #
277
+ # <input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
278
+ # # => <input type="text" aria-label="Search">
279
+ #
280
+ # <button <%= tag.attributes id: "call-to-action", disabled: false, aria: { expanded: false } %> class="primary">Get Started!</button>
281
+ # # => <button id="call-to-action" aria-expanded="false" class="primary">Get Started!</button>
282
+ #
283
+ # === Legacy syntax
284
+ #
285
+ # The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
286
+ #
287
+ # tag(name, options = nil, open = false, escape = true)
288
+ #
289
+ # It returns an empty HTML tag of type +name+ which by default is XHTML
30
290
  # compliant. Set +open+ to true to create an open tag compatible
31
291
  # with HTML 4.0 and below. Add HTML attributes by passing an attributes
32
292
  # hash to +options+. Set +escape+ to false to disable attribute value
33
293
  # escaping.
34
294
  #
35
295
  # ==== Options
296
+ #
36
297
  # You can use symbols or strings for the attribute names.
37
298
  #
38
299
  # Use +true+ with boolean attributes that can render with no value, like
@@ -41,16 +302,8 @@ module ActionView
41
302
  # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
42
303
  # pointing to a hash of sub-attributes.
43
304
  #
44
- # To play nicely with JavaScript conventions sub-attributes are dasherized.
45
- # For example, a key +user_id+ would render as <tt>data-user-id</tt> and
46
- # thus accessed as <tt>dataset.userId</tt>.
47
- #
48
- # Values are encoded to JSON, with the exception of strings, symbols and
49
- # BigDecimals.
50
- # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
51
- # from 1.4.3.
52
- #
53
305
  # ==== Examples
306
+ #
54
307
  # tag("br")
55
308
  # # => <br />
56
309
  #
@@ -66,20 +319,29 @@ module ActionView
66
319
  # tag("img", src: "open & shut.png")
67
320
  # # => <img src="open &amp; shut.png" />
68
321
  #
69
- # tag("img", {src: "open &amp; shut.png"}, false, false)
322
+ # tag("img", { src: "open &amp; shut.png" }, false, false)
70
323
  # # => <img src="open &amp; shut.png" />
71
324
  #
72
- # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)})
325
+ # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
73
326
  # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
74
- def tag(name, options = nil, open = false, escape = true)
75
- "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
327
+ #
328
+ # tag("div", class: { highlight: current_user.admin? })
329
+ # # => <div class="highlight" />
330
+ def tag(name = nil, options = nil, open = false, escape = true)
331
+ if name.nil?
332
+ tag_builder
333
+ else
334
+ name = ERB::Util.xml_name_escape(name) if escape
335
+ "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
336
+ end
76
337
  end
77
338
 
78
339
  # Returns an HTML block tag of type +name+ surrounding the +content+. Add
79
340
  # HTML attributes by passing an attributes hash to +options+.
80
341
  # Instead of passing the content as an argument, you can also use a block
81
342
  # in which case, you pass your +options+ as the second parameter.
82
- # Set escape to false to disable attribute value escaping.
343
+ # Set escape to false to disable escaping.
344
+ # Note: this is legacy syntax, see +tag+ method description for details.
83
345
  #
84
346
  # ==== Options
85
347
  # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
@@ -93,6 +355,8 @@ module ActionView
93
355
  # # => <div class="strong"><p>Hello world!</p></div>
94
356
  # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
95
357
  # # => <div class="strong highlight">Hello world!</div>
358
+ # content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
359
+ # # => <div class="strong highlight">Hello world!</div>
96
360
  # content_tag("select", options, multiple: true)
97
361
  # # => <select multiple="multiple">...options...</select>
98
362
  #
@@ -103,12 +367,30 @@ module ActionView
103
367
  def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
104
368
  if block_given?
105
369
  options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
106
- content_tag_string(name, capture(&block), options, escape)
370
+ tag_builder.content_tag_string(name, capture(&block), options, escape)
107
371
  else
108
- content_tag_string(name, content_or_options_with_block, options, escape)
372
+ tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
109
373
  end
110
374
  end
111
375
 
376
+ # Returns a string of tokens built from +args+.
377
+ #
378
+ # ==== Examples
379
+ # token_list("foo", "bar")
380
+ # # => "foo bar"
381
+ # token_list("foo", "foo bar")
382
+ # # => "foo bar"
383
+ # token_list({ foo: true, bar: false })
384
+ # # => "foo"
385
+ # token_list(nil, false, 123, "", "foo", { bar: true })
386
+ # # => "123 foo bar"
387
+ def token_list(*args)
388
+ tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
389
+
390
+ safe_join(tokens, " ")
391
+ end
392
+ alias_method :class_names, :token_list
393
+
112
394
  # Returns a CDATA section with the given +content+. CDATA sections
113
395
  # are used to escape blocks of text containing characters which would
114
396
  # otherwise be recognized as markup. CDATA sections begin with the string
@@ -123,7 +405,7 @@ module ActionView
123
405
  # cdata_section("hello]]>world")
124
406
  # # => <![CDATA[hello]]]]><![CDATA[>world]]>
125
407
  def cdata_section(content)
126
- splitted = content.to_s.gsub(/\]\]\>/, ']]]]><![CDATA[>')
408
+ splitted = content.to_s.gsub(/\]\]>/, "]]]]><![CDATA[>")
127
409
  "<![CDATA[#{splitted}]]>".html_safe
128
410
  end
129
411
 
@@ -139,49 +421,28 @@ module ActionView
139
421
  end
140
422
 
141
423
  private
424
+ def build_tag_values(*args)
425
+ tag_values = []
142
426
 
143
- def content_tag_string(name, content, options, escape = true)
144
- tag_options = tag_options(options, escape) if options
145
- content = ERB::Util.unwrapped_html_escape(content) if escape
146
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
147
- end
148
-
149
- def tag_options(options, escape = true)
150
- return if options.blank?
151
- attrs = []
152
- options.each_pair do |key, value|
153
- if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
154
- value.each_pair do |k, v|
155
- attrs << prefix_tag_option(key, k, v, escape)
427
+ args.each do |tag_value|
428
+ case tag_value
429
+ when Hash
430
+ tag_value.each do |key, val|
431
+ tag_values << key.to_s if val && key.present?
156
432
  end
157
- elsif BOOLEAN_ATTRIBUTES.include?(key)
158
- attrs << boolean_tag_option(key) if value
159
- elsif !value.nil?
160
- attrs << tag_option(key, value, escape)
433
+ when Array
434
+ tag_values.concat build_tag_values(*tag_value)
435
+ else
436
+ tag_values << tag_value.to_s if tag_value.present?
161
437
  end
162
438
  end
163
- " #{attrs * ' '}" unless attrs.empty?
164
- end
165
439
 
166
- def prefix_tag_option(prefix, key, value, escape)
167
- key = "#{prefix}-#{key.to_s.dasherize}"
168
- unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
169
- value = value.to_json
170
- end
171
- tag_option(key, value, escape)
440
+ tag_values
172
441
  end
442
+ module_function :build_tag_values
173
443
 
174
- def boolean_tag_option(key)
175
- %(#{key}="#{key}")
176
- end
177
-
178
- def tag_option(key, value, escape)
179
- if value.is_a?(Array)
180
- value = escape ? safe_join(value, " ") : value.join(" ")
181
- else
182
- value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
183
- end
184
- %(#{key}="#{value.gsub('"'.freeze, '&quot;'.freeze)}")
444
+ def tag_builder
445
+ @tag_builder ||= TagBuilder.new(self)
185
446
  end
186
447
  end
187
448
  end