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,680 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract_options"
4
+ require "active_support/core_ext/hash/keys"
5
+ require "active_support/core_ext/object/inclusion"
6
+ require "action_view/helpers/asset_url_helper"
7
+ require "action_view/helpers/tag_helper"
8
+
9
+ module ActionView
10
+ module Helpers # :nodoc:
11
+ # = Action View Asset Tag \Helpers
12
+ #
13
+ # This module provides methods for generating HTML that links views to assets such
14
+ # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
15
+ # the assets exist before linking to them:
16
+ #
17
+ # image_tag("rails.png")
18
+ # # => <img src="/assets/rails.png" />
19
+ # stylesheet_link_tag("application")
20
+ # # => <link href="/assets/application.css?body=1" rel="stylesheet" />
21
+ module AssetTagHelper
22
+ include AssetUrlHelper
23
+ include TagHelper
24
+
25
+ mattr_accessor :image_loading
26
+ mattr_accessor :image_decoding
27
+ mattr_accessor :preload_links_header
28
+ mattr_accessor :apply_stylesheet_media_default
29
+
30
+ # Returns an HTML script tag for each of the +sources+ provided.
31
+ #
32
+ # Sources may be paths to JavaScript files. Relative paths are assumed to be relative
33
+ # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
34
+ # root. Relative paths are idiomatic, use absolute paths only when needed.
35
+ #
36
+ # When passing paths, the ".js" extension is optional. If you do not want ".js"
37
+ # appended to the path <tt>extname: false</tt> can be set on the options.
38
+ #
39
+ # You can modify the HTML attributes of the script tag by passing a hash as the
40
+ # last argument.
41
+ #
42
+ # When the Asset Pipeline is enabled, you can pass the name of your manifest as
43
+ # source, and include other JavaScript or CoffeeScript files inside the manifest.
44
+ #
45
+ # If the server supports HTTP Early Hints, and the +defer+ option is not
46
+ # enabled, \Rails will push a <tt>103 Early Hints</tt> response that links
47
+ # to the assets.
48
+ #
49
+ # ==== Options
50
+ #
51
+ # When the last parameter is a hash you can add HTML attributes using that
52
+ # parameter. This includes but is not limited to the following options:
53
+ #
54
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
55
+ # already exists. This only applies for relative URLs.
56
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
57
+ # applies when a relative URL and +host+ options are provided.
58
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
59
+ # that path.
60
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
61
+ # when it is set to true.
62
+ # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
63
+ # you have Content Security Policy enabled.
64
+ # * <tt>:async</tt> - When set to +true+, adds the +async+ HTML
65
+ # attribute, allowing the script to be fetched in parallel to be parsed
66
+ # and evaluated as soon as possible.
67
+ # * <tt>:defer</tt> - When set to +true+, adds the +defer+ HTML
68
+ # attribute, which indicates to the browser that the script is meant to
69
+ # be executed after the document has been parsed. Additionally, prevents
70
+ # sending the Preload Links header.
71
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired
72
+ # for the script. Defaults to +true+.
73
+ #
74
+ # Any other specified options will be treated as HTML attributes for the
75
+ # +script+ tag.
76
+ #
77
+ # For more information regarding how the <tt>:async</tt> and <tt>:defer</tt>
78
+ # options affect the <tt><script></tt> tag, please refer to the
79
+ # {MDN docs}[https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script].
80
+ #
81
+ # ==== Examples
82
+ #
83
+ # javascript_include_tag "xmlhr"
84
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
85
+ #
86
+ # javascript_include_tag "xmlhr", host: "localhost", protocol: "https"
87
+ # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
88
+ #
89
+ # javascript_include_tag "template.jst", extname: false
90
+ # # => <script src="/assets/template.debug-1284139606.jst"></script>
91
+ #
92
+ # javascript_include_tag "xmlhr.js"
93
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
94
+ #
95
+ # javascript_include_tag "common.javascript", "/elsewhere/cools"
96
+ # # => <script src="/assets/common.javascript.debug-1284139606.js"></script>
97
+ # # <script src="/elsewhere/cools.debug-1284139606.js"></script>
98
+ #
99
+ # javascript_include_tag "http://www.example.com/xmlhr"
100
+ # # => <script src="http://www.example.com/xmlhr"></script>
101
+ #
102
+ # javascript_include_tag "http://www.example.com/xmlhr.js"
103
+ # # => <script src="http://www.example.com/xmlhr.js"></script>
104
+ #
105
+ # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
106
+ # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
107
+ #
108
+ # javascript_include_tag "http://www.example.com/xmlhr.js", async: true
109
+ # # => <script src="http://www.example.com/xmlhr.js" async="async"></script>
110
+ #
111
+ # javascript_include_tag "http://www.example.com/xmlhr.js", defer: true
112
+ # # => <script src="http://www.example.com/xmlhr.js" defer="defer"></script>
113
+ def javascript_include_tag(*sources)
114
+ options = sources.extract_options!.stringify_keys
115
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
116
+ preload_links = []
117
+ use_preload_links_header = options["preload_links_header"].nil? ? preload_links_header : options.delete("preload_links_header")
118
+ nopush = options["nopush"].nil? ? true : options.delete("nopush")
119
+ crossorigin = options.delete("crossorigin")
120
+ crossorigin = "anonymous" if crossorigin == true
121
+ integrity = options["integrity"]
122
+ rel = options["type"] == "module" ? "modulepreload" : "preload"
123
+
124
+ sources_tags = sources.uniq.map { |source|
125
+ href = path_to_javascript(source, path_options)
126
+ if use_preload_links_header && !options["defer"] && href.present? && !href.start_with?("data:")
127
+ preload_link = "<#{href}>; rel=#{rel}; as=script"
128
+ preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
129
+ preload_link += "; integrity=#{integrity}" unless integrity.nil?
130
+ preload_link += "; nopush" if nopush
131
+ preload_links << preload_link
132
+ end
133
+ tag_options = {
134
+ "src" => href,
135
+ "crossorigin" => crossorigin
136
+ }.merge!(options)
137
+ if tag_options["nonce"] == true
138
+ tag_options["nonce"] = content_security_policy_nonce
139
+ end
140
+ content_tag("script", "", tag_options)
141
+ }.join("\n").html_safe
142
+
143
+ if use_preload_links_header
144
+ send_preload_links_header(preload_links)
145
+ end
146
+
147
+ sources_tags
148
+ end
149
+
150
+ # Returns a stylesheet link tag for the sources specified as arguments.
151
+ #
152
+ # When passing paths, the <tt>.css</tt> extension is optional.
153
+ # If you don't specify an extension, <tt>.css</tt> will be appended automatically.
154
+ # If you do not want <tt>.css</tt> appended to the path,
155
+ # set <tt>extname: false</tt> in the options.
156
+ # You can modify the link attributes by passing a hash as the last argument.
157
+ #
158
+ # If the server supports HTTP Early Hints, \Rails will push a <tt>103 Early
159
+ # Hints</tt> response that links to the assets.
160
+ #
161
+ # ==== Options
162
+ #
163
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
164
+ # already exists. This only applies for relative URLs.
165
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
166
+ # applies when a relative URL and +host+ options are provided.
167
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
168
+ # that path.
169
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
170
+ # when it is set to true.
171
+ # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
172
+ # you have Content Security Policy enabled.
173
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired
174
+ # for the stylesheet. Defaults to +true+.
175
+ #
176
+ # ==== Examples
177
+ #
178
+ # stylesheet_link_tag "style"
179
+ # # => <link href="/assets/style.css" rel="stylesheet" />
180
+ #
181
+ # stylesheet_link_tag "style.css"
182
+ # # => <link href="/assets/style.css" rel="stylesheet" />
183
+ #
184
+ # stylesheet_link_tag "http://www.example.com/style.css"
185
+ # # => <link href="http://www.example.com/style.css" rel="stylesheet" />
186
+ #
187
+ # stylesheet_link_tag "style.less", extname: false, skip_pipeline: true, rel: "stylesheet/less"
188
+ # # => <link href="/stylesheets/style.less" rel="stylesheet/less">
189
+ #
190
+ # stylesheet_link_tag "style", media: "all"
191
+ # # => <link href="/assets/style.css" media="all" rel="stylesheet" />
192
+ #
193
+ # stylesheet_link_tag "style", media: "print"
194
+ # # => <link href="/assets/style.css" media="print" rel="stylesheet" />
195
+ #
196
+ # stylesheet_link_tag "random.styles", "/css/stylish"
197
+ # # => <link href="/assets/random.styles" rel="stylesheet" />
198
+ # # <link href="/css/stylish.css" rel="stylesheet" />
199
+ #
200
+ # stylesheet_link_tag "style", nonce: true
201
+ # # => <link href="/assets/style.css" rel="stylesheet" nonce="..." />
202
+ def stylesheet_link_tag(*sources)
203
+ options = sources.extract_options!.stringify_keys
204
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
205
+ use_preload_links_header = options["preload_links_header"].nil? ? preload_links_header : options.delete("preload_links_header")
206
+ preload_links = []
207
+ crossorigin = options.delete("crossorigin")
208
+ crossorigin = "anonymous" if crossorigin == true
209
+ nopush = options["nopush"].nil? ? true : options.delete("nopush")
210
+ integrity = options["integrity"]
211
+
212
+ sources_tags = sources.uniq.map { |source|
213
+ href = path_to_stylesheet(source, path_options)
214
+ if use_preload_links_header && href.present? && !href.start_with?("data:")
215
+ preload_link = "<#{href}>; rel=preload; as=style"
216
+ preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
217
+ preload_link += "; integrity=#{integrity}" unless integrity.nil?
218
+ preload_link += "; nopush" if nopush
219
+ preload_links << preload_link
220
+ end
221
+ tag_options = {
222
+ "rel" => "stylesheet",
223
+ "crossorigin" => crossorigin,
224
+ "href" => href
225
+ }.merge!(options)
226
+ if tag_options["nonce"] == true
227
+ tag_options["nonce"] = content_security_policy_nonce
228
+ end
229
+
230
+ if apply_stylesheet_media_default && tag_options["media"].blank?
231
+ tag_options["media"] = "screen"
232
+ end
233
+
234
+ tag(:link, tag_options)
235
+ }.join("\n").html_safe
236
+
237
+ if use_preload_links_header
238
+ send_preload_links_header(preload_links)
239
+ end
240
+
241
+ sources_tags
242
+ end
243
+
244
+ # Returns a link tag that browsers and feed readers can use to auto-detect
245
+ # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
246
+ # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
247
+ # using the +url_options+. You can modify the LINK tag itself in +tag_options+.
248
+ #
249
+ # ==== Options
250
+ #
251
+ # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
252
+ # * <tt>:type</tt> - Override the auto-generated mime type
253
+ # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
254
+ #
255
+ # ==== Examples
256
+ #
257
+ # auto_discovery_link_tag
258
+ # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
259
+ # auto_discovery_link_tag(:atom)
260
+ # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
261
+ # auto_discovery_link_tag(:json)
262
+ # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
263
+ # auto_discovery_link_tag(:rss, {action: "feed"})
264
+ # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
265
+ # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
266
+ # # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" />
267
+ # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
268
+ # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
269
+ # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
270
+ # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
271
+ def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
272
+ if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
273
+ raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
274
+ end
275
+
276
+ tag(
277
+ "link",
278
+ "rel" => tag_options[:rel] || "alternate",
279
+ "type" => tag_options[:type] || Template::Types[type].to_s,
280
+ "title" => tag_options[:title] || type.to_s.upcase,
281
+ "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options
282
+ )
283
+ end
284
+
285
+ # Returns a link tag for a favicon managed by the asset pipeline.
286
+ #
287
+ # If a page has no link like the one generated by this helper, browsers
288
+ # ask for <tt>/favicon.ico</tt> automatically, and cache the file if the
289
+ # request succeeds. If the favicon changes it is hard to get it updated.
290
+ #
291
+ # To have better control applications may let the asset pipeline manage
292
+ # their favicon storing the file under <tt>app/assets/images</tt>, and
293
+ # using this helper to generate its corresponding link tag.
294
+ #
295
+ # The helper gets the name of the favicon file as first argument, which
296
+ # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
297
+ # to override their defaults, "icon" and "image/x-icon"
298
+ # respectively:
299
+ #
300
+ # favicon_link_tag
301
+ # # => <link href="/assets/favicon.ico" rel="icon" type="image/x-icon" />
302
+ #
303
+ # favicon_link_tag 'myicon.ico'
304
+ # # => <link href="/assets/myicon.ico" rel="icon" type="image/x-icon" />
305
+ #
306
+ # Mobile Safari looks for a different link tag, pointing to an image that
307
+ # will be used if you add the page to the home screen of an iOS device.
308
+ # The following call would generate such a tag:
309
+ #
310
+ # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
311
+ # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
312
+ def favicon_link_tag(source = "favicon.ico", options = {})
313
+ tag("link", {
314
+ rel: "icon",
315
+ type: "image/x-icon",
316
+ href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
317
+ }.merge!(options.symbolize_keys))
318
+ end
319
+
320
+ # Returns a link tag that browsers can use to preload the +source+.
321
+ # The +source+ can be the path of a resource managed by asset pipeline,
322
+ # a full path, or an URI.
323
+ #
324
+ # ==== Options
325
+ #
326
+ # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
327
+ # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
328
+ # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
329
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
330
+ # * <tt>:integrity</tt> - Specify the integrity attribute.
331
+ #
332
+ # ==== Examples
333
+ #
334
+ # preload_link_tag("custom_theme.css")
335
+ # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
336
+ #
337
+ # preload_link_tag("/videos/video.webm")
338
+ # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
339
+ #
340
+ # preload_link_tag(post_path(format: :json), as: "fetch")
341
+ # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
342
+ #
343
+ # preload_link_tag("worker.js", as: "worker")
344
+ # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
345
+ #
346
+ # preload_link_tag("//example.com/font.woff2")
347
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
348
+ #
349
+ # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
350
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
351
+ #
352
+ # preload_link_tag("/media/audio.ogg", nopush: true)
353
+ # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
354
+ #
355
+ def preload_link_tag(source, options = {})
356
+ href = path_to_asset(source, skip_pipeline: options.delete(:skip_pipeline))
357
+ extname = File.extname(source).downcase.delete(".")
358
+ mime_type = options.delete(:type) || Template::Types[extname]&.to_s
359
+ as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
360
+ crossorigin = options.delete(:crossorigin)
361
+ crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
362
+ integrity = options[:integrity]
363
+ nopush = options.delete(:nopush) || false
364
+ rel = mime_type == "module" ? "modulepreload" : "preload"
365
+
366
+ link_tag = tag.link(
367
+ rel: rel,
368
+ href: href,
369
+ as: as_type,
370
+ type: mime_type,
371
+ crossorigin: crossorigin,
372
+ **options.symbolize_keys)
373
+
374
+ preload_link = "<#{href}>; rel=#{rel}; as=#{as_type}"
375
+ preload_link += "; type=#{mime_type}" if mime_type
376
+ preload_link += "; crossorigin=#{crossorigin}" if crossorigin
377
+ preload_link += "; integrity=#{integrity}" if integrity
378
+ preload_link += "; nopush" if nopush
379
+
380
+ send_preload_links_header([preload_link])
381
+
382
+ link_tag
383
+ end
384
+
385
+ # Returns an HTML image tag for the +source+. The +source+ can be a full
386
+ # path, a file, or an Active Storage attachment.
387
+ #
388
+ # ==== Options
389
+ #
390
+ # You can add HTML attributes using the +options+. The +options+ supports
391
+ # additional keys for convenience and conformance:
392
+ #
393
+ # * <tt>:size</tt> - Supplied as <tt>"#{width}x#{height}"</tt> or <tt>"#{number}"</tt>, so <tt>"30x45"</tt> becomes
394
+ # <tt>width="30" height="45"</tt>, and <tt>"50"</tt> becomes <tt>width="50" height="50"</tt>.
395
+ # <tt>:size</tt> will be ignored if the value is not in the correct format.
396
+ # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
397
+ # pairs, each image path will be expanded before the list is formatted as a string.
398
+ #
399
+ # ==== Examples
400
+ #
401
+ # Assets (images that are part of your app):
402
+ #
403
+ # image_tag("icon")
404
+ # # => <img src="/assets/icon" />
405
+ # image_tag("icon.png")
406
+ # # => <img src="/assets/icon.png" />
407
+ # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
408
+ # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
409
+ # image_tag("/icons/icon.gif", size: "16")
410
+ # # => <img src="/icons/icon.gif" width="16" height="16" />
411
+ # image_tag("/icons/icon.gif", height: '32', width: '32')
412
+ # # => <img height="32" src="/icons/icon.gif" width="32" />
413
+ # image_tag("/icons/icon.gif", class: "menu_icon")
414
+ # # => <img class="menu_icon" src="/icons/icon.gif" />
415
+ # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
416
+ # # => <img data-title="Rails Application" src="/icons/icon.gif" />
417
+ # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
418
+ # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
419
+ # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
420
+ # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
421
+ #
422
+ # Active Storage blobs (images that are uploaded by the users of your app):
423
+ #
424
+ # image_tag(user.avatar)
425
+ # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
426
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
427
+ # # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
428
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
429
+ # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
430
+ def image_tag(source, options = {})
431
+ options = options.symbolize_keys
432
+ check_for_image_tag_errors(options)
433
+ skip_pipeline = options.delete(:skip_pipeline)
434
+
435
+ options[:src] = resolve_asset_source("image", source, skip_pipeline)
436
+
437
+ if options[:srcset] && !options[:srcset].is_a?(String)
438
+ options[:srcset] = options[:srcset].map do |src_path, size|
439
+ src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
440
+ "#{src_path} #{size}"
441
+ end.join(", ")
442
+ end
443
+
444
+ options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
445
+
446
+ options[:loading] ||= image_loading if image_loading
447
+ options[:decoding] ||= image_decoding if image_decoding
448
+
449
+ tag("img", options)
450
+ end
451
+
452
+ # Returns an HTML picture tag for the +sources+. If +sources+ is a string,
453
+ # a single picture tag will be returned. If +sources+ is an array, a picture
454
+ # tag with nested source tags for each source will be returned. The
455
+ # +sources+ can be full paths, files that exist in your public images
456
+ # directory, or Active Storage attachments. Since the picture tag requires
457
+ # an img tag, the last element you provide will be used for the img tag.
458
+ # For complete control over the picture tag, a block can be passed, which
459
+ # will populate the contents of the tag accordingly.
460
+ #
461
+ # ==== Options
462
+ #
463
+ # When the last parameter is a hash you can add HTML attributes using that
464
+ # parameter. Apart from all the HTML supported options, the following are supported:
465
+ #
466
+ # * <tt>:image</tt> - Hash of options that are passed directly to the +image_tag+ helper.
467
+ #
468
+ # ==== Examples
469
+ #
470
+ # picture_tag("picture.webp")
471
+ # # => <picture><img src="/images/picture.webp" /></picture>
472
+ # picture_tag("gold.png", :image => { :size => "20" })
473
+ # # => <picture><img height="20" src="/images/gold.png" width="20" /></picture>
474
+ # picture_tag("gold.png", :image => { :size => "45x70" })
475
+ # # => <picture><img height="70" src="/images/gold.png" width="45" /></picture>
476
+ # picture_tag("picture.webp", "picture.png")
477
+ # # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>
478
+ # picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })
479
+ # # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
480
+ # picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })
481
+ # # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
482
+ # picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }
483
+ # # => <picture class="my-class"><source srcset="/images/picture.webp" /><img alt="Image" src="/images/picture.png" /></picture>
484
+ # picture_tag { tag(:source, :srcset => image_path("picture-small.webp"), :media => "(min-width: 600px)") + tag(:source, :srcset => image_path("picture-big.webp")) + image_tag("picture.png", :alt => "Image") }
485
+ # # => <picture><source srcset="/images/picture-small.webp" media="(min-width: 600px)" /><source srcset="/images/picture-big.webp" /><img alt="Image" src="/images/picture.png" /></picture>
486
+ #
487
+ # Active Storage blobs (images that are uploaded by the users of your app):
488
+ #
489
+ # picture_tag(user.profile_picture)
490
+ # # => <picture><img src="/rails/active_storage/blobs/.../profile_picture.webp" /></picture>
491
+ def picture_tag(*sources, &block)
492
+ sources.flatten!
493
+ options = sources.extract_options!.symbolize_keys
494
+ image_options = options.delete(:image) || {}
495
+ skip_pipeline = options.delete(:skip_pipeline)
496
+
497
+ content_tag("picture", options) do
498
+ if block.present?
499
+ capture(&block).html_safe
500
+ elsif sources.size <= 1
501
+ image_tag(sources.last, image_options)
502
+ else
503
+ source_tags = sources.map do |source|
504
+ tag("source",
505
+ srcset: resolve_asset_source("image", source, skip_pipeline),
506
+ type: Template::Types[File.extname(source)[1..]]&.to_s)
507
+ end
508
+ safe_join(source_tags << image_tag(sources.last, image_options))
509
+ end
510
+ end
511
+ end
512
+
513
+ # Returns an HTML video tag for the +sources+. If +sources+ is a string,
514
+ # a single video tag will be returned. If +sources+ is an array, a video
515
+ # tag with nested source tags for each source will be returned. The
516
+ # +sources+ can be full paths, files that exist in your public videos
517
+ # directory, or Active Storage attachments.
518
+ #
519
+ # ==== Options
520
+ #
521
+ # When the last parameter is a hash you can add HTML attributes using that
522
+ # parameter. The following options are supported:
523
+ #
524
+ # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
525
+ # before the video loads. The path is calculated like the +src+ of +image_tag+.
526
+ # * <tt>:size</tt> - Supplied as <tt>"#{width}x#{height}"</tt> or <tt>"#{number}"</tt>, so <tt>"30x45"</tt> becomes
527
+ # <tt>width="30" height="45"</tt>, and <tt>"50"</tt> becomes <tt>width="50" height="50"</tt>.
528
+ # <tt>:size</tt> will be ignored if the value is not in the correct format.
529
+ # * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using
530
+ # the <tt>:poster</tt> option instead using an asset in the public folder.
531
+ #
532
+ # ==== Examples
533
+ #
534
+ # video_tag("trailer")
535
+ # # => <video src="/videos/trailer"></video>
536
+ # video_tag("trailer.ogg")
537
+ # # => <video src="/videos/trailer.ogg"></video>
538
+ # video_tag("trailer.ogg", controls: true, preload: 'none')
539
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
540
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
541
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
542
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
543
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video>
544
+ # video_tag("/trailers/hd.avi", size: "16x16")
545
+ # # => <video src="/trailers/hd.avi" width="16" height="16"></video>
546
+ # video_tag("/trailers/hd.avi", size: "16")
547
+ # # => <video height="16" src="/trailers/hd.avi" width="16"></video>
548
+ # video_tag("/trailers/hd.avi", height: '32', width: '32')
549
+ # # => <video height="32" src="/trailers/hd.avi" width="32"></video>
550
+ # video_tag("trailer.ogg", "trailer.flv")
551
+ # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
552
+ # video_tag(["trailer.ogg", "trailer.flv"])
553
+ # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
554
+ # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
555
+ # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
556
+ #
557
+ # Active Storage blobs (videos that are uploaded by the users of your app):
558
+ #
559
+ # video_tag(user.intro_video)
560
+ # # => <video src="/rails/active_storage/blobs/.../intro_video.mp4"></video>
561
+ def video_tag(*sources)
562
+ options = sources.extract_options!.symbolize_keys
563
+ public_poster_folder = options.delete(:poster_skip_pipeline)
564
+ sources << options
565
+ multiple_sources_tag_builder("video", sources) do |tag_options|
566
+ tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster]
567
+ tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size]
568
+ end
569
+ end
570
+
571
+ # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
572
+ # a single audio tag will be returned. If +sources+ is an array, an audio
573
+ # tag with nested source tags for each source will be returned. The
574
+ # +sources+ can be full paths, files that exist in your public audios
575
+ # directory, or Active Storage attachments.
576
+ #
577
+ # When the last parameter is a hash you can add HTML attributes using that
578
+ # parameter.
579
+ #
580
+ # audio_tag("sound")
581
+ # # => <audio src="/audios/sound"></audio>
582
+ # audio_tag("sound.wav")
583
+ # # => <audio src="/audios/sound.wav"></audio>
584
+ # audio_tag("sound.wav", autoplay: true, controls: true)
585
+ # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
586
+ # audio_tag("sound.wav", "sound.mid")
587
+ # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
588
+ #
589
+ # Active Storage blobs (audios that are uploaded by the users of your app):
590
+ #
591
+ # audio_tag(user.name_pronunciation_audio)
592
+ # # => <audio src="/rails/active_storage/blobs/.../name_pronunciation_audio.mp3"></audio>
593
+ def audio_tag(*sources)
594
+ multiple_sources_tag_builder("audio", sources)
595
+ end
596
+
597
+ private
598
+ def multiple_sources_tag_builder(type, sources)
599
+ options = sources.extract_options!.symbolize_keys
600
+ skip_pipeline = options.delete(:skip_pipeline)
601
+ sources.flatten!
602
+
603
+ yield options if block_given?
604
+
605
+ if sources.size > 1
606
+ content_tag(type, options) do
607
+ safe_join sources.map { |source| tag("source", src: resolve_asset_source(type, source, skip_pipeline)) }
608
+ end
609
+ else
610
+ options[:src] = resolve_asset_source(type, sources.first, skip_pipeline)
611
+ content_tag(type, nil, options)
612
+ end
613
+ end
614
+
615
+ def resolve_asset_source(asset_type, source, skip_pipeline)
616
+ if source.is_a?(Symbol) || source.is_a?(String)
617
+ path_to_asset(source, type: asset_type.to_sym, skip_pipeline: skip_pipeline)
618
+ else
619
+ polymorphic_url(source)
620
+ end
621
+ rescue NoMethodError => e
622
+ raise ArgumentError, "Can't resolve #{asset_type} into URL: #{e}"
623
+ end
624
+
625
+ def extract_dimensions(size)
626
+ size = size.to_s
627
+ if /\A\d+(?:\.\d+)?x\d+(?:\.\d+)?\z/.match?(size)
628
+ size.split("x")
629
+ elsif /\A\d+(?:\.\d+)?\z/.match?(size)
630
+ [size, size]
631
+ end
632
+ end
633
+
634
+ def check_for_image_tag_errors(options)
635
+ if options[:size] && (options[:height] || options[:width])
636
+ raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
637
+ end
638
+ end
639
+
640
+ def resolve_link_as(extname, mime_type)
641
+ case extname
642
+ when "js" then "script"
643
+ when "css" then "style"
644
+ when "vtt" then "track"
645
+ else
646
+ mime_type.to_s.split("/").first.presence_in(%w(audio video font image))
647
+ end
648
+ end
649
+
650
+ # Some HTTP client and proxies have a 4kiB header limit, but more importantly
651
+ # including preload links has diminishing returns so it's best to not go overboard
652
+ MAX_HEADER_SIZE = 1_000 # :nodoc:
653
+
654
+ def send_preload_links_header(preload_links, max_header_size: MAX_HEADER_SIZE)
655
+ return if preload_links.empty?
656
+ response_present = respond_to?(:response) && response
657
+ return if response_present && response.sending?
658
+
659
+ if respond_to?(:request) && request
660
+ request.send_early_hints("link" => preload_links.join(","))
661
+ end
662
+
663
+ if response_present
664
+ header = +response.headers["link"].to_s
665
+ preload_links.each do |link|
666
+ break if header.bytesize + link.bytesize > max_header_size
667
+
668
+ if header.empty?
669
+ header << link
670
+ else
671
+ header << "," << link
672
+ end
673
+ end
674
+
675
+ response.headers["link"] = header
676
+ end
677
+ end
678
+ end
679
+ end
680
+ end