omg-actionview 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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