omg-actionview 8.0.0.alpha1

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 (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +40 -0
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +316 -0
  8. data/lib/action_view/buffers.rb +165 -0
  9. data/lib/action_view/cache_expiry.rb +69 -0
  10. data/lib/action_view/context.rb +32 -0
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +159 -0
  12. data/lib/action_view/dependency_tracker/ruby_tracker.rb +43 -0
  13. data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
  14. data/lib/action_view/dependency_tracker.rb +41 -0
  15. data/lib/action_view/deprecator.rb +7 -0
  16. data/lib/action_view/digestor.rb +130 -0
  17. data/lib/action_view/flows.rb +75 -0
  18. data/lib/action_view/gem_version.rb +17 -0
  19. data/lib/action_view/helpers/active_model_helper.rb +54 -0
  20. data/lib/action_view/helpers/asset_tag_helper.rb +680 -0
  21. data/lib/action_view/helpers/asset_url_helper.rb +473 -0
  22. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  23. data/lib/action_view/helpers/cache_helper.rb +315 -0
  24. data/lib/action_view/helpers/capture_helper.rb +236 -0
  25. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  26. data/lib/action_view/helpers/controller_helper.rb +42 -0
  27. data/lib/action_view/helpers/csp_helper.rb +26 -0
  28. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  29. data/lib/action_view/helpers/date_helper.rb +1266 -0
  30. data/lib/action_view/helpers/debug_helper.rb +38 -0
  31. data/lib/action_view/helpers/form_helper.rb +2765 -0
  32. data/lib/action_view/helpers/form_options_helper.rb +927 -0
  33. data/lib/action_view/helpers/form_tag_helper.rb +1088 -0
  34. data/lib/action_view/helpers/javascript_helper.rb +96 -0
  35. data/lib/action_view/helpers/number_helper.rb +165 -0
  36. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  37. data/lib/action_view/helpers/rendering_helper.rb +218 -0
  38. data/lib/action_view/helpers/sanitize_helper.rb +201 -0
  39. data/lib/action_view/helpers/tag_helper.rb +621 -0
  40. data/lib/action_view/helpers/tags/base.rb +138 -0
  41. data/lib/action_view/helpers/tags/check_box.rb +65 -0
  42. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  43. data/lib/action_view/helpers/tags/collection_check_boxes.rb +37 -0
  44. data/lib/action_view/helpers/tags/collection_helpers.rb +118 -0
  45. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  46. data/lib/action_view/helpers/tags/collection_select.rb +33 -0
  47. data/lib/action_view/helpers/tags/color_field.rb +26 -0
  48. data/lib/action_view/helpers/tags/date_field.rb +14 -0
  49. data/lib/action_view/helpers/tags/date_select.rb +75 -0
  50. data/lib/action_view/helpers/tags/datetime_field.rb +39 -0
  51. data/lib/action_view/helpers/tags/datetime_local_field.rb +29 -0
  52. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  53. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  54. data/lib/action_view/helpers/tags/file_field.rb +26 -0
  55. data/lib/action_view/helpers/tags/grouped_collection_select.rb +34 -0
  56. data/lib/action_view/helpers/tags/hidden_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/label.rb +84 -0
  58. data/lib/action_view/helpers/tags/month_field.rb +14 -0
  59. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  60. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  61. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  62. data/lib/action_view/helpers/tags/radio_button.rb +32 -0
  63. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  64. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  65. data/lib/action_view/helpers/tags/select.rb +45 -0
  66. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  67. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  68. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  69. data/lib/action_view/helpers/tags/text_field.rb +33 -0
  70. data/lib/action_view/helpers/tags/time_field.rb +23 -0
  71. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  72. data/lib/action_view/helpers/tags/time_zone_select.rb +25 -0
  73. data/lib/action_view/helpers/tags/translator.rb +39 -0
  74. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  75. data/lib/action_view/helpers/tags/week_field.rb +14 -0
  76. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  77. data/lib/action_view/helpers/tags.rb +47 -0
  78. data/lib/action_view/helpers/text_helper.rb +568 -0
  79. data/lib/action_view/helpers/translation_helper.rb +161 -0
  80. data/lib/action_view/helpers/url_helper.rb +812 -0
  81. data/lib/action_view/helpers.rb +68 -0
  82. data/lib/action_view/layouts.rb +434 -0
  83. data/lib/action_view/locale/en.yml +56 -0
  84. data/lib/action_view/log_subscriber.rb +132 -0
  85. data/lib/action_view/lookup_context.rb +299 -0
  86. data/lib/action_view/model_naming.rb +14 -0
  87. data/lib/action_view/path_registry.rb +57 -0
  88. data/lib/action_view/path_set.rb +84 -0
  89. data/lib/action_view/railtie.rb +132 -0
  90. data/lib/action_view/record_identifier.rb +118 -0
  91. data/lib/action_view/render_parser/prism_render_parser.rb +139 -0
  92. data/lib/action_view/render_parser/ripper_render_parser.rb +350 -0
  93. data/lib/action_view/render_parser.rb +40 -0
  94. data/lib/action_view/renderer/abstract_renderer.rb +186 -0
  95. data/lib/action_view/renderer/collection_renderer.rb +204 -0
  96. data/lib/action_view/renderer/object_renderer.rb +34 -0
  97. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +120 -0
  98. data/lib/action_view/renderer/partial_renderer.rb +267 -0
  99. data/lib/action_view/renderer/renderer.rb +107 -0
  100. data/lib/action_view/renderer/streaming_template_renderer.rb +107 -0
  101. data/lib/action_view/renderer/template_renderer.rb +115 -0
  102. data/lib/action_view/rendering.rb +190 -0
  103. data/lib/action_view/routing_url_for.rb +149 -0
  104. data/lib/action_view/tasks/cache_digests.rake +25 -0
  105. data/lib/action_view/template/error.rb +264 -0
  106. data/lib/action_view/template/handlers/builder.rb +25 -0
  107. data/lib/action_view/template/handlers/erb/erubi.rb +85 -0
  108. data/lib/action_view/template/handlers/erb.rb +157 -0
  109. data/lib/action_view/template/handlers/html.rb +11 -0
  110. data/lib/action_view/template/handlers/raw.rb +11 -0
  111. data/lib/action_view/template/handlers.rb +66 -0
  112. data/lib/action_view/template/html.rb +33 -0
  113. data/lib/action_view/template/inline.rb +22 -0
  114. data/lib/action_view/template/raw_file.rb +25 -0
  115. data/lib/action_view/template/renderable.rb +30 -0
  116. data/lib/action_view/template/resolver.rb +212 -0
  117. data/lib/action_view/template/sources/file.rb +17 -0
  118. data/lib/action_view/template/sources.rb +13 -0
  119. data/lib/action_view/template/text.rb +32 -0
  120. data/lib/action_view/template/types.rb +50 -0
  121. data/lib/action_view/template.rb +580 -0
  122. data/lib/action_view/template_details.rb +66 -0
  123. data/lib/action_view/template_path.rb +66 -0
  124. data/lib/action_view/test_case.rb +449 -0
  125. data/lib/action_view/testing/resolvers.rb +44 -0
  126. data/lib/action_view/unbound_template.rb +67 -0
  127. data/lib/action_view/version.rb +10 -0
  128. data/lib/action_view/view_paths.rb +117 -0
  129. data/lib/action_view.rb +104 -0
  130. metadata +275 -0
@@ -0,0 +1,621 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/code_generator"
4
+ require "active_support/core_ext/enumerable"
5
+ require "active_support/core_ext/string/output_safety"
6
+ require "active_support/core_ext/string/inflections"
7
+ require "set"
8
+ require "action_view/helpers/capture_helper"
9
+ require "action_view/helpers/output_safety_helper"
10
+
11
+ module ActionView
12
+ module Helpers # :nodoc:
13
+ # = Action View Tag \Helpers
14
+ #
15
+ # Provides methods to generate HTML tags programmatically both as a modern
16
+ # HTML5 compliant builder style and legacy XHTML compliant tags.
17
+ module TagHelper
18
+ include CaptureHelper
19
+ include OutputSafetyHelper
20
+
21
+ BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
22
+ autoplay checked compact controls declare default
23
+ defaultchecked defaultmuted defaultselected defer
24
+ disabled enabled formnovalidate hidden indeterminate
25
+ inert ismap itemscope loop multiple muted nohref
26
+ nomodule noresize noshade novalidate nowrap open
27
+ pauseonexit playsinline readonly required reversed
28
+ scoped seamless selected sortable truespeed
29
+ typemustmatch visible).to_set
30
+
31
+ BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
32
+ BOOLEAN_ATTRIBUTES.freeze
33
+
34
+ ARIA_PREFIXES = ["aria", :aria].to_set.freeze
35
+ DATA_PREFIXES = ["data", :data].to_set.freeze
36
+
37
+ TAG_TYPES = {}
38
+ TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
39
+ TAG_TYPES.merge! DATA_PREFIXES.index_with(:data)
40
+ TAG_TYPES.merge! ARIA_PREFIXES.index_with(:aria)
41
+ TAG_TYPES.freeze
42
+
43
+ PRE_CONTENT_STRINGS = Hash.new { "" }
44
+ PRE_CONTENT_STRINGS[:textarea] = "\n"
45
+ PRE_CONTENT_STRINGS["textarea"] = "\n"
46
+
47
+ class TagBuilder # :nodoc:
48
+ include CaptureHelper
49
+ include OutputSafetyHelper
50
+
51
+ def deprecated_void_content(name)
52
+ ActionView.deprecator.warn <<~TEXT
53
+ Putting content inside a void element (#{name}) is invalid
54
+ according to the HTML5 spec, and so it is being deprecated
55
+ without replacement. In Rails 8.0, passing content as a
56
+ positional argument will raise, and using a block will have
57
+ no effect.
58
+ TEXT
59
+ end
60
+
61
+ def self.define_element(name, code_generator:, method_name: name)
62
+ return if method_defined?(name)
63
+
64
+ code_generator.class_eval do |batch|
65
+ batch << "\n" <<
66
+ "def #{method_name}(content = nil, escape: true, **options, &block)" <<
67
+ " tag_string(#{name.inspect}, content, options, escape: escape, &block)" <<
68
+ "end"
69
+ end
70
+ end
71
+
72
+ def self.define_void_element(name, code_generator:, method_name: name)
73
+ code_generator.class_eval do |batch|
74
+ batch << "\n" <<
75
+ "def #{method_name}(content = nil, escape: true, **options, &block)" <<
76
+ " if content || block" <<
77
+ " deprecated_void_content(#{name.inspect})" <<
78
+ " tag_string(#{name.inspect}, content, options, escape: escape, &block)" <<
79
+ " else" <<
80
+ " self_closing_tag_string(#{name.inspect}, options, escape, '>')" <<
81
+ " end" <<
82
+ "end"
83
+ end
84
+ end
85
+
86
+ def self.define_self_closing_element(name, code_generator:, method_name: name)
87
+ code_generator.class_eval do |batch|
88
+ batch << "\n" <<
89
+ "def #{method_name}(content = nil, escape: true, **options, &block)" <<
90
+ " if content || block" <<
91
+ " tag_string(#{name.inspect}, content, options, escape: escape, &block)" <<
92
+ " else" <<
93
+ " self_closing_tag_string(#{name.inspect}, options, escape)" <<
94
+ " end" <<
95
+ "end"
96
+ end
97
+ end
98
+
99
+ ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
100
+ define_void_element :area, code_generator: code_generator
101
+ define_void_element :base, code_generator: code_generator
102
+ define_void_element :br, code_generator: code_generator
103
+ define_void_element :col, code_generator: code_generator
104
+ define_void_element :embed, code_generator: code_generator
105
+ define_void_element :hr, code_generator: code_generator
106
+ define_void_element :img, code_generator: code_generator
107
+ define_void_element :input, code_generator: code_generator
108
+ define_void_element :keygen, code_generator: code_generator
109
+ define_void_element :link, code_generator: code_generator
110
+ define_void_element :meta, code_generator: code_generator
111
+ define_void_element :source, code_generator: code_generator
112
+ define_void_element :track, code_generator: code_generator
113
+ define_void_element :wbr, code_generator: code_generator
114
+
115
+ define_self_closing_element :animate, code_generator: code_generator
116
+ define_self_closing_element :animateMotion, code_generator: code_generator, method_name: :animate_motion
117
+ define_self_closing_element :animateTransform, code_generator: code_generator, method_name: :animate_transform
118
+ define_self_closing_element :circle, code_generator: code_generator
119
+ define_self_closing_element :ellipse, code_generator: code_generator
120
+ define_self_closing_element :line, code_generator: code_generator
121
+ define_self_closing_element :path, code_generator: code_generator
122
+ define_self_closing_element :polygon, code_generator: code_generator
123
+ define_self_closing_element :polyline, code_generator: code_generator
124
+ define_self_closing_element :rect, code_generator: code_generator
125
+ define_self_closing_element :set, code_generator: code_generator
126
+ define_self_closing_element :stop, code_generator: code_generator
127
+ define_self_closing_element :use, code_generator: code_generator
128
+ define_self_closing_element :view, code_generator: code_generator
129
+
130
+ define_element :a, code_generator: code_generator
131
+ define_element :abbr, code_generator: code_generator
132
+ define_element :address, code_generator: code_generator
133
+ define_element :article, code_generator: code_generator
134
+ define_element :aside, code_generator: code_generator
135
+ define_element :audio, code_generator: code_generator
136
+ define_element :b, code_generator: code_generator
137
+ define_element :bdi, code_generator: code_generator
138
+ define_element :bdo, code_generator: code_generator
139
+ define_element :blockquote, code_generator: code_generator
140
+ define_element :body, code_generator: code_generator
141
+ define_element :button, code_generator: code_generator
142
+ define_element :canvas, code_generator: code_generator
143
+ define_element :caption, code_generator: code_generator
144
+ define_element :cite, code_generator: code_generator
145
+ define_element :code, code_generator: code_generator
146
+ define_element :colgroup, code_generator: code_generator
147
+ define_element :data, code_generator: code_generator
148
+ define_element :datalist, code_generator: code_generator
149
+ define_element :dd, code_generator: code_generator
150
+ define_element :del, code_generator: code_generator
151
+ define_element :details, code_generator: code_generator
152
+ define_element :dfn, code_generator: code_generator
153
+ define_element :dialog, code_generator: code_generator
154
+ define_element :div, code_generator: code_generator
155
+ define_element :dl, code_generator: code_generator
156
+ define_element :dt, code_generator: code_generator
157
+ define_element :em, code_generator: code_generator
158
+ define_element :fieldset, code_generator: code_generator
159
+ define_element :figcaption, code_generator: code_generator
160
+ define_element :figure, code_generator: code_generator
161
+ define_element :footer, code_generator: code_generator
162
+ define_element :form, code_generator: code_generator
163
+ define_element :h1, code_generator: code_generator
164
+ define_element :h2, code_generator: code_generator
165
+ define_element :h3, code_generator: code_generator
166
+ define_element :h4, code_generator: code_generator
167
+ define_element :h5, code_generator: code_generator
168
+ define_element :h6, code_generator: code_generator
169
+ define_element :head, code_generator: code_generator
170
+ define_element :header, code_generator: code_generator
171
+ define_element :hgroup, code_generator: code_generator
172
+ define_element :html, code_generator: code_generator
173
+ define_element :i, code_generator: code_generator
174
+ define_element :iframe, code_generator: code_generator
175
+ define_element :ins, code_generator: code_generator
176
+ define_element :kbd, code_generator: code_generator
177
+ define_element :label, code_generator: code_generator
178
+ define_element :legend, code_generator: code_generator
179
+ define_element :li, code_generator: code_generator
180
+ define_element :main, code_generator: code_generator
181
+ define_element :map, code_generator: code_generator
182
+ define_element :mark, code_generator: code_generator
183
+ define_element :menu, code_generator: code_generator
184
+ define_element :meter, code_generator: code_generator
185
+ define_element :nav, code_generator: code_generator
186
+ define_element :noscript, code_generator: code_generator
187
+ define_element :object, code_generator: code_generator
188
+ define_element :ol, code_generator: code_generator
189
+ define_element :optgroup, code_generator: code_generator
190
+ define_element :option, code_generator: code_generator
191
+ define_element :output, code_generator: code_generator
192
+ define_element :p, code_generator: code_generator
193
+ define_element :picture, code_generator: code_generator
194
+ define_element :portal, code_generator: code_generator
195
+ define_element :pre, code_generator: code_generator
196
+ define_element :progress, code_generator: code_generator
197
+ define_element :q, code_generator: code_generator
198
+ define_element :rp, code_generator: code_generator
199
+ define_element :rt, code_generator: code_generator
200
+ define_element :ruby, code_generator: code_generator
201
+ define_element :s, code_generator: code_generator
202
+ define_element :samp, code_generator: code_generator
203
+ define_element :script, code_generator: code_generator
204
+ define_element :search, code_generator: code_generator
205
+ define_element :section, code_generator: code_generator
206
+ define_element :select, code_generator: code_generator
207
+ define_element :slot, code_generator: code_generator
208
+ define_element :small, code_generator: code_generator
209
+ define_element :span, code_generator: code_generator
210
+ define_element :strong, code_generator: code_generator
211
+ define_element :style, code_generator: code_generator
212
+ define_element :sub, code_generator: code_generator
213
+ define_element :summary, code_generator: code_generator
214
+ define_element :sup, code_generator: code_generator
215
+ define_element :table, code_generator: code_generator
216
+ define_element :tbody, code_generator: code_generator
217
+ define_element :td, code_generator: code_generator
218
+ define_element :template, code_generator: code_generator
219
+ define_element :textarea, code_generator: code_generator
220
+ define_element :tfoot, code_generator: code_generator
221
+ define_element :th, code_generator: code_generator
222
+ define_element :thead, code_generator: code_generator
223
+ define_element :time, code_generator: code_generator
224
+ define_element :title, code_generator: code_generator
225
+ define_element :tr, code_generator: code_generator
226
+ define_element :u, code_generator: code_generator
227
+ define_element :ul, code_generator: code_generator
228
+ define_element :var, code_generator: code_generator
229
+ define_element :video, code_generator: code_generator
230
+ end
231
+
232
+ def initialize(view_context)
233
+ @view_context = view_context
234
+ end
235
+
236
+ # Transforms a Hash into HTML Attributes, ready to be interpolated into
237
+ # ERB.
238
+ #
239
+ # <input <%= tag.attributes(type: :text, aria: { label: "Search" }) %> >
240
+ # # => <input type="text" aria-label="Search">
241
+ def attributes(attributes)
242
+ tag_options(attributes.to_h).to_s.strip.html_safe
243
+ end
244
+
245
+ def tag_string(name, content = nil, options, escape: true, &block)
246
+ content = @view_context.capture(self, &block) if block
247
+
248
+ content_tag_string(name, content, options, escape)
249
+ end
250
+
251
+ def self_closing_tag_string(name, options, escape = true, tag_suffix = " />")
252
+ "<#{name}#{tag_options(options, escape)}#{tag_suffix}".html_safe
253
+ end
254
+
255
+ def content_tag_string(name, content, options, escape = true)
256
+ tag_options = tag_options(options, escape) if options
257
+
258
+ if escape && content.present?
259
+ content = ERB::Util.unwrapped_html_escape(content)
260
+ end
261
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
262
+ end
263
+
264
+ def tag_options(options, escape = true)
265
+ return if options.blank?
266
+ output = +""
267
+ sep = " "
268
+ options.each_pair do |key, value|
269
+ type = TAG_TYPES[key]
270
+ if type == :data && value.is_a?(Hash)
271
+ value.each_pair do |k, v|
272
+ next if v.nil?
273
+ output << sep
274
+ output << prefix_tag_option(key, k, v, escape)
275
+ end
276
+ elsif type == :aria && value.is_a?(Hash)
277
+ value.each_pair do |k, v|
278
+ next if v.nil?
279
+
280
+ case v
281
+ when Array, Hash
282
+ tokens = TagHelper.build_tag_values(v)
283
+ next if tokens.none?
284
+
285
+ v = safe_join(tokens, " ")
286
+ else
287
+ v = v.to_s
288
+ end
289
+
290
+ output << sep
291
+ output << prefix_tag_option(key, k, v, escape)
292
+ end
293
+ elsif type == :boolean
294
+ if value
295
+ output << sep
296
+ output << boolean_tag_option(key)
297
+ end
298
+ elsif !value.nil?
299
+ output << sep
300
+ output << tag_option(key, value, escape)
301
+ end
302
+ end
303
+ output unless output.empty?
304
+ end
305
+
306
+ def boolean_tag_option(key)
307
+ %(#{key}="#{key}")
308
+ end
309
+
310
+ def tag_option(key, value, escape)
311
+ key = ERB::Util.xml_name_escape(key) if escape
312
+
313
+ case value
314
+ when Array, Hash
315
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
316
+ value = escape ? safe_join(value, " ") : value.join(" ")
317
+ when Regexp
318
+ value = escape ? ERB::Util.unwrapped_html_escape(value.source) : value.source
319
+ else
320
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
321
+ end
322
+ value = value.gsub('"', "&quot;") if value.include?('"')
323
+
324
+ %(#{key}="#{value}")
325
+ end
326
+
327
+ private
328
+ def prefix_tag_option(prefix, key, value, escape)
329
+ key = "#{prefix}-#{key.to_s.dasherize}"
330
+ unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
331
+ value = value.to_json
332
+ end
333
+ tag_option(key, value, escape)
334
+ end
335
+
336
+ def respond_to_missing?(*args)
337
+ true
338
+ end
339
+
340
+ def method_missing(called, *args, escape: true, **options, &block)
341
+ name = called.name.dasherize
342
+
343
+ TagHelper.ensure_valid_html5_tag_name(name)
344
+
345
+ tag_string(name, *args, options, escape: escape, &block)
346
+ end
347
+ end
348
+
349
+ # Returns an HTML tag.
350
+ #
351
+ # === Building HTML tags
352
+ #
353
+ # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with:
354
+ #
355
+ # tag.<tag name>(optional content, options)
356
+ #
357
+ # where tag name can be e.g. br, div, section, article, or any tag really.
358
+ #
359
+ # ==== Passing content
360
+ #
361
+ # Tags can pass content to embed within it:
362
+ #
363
+ # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1>
364
+ #
365
+ # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div>
366
+ #
367
+ # Content can also be captured with a block, which is useful in templates:
368
+ #
369
+ # <%= tag.p do %>
370
+ # The next great American novel starts here.
371
+ # <% end %>
372
+ # # => <p>The next great American novel starts here.</p>
373
+ #
374
+ # ==== Options
375
+ #
376
+ # Use symbol keyed options to add attributes to the generated tag.
377
+ #
378
+ # tag.section class: %w( kitties puppies )
379
+ # # => <section class="kitties puppies"></section>
380
+ #
381
+ # tag.section id: dom_id(@post)
382
+ # # => <section id="<generated dom id>"></section>
383
+ #
384
+ # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+.
385
+ #
386
+ # tag.input type: 'text', disabled: true
387
+ # # => <input type="text" disabled="disabled">
388
+ #
389
+ # HTML5 <tt>data-*</tt> and <tt>aria-*</tt> attributes can be set with a
390
+ # single +data+ or +aria+ key pointing to a hash of sub-attributes.
391
+ #
392
+ # To play nicely with JavaScript conventions, sub-attributes are dasherized.
393
+ #
394
+ # tag.article data: { user_id: 123 }
395
+ # # => <article data-user-id="123"></article>
396
+ #
397
+ # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>.
398
+ #
399
+ # Data attribute values are encoded to JSON, with the exception of strings, symbols, and
400
+ # BigDecimals.
401
+ # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
402
+ # from 1.4.3.
403
+ #
404
+ # tag.div data: { city_state: %w( Chicago IL ) }
405
+ # # => <div data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
406
+ #
407
+ # The generated tag names and attributes are escaped by default. This can be disabled using
408
+ # +escape+.
409
+ #
410
+ # tag.img src: 'open & shut.png'
411
+ # # => <img src="open &amp; shut.png">
412
+ #
413
+ # tag.img src: 'open & shut.png', escape: false
414
+ # # => <img src="open & shut.png">
415
+ #
416
+ # The tag builder respects
417
+ # {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements]
418
+ # if no content is passed, and omits closing tags for those elements.
419
+ #
420
+ # # A standard element:
421
+ # tag.div # => <div></div>
422
+ #
423
+ # # A void element:
424
+ # tag.br # => <br>
425
+ #
426
+ # Note that when using the block form options should be wrapped in
427
+ # parenthesis.
428
+ #
429
+ # <%= tag.a(href: "/about", class: "font-bold") do %>
430
+ # About the author
431
+ # <% end %>
432
+ # # => <a href="/about" class="font-bold">About the author</a>
433
+ #
434
+ # === Building HTML attributes
435
+ #
436
+ # Transforms a Hash into HTML attributes, ready to be interpolated into
437
+ # ERB. Includes or omits boolean attributes based on their truthiness.
438
+ # Transforms keys nested within
439
+ # <tt>aria:</tt> or <tt>data:</tt> objects into <tt>aria-</tt> and <tt>data-</tt>
440
+ # prefixed attributes:
441
+ #
442
+ # <input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
443
+ # # => <input type="text" aria-label="Search">
444
+ #
445
+ # <button <%= tag.attributes id: "call-to-action", disabled: false, aria: { expanded: false } %> class="primary">Get Started!</button>
446
+ # # => <button id="call-to-action" aria-expanded="false" class="primary">Get Started!</button>
447
+ #
448
+ # === Legacy syntax
449
+ #
450
+ # The following format is for legacy syntax support. It will be deprecated in future versions of \Rails.
451
+ #
452
+ # tag(name, options = nil, open = false, escape = true)
453
+ #
454
+ # It returns an empty HTML tag of type +name+ which by default is XHTML
455
+ # compliant. Set +open+ to true to create an open tag compatible
456
+ # with HTML 4.0 and below. Add HTML attributes by passing an attributes
457
+ # hash to +options+. Set +escape+ to false to disable attribute value
458
+ # escaping.
459
+ #
460
+ # ==== Options
461
+ #
462
+ # You can use symbols or strings for the attribute names.
463
+ #
464
+ # Use +true+ with boolean attributes that can render with no value, like
465
+ # +disabled+ and +readonly+.
466
+ #
467
+ # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
468
+ # pointing to a hash of sub-attributes.
469
+ #
470
+ # ==== Examples
471
+ #
472
+ # tag("br")
473
+ # # => <br />
474
+ #
475
+ # tag("br", nil, true)
476
+ # # => <br>
477
+ #
478
+ # tag("input", type: 'text', disabled: true)
479
+ # # => <input type="text" disabled="disabled" />
480
+ #
481
+ # tag("input", type: 'text', class: ["strong", "highlight"])
482
+ # # => <input class="strong highlight" type="text" />
483
+ #
484
+ # tag("img", src: "open & shut.png")
485
+ # # => <img src="open &amp; shut.png" />
486
+ #
487
+ # tag("img", { src: "open &amp; shut.png" }, false, false)
488
+ # # => <img src="open &amp; shut.png" />
489
+ #
490
+ # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
491
+ # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
492
+ #
493
+ # tag("div", class: { highlight: current_user.admin? })
494
+ # # => <div class="highlight" />
495
+ def tag(name = nil, options = nil, open = false, escape = true)
496
+ if name.nil?
497
+ tag_builder
498
+ else
499
+ ensure_valid_html5_tag_name(name)
500
+ "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
501
+ end
502
+ end
503
+
504
+ # Returns an HTML block tag of type +name+ surrounding the +content+. Add
505
+ # HTML attributes by passing an attributes hash to +options+.
506
+ # Instead of passing the content as an argument, you can also use a block
507
+ # in which case, you pass your +options+ as the second parameter.
508
+ # Set escape to false to disable escaping.
509
+ # Note: this is legacy syntax, see +tag+ method description for details.
510
+ #
511
+ # ==== Options
512
+ # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
513
+ # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
514
+ # symbols or strings for the attribute names.
515
+ #
516
+ # ==== Examples
517
+ # content_tag(:p, "Hello world!")
518
+ # # => <p>Hello world!</p>
519
+ # content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
520
+ # # => <div class="strong"><p>Hello world!</p></div>
521
+ # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
522
+ # # => <div class="strong highlight">Hello world!</div>
523
+ # content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
524
+ # # => <div class="strong highlight">Hello world!</div>
525
+ # content_tag("select", options, multiple: true)
526
+ # # => <select multiple="multiple">...options...</select>
527
+ #
528
+ # <%= content_tag :div, class: "strong" do -%>
529
+ # Hello world!
530
+ # <% end -%>
531
+ # # => <div class="strong">Hello world!</div>
532
+ def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
533
+ ensure_valid_html5_tag_name(name)
534
+
535
+ if block_given?
536
+ options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
537
+ tag_builder.content_tag_string(name, capture(&block), options, escape)
538
+ else
539
+ tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
540
+ end
541
+ end
542
+
543
+ # Returns a string of tokens built from +args+.
544
+ #
545
+ # ==== Examples
546
+ # token_list("foo", "bar")
547
+ # # => "foo bar"
548
+ # token_list("foo", "foo bar")
549
+ # # => "foo bar"
550
+ # token_list({ foo: true, bar: false })
551
+ # # => "foo"
552
+ # token_list(nil, false, 123, "", "foo", { bar: true })
553
+ # # => "123 foo bar"
554
+ def token_list(*args)
555
+ tokens = build_tag_values(*args).flat_map { |value| CGI.unescape_html(value.to_s).split(/\s+/) }.uniq
556
+
557
+ safe_join(tokens, " ")
558
+ end
559
+ alias_method :class_names, :token_list
560
+
561
+ # Returns a CDATA section with the given +content+. CDATA sections
562
+ # are used to escape blocks of text containing characters which would
563
+ # otherwise be recognized as markup. CDATA sections begin with the string
564
+ # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
565
+ #
566
+ # cdata_section("<hello world>")
567
+ # # => <![CDATA[<hello world>]]>
568
+ #
569
+ # cdata_section(File.read("hello_world.txt"))
570
+ # # => <![CDATA[<hello from a text file]]>
571
+ #
572
+ # cdata_section("hello]]>world")
573
+ # # => <![CDATA[hello]]]]><![CDATA[>world]]>
574
+ def cdata_section(content)
575
+ splitted = content.to_s.gsub(/\]\]>/, "]]]]><![CDATA[>")
576
+ "<![CDATA[#{splitted}]]>".html_safe
577
+ end
578
+
579
+ # Returns an escaped version of +html+ without affecting existing escaped entities.
580
+ #
581
+ # escape_once("1 < 2 &amp; 3")
582
+ # # => "1 &lt; 2 &amp; 3"
583
+ #
584
+ # escape_once("&lt;&lt; Accept & Checkout")
585
+ # # => "&lt;&lt; Accept &amp; Checkout"
586
+ def escape_once(html)
587
+ ERB::Util.html_escape_once(html)
588
+ end
589
+
590
+ private
591
+ def ensure_valid_html5_tag_name(name)
592
+ raise ArgumentError, "Invalid HTML5 tag name: #{name.inspect}" unless /\A[a-zA-Z][^\s\/>]*\z/.match?(name)
593
+ end
594
+ module_function :ensure_valid_html5_tag_name
595
+
596
+ def build_tag_values(*args)
597
+ tag_values = []
598
+
599
+ args.each do |tag_value|
600
+ case tag_value
601
+ when Hash
602
+ tag_value.each do |key, val|
603
+ tag_values << key.to_s if val && key.present?
604
+ end
605
+ when Array
606
+ tag_values.concat build_tag_values(*tag_value)
607
+ else
608
+ tag_values << tag_value.to_s if tag_value.present?
609
+ end
610
+ end
611
+
612
+ tag_values
613
+ end
614
+ module_function :build_tag_values
615
+
616
+ def tag_builder
617
+ @tag_builder ||= TagBuilder.new(self)
618
+ end
619
+ end
620
+ end
621
+ end