actionview 4.2.11.1 → 7.0.2.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -215
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +116 -43
  6. data/lib/action_view/buffers.rb +20 -3
  7. data/lib/action_view/cache_expiry.rb +66 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  10. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  11. data/lib/action_view/dependency_tracker.rb +21 -122
  12. data/lib/action_view/digestor.rb +92 -85
  13. data/lib/action_view/flows.rb +15 -16
  14. data/lib/action_view/gem_version.rb +6 -4
  15. data/lib/action_view/helpers/active_model_helper.rb +17 -12
  16. data/lib/action_view/helpers/asset_tag_helper.rb +356 -101
  17. data/lib/action_view/helpers/asset_url_helper.rb +180 -74
  18. data/lib/action_view/helpers/atom_feed_helper.rb +21 -19
  19. data/lib/action_view/helpers/cache_helper.rb +156 -43
  20. data/lib/action_view/helpers/capture_helper.rb +21 -14
  21. data/lib/action_view/helpers/controller_helper.rb +16 -5
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  24. data/lib/action_view/helpers/date_helper.rb +288 -132
  25. data/lib/action_view/helpers/debug_helper.rb +9 -6
  26. data/lib/action_view/helpers/form_helper.rb +956 -173
  27. data/lib/action_view/helpers/form_options_helper.rb +178 -97
  28. data/lib/action_view/helpers/form_tag_helper.rb +220 -101
  29. data/lib/action_view/helpers/javascript_helper.rb +33 -19
  30. data/lib/action_view/helpers/number_helper.rb +88 -63
  31. data/lib/action_view/helpers/output_safety_helper.rb +38 -6
  32. data/lib/action_view/helpers/rendering_helper.rb +21 -10
  33. data/lib/action_view/helpers/sanitize_helper.rb +31 -32
  34. data/lib/action_view/helpers/tag_helper.rb +332 -71
  35. data/lib/action_view/helpers/tags/base.rb +123 -99
  36. data/lib/action_view/helpers/tags/check_box.rb +21 -20
  37. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  38. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  39. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  40. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  41. data/lib/action_view/helpers/tags/collection_select.rb +5 -3
  42. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  43. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  44. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  45. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  46. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  48. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  49. data/lib/action_view/helpers/tags/file_field.rb +18 -0
  50. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  51. data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
  52. data/lib/action_view/helpers/tags/label.rb +7 -2
  53. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  54. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  55. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  56. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  57. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  58. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  59. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  60. data/lib/action_view/helpers/tags/select.rb +11 -10
  61. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  62. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  63. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  64. data/lib/action_view/helpers/tags/time_field.rb +12 -2
  65. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  66. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  67. data/lib/action_view/helpers/tags/translator.rb +15 -16
  68. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  69. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  70. data/lib/action_view/helpers/tags/weekday_select.rb +28 -0
  71. data/lib/action_view/helpers/tags.rb +5 -2
  72. data/lib/action_view/helpers/text_helper.rb +80 -51
  73. data/lib/action_view/helpers/translation_helper.rb +120 -69
  74. data/lib/action_view/helpers/url_helper.rb +398 -171
  75. data/lib/action_view/helpers.rb +29 -27
  76. data/lib/action_view/layouts.rb +68 -63
  77. data/lib/action_view/log_subscriber.rb +77 -10
  78. data/lib/action_view/lookup_context.rb +137 -113
  79. data/lib/action_view/model_naming.rb +4 -2
  80. data/lib/action_view/path_set.rb +28 -32
  81. data/lib/action_view/railtie.rb +74 -13
  82. data/lib/action_view/record_identifier.rb +53 -26
  83. data/lib/action_view/render_parser.rb +188 -0
  84. data/lib/action_view/renderer/abstract_renderer.rb +152 -15
  85. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  86. data/lib/action_view/renderer/object_renderer.rb +34 -0
  87. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  88. data/lib/action_view/renderer/partial_renderer.rb +51 -333
  89. data/lib/action_view/renderer/renderer.rb +68 -11
  90. data/lib/action_view/renderer/streaming_template_renderer.rb +60 -56
  91. data/lib/action_view/renderer/template_renderer.rb +87 -74
  92. data/lib/action_view/rendering.rb +73 -47
  93. data/lib/action_view/ripper_ast_parser.rb +198 -0
  94. data/lib/action_view/routing_url_for.rb +35 -24
  95. data/lib/action_view/tasks/cache_digests.rake +25 -0
  96. data/lib/action_view/template/error.rb +151 -41
  97. data/lib/action_view/template/handlers/builder.rb +12 -13
  98. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  99. data/lib/action_view/template/handlers/erb.rb +29 -89
  100. data/lib/action_view/template/handlers/html.rb +11 -0
  101. data/lib/action_view/template/handlers/raw.rb +4 -4
  102. data/lib/action_view/template/handlers.rb +14 -10
  103. data/lib/action_view/template/html.rb +12 -13
  104. data/lib/action_view/template/inline.rb +22 -0
  105. data/lib/action_view/template/raw_file.rb +25 -0
  106. data/lib/action_view/template/renderable.rb +24 -0
  107. data/lib/action_view/template/resolver.rb +139 -300
  108. data/lib/action_view/template/sources/file.rb +17 -0
  109. data/lib/action_view/template/sources.rb +13 -0
  110. data/lib/action_view/template/text.rb +10 -12
  111. data/lib/action_view/template/types.rb +28 -26
  112. data/lib/action_view/template.rb +123 -91
  113. data/lib/action_view/template_details.rb +66 -0
  114. data/lib/action_view/template_path.rb +64 -0
  115. data/lib/action_view/test_case.rb +70 -53
  116. data/lib/action_view/testing/resolvers.rb +25 -35
  117. data/lib/action_view/unbound_template.rb +57 -0
  118. data/lib/action_view/version.rb +3 -1
  119. data/lib/action_view/view_paths.rb +73 -58
  120. data/lib/action_view.rb +16 -11
  121. data/lib/assets/compiled/rails-ujs.js +746 -0
  122. metadata +52 -32
  123. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  124. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,32 +1,38 @@
1
- require 'active_support/core_ext/array/extract_options'
2
- require 'active_support/core_ext/hash/keys'
3
- require 'action_view/helpers/asset_url_helper'
4
- require 'action_view/helpers/tag_helper'
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"
5
8
 
6
9
  module ActionView
7
10
  # = Action View Asset Tag Helpers
8
- module Helpers #:nodoc:
11
+ module Helpers # :nodoc:
9
12
  # This module provides methods for generating HTML that links views to assets such
10
13
  # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
11
14
  # the assets exist before linking to them:
12
15
  #
13
16
  # image_tag("rails.png")
14
- # # => <img alt="Rails" src="/assets/rails.png" />
17
+ # # => <img src="/assets/rails.png" />
15
18
  # stylesheet_link_tag("application")
16
- # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
19
+ # # => <link href="/assets/application.css?body=1" rel="stylesheet" />
17
20
  module AssetTagHelper
18
- extend ActiveSupport::Concern
19
-
20
21
  include AssetUrlHelper
21
22
  include TagHelper
22
23
 
24
+ mattr_accessor :image_loading
25
+ mattr_accessor :image_decoding
26
+ mattr_accessor :preload_links_header
27
+ mattr_accessor :apply_stylesheet_media_default
28
+
23
29
  # Returns an HTML script tag for each of the +sources+ provided.
24
30
  #
25
31
  # Sources may be paths to JavaScript files. Relative paths are assumed to be relative
26
32
  # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
27
33
  # root. Relative paths are idiomatic, use absolute paths only when needed.
28
34
  #
29
- # When passing paths, the ".js" extension is optional. If you do not want ".js"
35
+ # When passing paths, the ".js" extension is optional. If you do not want ".js"
30
36
  # appended to the path <tt>extname: false</tt> can be set on the options.
31
37
  #
32
38
  # You can modify the HTML attributes of the script tag by passing a hash as the
@@ -35,50 +41,122 @@ module ActionView
35
41
  # When the Asset Pipeline is enabled, you can pass the name of your manifest as
36
42
  # source, and include other JavaScript or CoffeeScript files inside the manifest.
37
43
  #
44
+ # If the server supports Early Hints header links for these assets will be
45
+ # automatically pushed.
46
+ #
47
+ # ==== Options
48
+ #
49
+ # When the last parameter is a hash you can add HTML attributes using that
50
+ # parameter. The following options are supported:
51
+ #
52
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
53
+ # already exists. This only applies for relative URLs.
54
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
55
+ # applies when a relative URL and +host+ options are provided.
56
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
57
+ # that path.
58
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
59
+ # when it is set to true.
60
+ # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
61
+ # you have Content Security Policy enabled.
62
+ #
63
+ # ==== Examples
64
+ #
38
65
  # javascript_include_tag "xmlhr"
39
- # # => <script src="/assets/xmlhr.js?1284139606"></script>
66
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
67
+ #
68
+ # javascript_include_tag "xmlhr", host: "localhost", protocol: "https"
69
+ # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
40
70
  #
41
71
  # javascript_include_tag "template.jst", extname: false
42
- # # => <script src="/assets/template.jst?1284139606"></script>
72
+ # # => <script src="/assets/template.debug-1284139606.jst"></script>
43
73
  #
44
74
  # javascript_include_tag "xmlhr.js"
45
- # # => <script src="/assets/xmlhr.js?1284139606"></script>
75
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
46
76
  #
47
77
  # javascript_include_tag "common.javascript", "/elsewhere/cools"
48
- # # => <script src="/assets/common.javascript?1284139606"></script>
49
- # # <script src="/elsewhere/cools.js?1423139606"></script>
78
+ # # => <script src="/assets/common.javascript.debug-1284139606.js"></script>
79
+ # # <script src="/elsewhere/cools.debug-1284139606.js"></script>
50
80
  #
51
81
  # javascript_include_tag "http://www.example.com/xmlhr"
52
82
  # # => <script src="http://www.example.com/xmlhr"></script>
53
83
  #
54
84
  # javascript_include_tag "http://www.example.com/xmlhr.js"
55
85
  # # => <script src="http://www.example.com/xmlhr.js"></script>
86
+ #
87
+ # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
88
+ # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
56
89
  def javascript_include_tag(*sources)
57
90
  options = sources.extract_options!.stringify_keys
58
- path_options = options.extract!('protocol', 'extname').symbolize_keys
59
- sources.uniq.map { |source|
91
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
92
+ preload_links = []
93
+ nopush = options["nopush"].nil? ? true : options.delete("nopush")
94
+ crossorigin = options.delete("crossorigin")
95
+ crossorigin = "anonymous" if crossorigin == true
96
+ integrity = options["integrity"]
97
+ rel = options["type"] == "module" ? "modulepreload" : "preload"
98
+
99
+ sources_tags = sources.uniq.map { |source|
100
+ href = path_to_javascript(source, path_options)
101
+ if preload_links_header && !options["defer"] && href.present? && !href.start_with?("data:")
102
+ preload_link = "<#{href}>; rel=#{rel}; as=script"
103
+ preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
104
+ preload_link += "; integrity=#{integrity}" unless integrity.nil?
105
+ preload_link += "; nopush" if nopush
106
+ preload_links << preload_link
107
+ end
60
108
  tag_options = {
61
- "src" => path_to_javascript(source, path_options)
109
+ "src" => href,
110
+ "crossorigin" => crossorigin
62
111
  }.merge!(options)
63
- content_tag(:script, "", tag_options)
112
+ if tag_options["nonce"] == true
113
+ tag_options["nonce"] = content_security_policy_nonce
114
+ end
115
+ content_tag("script", "", tag_options)
64
116
  }.join("\n").html_safe
117
+
118
+ if preload_links_header
119
+ send_preload_links_header(preload_links)
120
+ end
121
+
122
+ sources_tags
65
123
  end
66
124
 
67
- # Returns a stylesheet link tag for the sources specified as arguments. If
68
- # you don't specify an extension, <tt>.css</tt> will be appended automatically.
125
+ # Returns a stylesheet link tag for the sources specified as arguments.
126
+ #
127
+ # When passing paths, the <tt>.css</tt> extension is optional.
128
+ # If you don't specify an extension, <tt>.css</tt> will be appended automatically.
129
+ # If you do not want <tt>.css</tt> appended to the path,
130
+ # set <tt>extname: false</tt> in the options.
69
131
  # You can modify the link attributes by passing a hash as the last argument.
70
- # For historical reasons, the 'media' attribute will always be present and defaults
71
- # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
72
- # apply to all media types.
132
+ #
133
+ # If the server supports Early Hints header links for these assets will be
134
+ # automatically pushed.
135
+ #
136
+ # ==== Options
137
+ #
138
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
139
+ # already exists. This only applies for relative URLs.
140
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
141
+ # applies when a relative URL and +host+ options are provided.
142
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
143
+ # that path.
144
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
145
+ # when it is set to true.
146
+ #
147
+ # ==== Examples
73
148
  #
74
149
  # stylesheet_link_tag "style"
75
- # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
150
+ # # => <link href="/assets/style.css" rel="stylesheet" />
76
151
  #
77
152
  # stylesheet_link_tag "style.css"
78
- # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
153
+ # # => <link href="/assets/style.css" rel="stylesheet" />
79
154
  #
80
155
  # stylesheet_link_tag "http://www.example.com/style.css"
81
- # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />
156
+ # # => <link href="http://www.example.com/style.css" rel="stylesheet" />
157
+ #
158
+ # stylesheet_link_tag "style.less", extname: false, skip_pipeline: true, rel: "stylesheet/less"
159
+ # # => <link href="/stylesheets/style.less" rel="stylesheet/less">
82
160
  #
83
161
  # stylesheet_link_tag "style", media: "all"
84
162
  # # => <link href="/assets/style.css" media="all" rel="stylesheet" />
@@ -87,26 +165,50 @@ module ActionView
87
165
  # # => <link href="/assets/style.css" media="print" rel="stylesheet" />
88
166
  #
89
167
  # stylesheet_link_tag "random.styles", "/css/stylish"
90
- # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" />
91
- # # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
168
+ # # => <link href="/assets/random.styles" rel="stylesheet" />
169
+ # # <link href="/css/stylish.css" rel="stylesheet" />
92
170
  def stylesheet_link_tag(*sources)
93
171
  options = sources.extract_options!.stringify_keys
94
- path_options = options.extract!('protocol').symbolize_keys
172
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
173
+ preload_links = []
174
+ crossorigin = options.delete("crossorigin")
175
+ crossorigin = "anonymous" if crossorigin == true
176
+ nopush = options["nopush"].nil? ? true : options.delete("nopush")
177
+ integrity = options["integrity"]
95
178
 
96
- sources.uniq.map { |source|
179
+ sources_tags = sources.uniq.map { |source|
180
+ href = path_to_stylesheet(source, path_options)
181
+ if preload_links_header && href.present? && !href.start_with?("data:")
182
+ preload_link = "<#{href}>; rel=preload; as=style"
183
+ preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
184
+ preload_link += "; integrity=#{integrity}" unless integrity.nil?
185
+ preload_link += "; nopush" if nopush
186
+ preload_links << preload_link
187
+ end
97
188
  tag_options = {
98
189
  "rel" => "stylesheet",
99
- "media" => "screen",
100
- "href" => path_to_stylesheet(source, path_options)
190
+ "crossorigin" => crossorigin,
191
+ "href" => href
101
192
  }.merge!(options)
193
+
194
+ if apply_stylesheet_media_default && tag_options["media"].blank?
195
+ tag_options["media"] = "screen"
196
+ end
197
+
102
198
  tag(:link, tag_options)
103
199
  }.join("\n").html_safe
200
+
201
+ if preload_links_header
202
+ send_preload_links_header(preload_links)
203
+ end
204
+
205
+ sources_tags
104
206
  end
105
207
 
106
208
  # Returns a link tag that browsers and feed readers can use to auto-detect
107
- # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
108
- # <tt>:atom</tt>. Control the link options in url_for format using the
109
- # +url_options+. You can modify the LINK tag itself in +tag_options+.
209
+ # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
210
+ # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
211
+ # using the +url_options+. You can modify the LINK tag itself in +tag_options+.
110
212
  #
111
213
  # ==== Options
112
214
  #
@@ -120,6 +222,8 @@ module ActionView
120
222
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
121
223
  # auto_discovery_link_tag(:atom)
122
224
  # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
225
+ # auto_discovery_link_tag(:json)
226
+ # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
123
227
  # auto_discovery_link_tag(:rss, {action: "feed"})
124
228
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
125
229
  # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
@@ -129,16 +233,16 @@ module ActionView
129
233
  # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
130
234
  # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
131
235
  def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
132
- if !(type == :rss || type == :atom) && tag_options[:type].blank?
133
- raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
236
+ if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
237
+ raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
134
238
  end
135
239
 
136
240
  tag(
137
241
  "link",
138
242
  "rel" => tag_options[:rel] || "alternate",
139
- "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
243
+ "type" => tag_options[:type] || Template::Types[type].to_s,
140
244
  "title" => tag_options[:title] || type.to_s.upcase,
141
- "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
245
+ "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options
142
246
  )
143
247
  end
144
248
 
@@ -154,14 +258,14 @@ module ActionView
154
258
  #
155
259
  # The helper gets the name of the favicon file as first argument, which
156
260
  # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
157
- # to override their defaults, "shortcut icon" and "image/x-icon"
261
+ # to override their defaults, "icon" and "image/x-icon"
158
262
  # respectively:
159
263
  #
160
264
  # favicon_link_tag
161
- # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
265
+ # # => <link href="/assets/favicon.ico" rel="icon" type="image/x-icon" />
162
266
  #
163
267
  # favicon_link_tag 'myicon.ico'
164
- # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
268
+ # # => <link href="/assets/myicon.ico" rel="icon" type="image/x-icon" />
165
269
  #
166
270
  # Mobile Safari looks for a different link tag, pointing to an image that
167
271
  # will be used if you add the page to the home screen of an iOS device.
@@ -169,91 +273,164 @@ module ActionView
169
273
  #
170
274
  # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
171
275
  # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
172
- def favicon_link_tag(source='favicon.ico', options={})
173
- tag('link', {
174
- :rel => 'shortcut icon',
175
- :type => 'image/x-icon',
176
- :href => path_to_image(source)
276
+ def favicon_link_tag(source = "favicon.ico", options = {})
277
+ tag("link", {
278
+ rel: "icon",
279
+ type: "image/x-icon",
280
+ href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
281
+ }.merge!(options.symbolize_keys))
282
+ end
283
+
284
+ # Returns a link tag that browsers can use to preload the +source+.
285
+ # The +source+ can be the path of a resource managed by asset pipeline,
286
+ # a full path, or an URI.
287
+ #
288
+ # ==== Options
289
+ #
290
+ # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
291
+ # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
292
+ # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
293
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
294
+ # * <tt>:integrity</tt> - Specify the integrity attribute.
295
+ #
296
+ # ==== Examples
297
+ #
298
+ # preload_link_tag("custom_theme.css")
299
+ # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
300
+ #
301
+ # preload_link_tag("/videos/video.webm")
302
+ # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
303
+ #
304
+ # preload_link_tag(post_path(format: :json), as: "fetch")
305
+ # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
306
+ #
307
+ # preload_link_tag("worker.js", as: "worker")
308
+ # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
309
+ #
310
+ # preload_link_tag("//example.com/font.woff2")
311
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
312
+ #
313
+ # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
314
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
315
+ #
316
+ # preload_link_tag("/media/audio.ogg", nopush: true)
317
+ # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
318
+ #
319
+ def preload_link_tag(source, options = {})
320
+ href = path_to_asset(source, skip_pipeline: options.delete(:skip_pipeline))
321
+ extname = File.extname(source).downcase.delete(".")
322
+ mime_type = options.delete(:type) || Template::Types[extname]&.to_s
323
+ as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
324
+ crossorigin = options.delete(:crossorigin)
325
+ crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
326
+ integrity = options[:integrity]
327
+ nopush = options.delete(:nopush) || false
328
+ rel = mime_type == "module" ? "modulepreload" : "preload"
329
+
330
+ link_tag = tag.link(**{
331
+ rel: rel,
332
+ href: href,
333
+ as: as_type,
334
+ type: mime_type,
335
+ crossorigin: crossorigin
177
336
  }.merge!(options.symbolize_keys))
337
+
338
+ preload_link = "<#{href}>; rel=#{rel}; as=#{as_type}"
339
+ preload_link += "; type=#{mime_type}" if mime_type
340
+ preload_link += "; crossorigin=#{crossorigin}" if crossorigin
341
+ preload_link += "; integrity=#{integrity}" if integrity
342
+ preload_link += "; nopush" if nopush
343
+
344
+ send_preload_links_header([preload_link])
345
+
346
+ link_tag
178
347
  end
179
348
 
180
349
  # Returns an HTML image tag for the +source+. The +source+ can be a full
181
- # path or a file.
350
+ # path, a file, or an Active Storage attachment.
182
351
  #
183
352
  # ==== Options
184
353
  #
185
354
  # You can add HTML attributes using the +options+. The +options+ supports
186
- # two additional keys for convenience and conformance:
355
+ # additional keys for convenience and conformance:
187
356
  #
188
- # * <tt>:alt</tt> - If no alt text is given, the file name part of the
189
- # +source+ is used (capitalized and without the extension)
190
357
  # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
191
358
  # width="30" and height="45", and "50" becomes width="50" and height="50".
192
359
  # <tt>:size</tt> will be ignored if the value is not in the correct format.
360
+ # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
361
+ # pairs, each image path will be expanded before the list is formatted as a string.
193
362
  #
194
363
  # ==== Examples
195
364
  #
365
+ # Assets (images that are part of your app):
366
+ #
196
367
  # image_tag("icon")
197
- # # => <img alt="Icon" src="/assets/icon" />
368
+ # # => <img src="/assets/icon" />
198
369
  # image_tag("icon.png")
199
- # # => <img alt="Icon" src="/assets/icon.png" />
370
+ # # => <img src="/assets/icon.png" />
200
371
  # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
201
372
  # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
202
373
  # image_tag("/icons/icon.gif", size: "16")
203
- # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
374
+ # # => <img src="/icons/icon.gif" width="16" height="16" />
204
375
  # image_tag("/icons/icon.gif", height: '32', width: '32')
205
- # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
376
+ # # => <img height="32" src="/icons/icon.gif" width="32" />
206
377
  # image_tag("/icons/icon.gif", class: "menu_icon")
207
- # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
208
- def image_tag(source, options={})
378
+ # # => <img class="menu_icon" src="/icons/icon.gif" />
379
+ # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
380
+ # # => <img data-title="Rails Application" src="/icons/icon.gif" />
381
+ # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
382
+ # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
383
+ # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
384
+ # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
385
+ #
386
+ # Active Storage blobs (images that are uploaded by the users of your app):
387
+ #
388
+ # image_tag(user.avatar)
389
+ # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
390
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
391
+ # # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
392
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
393
+ # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
394
+ def image_tag(source, options = {})
209
395
  options = options.symbolize_keys
396
+ check_for_image_tag_errors(options)
397
+ skip_pipeline = options.delete(:skip_pipeline)
210
398
 
211
- src = options[:src] = path_to_image(source)
399
+ options[:src] = resolve_image_source(source, skip_pipeline)
212
400
 
213
- unless src =~ /^(?:cid|data):/ || src.blank?
214
- options[:alt] = options.fetch(:alt){ image_alt(src) }
401
+ if options[:srcset] && !options[:srcset].is_a?(String)
402
+ options[:srcset] = options[:srcset].map do |src_path, size|
403
+ src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
404
+ "#{src_path} #{size}"
405
+ end.join(", ")
215
406
  end
216
407
 
217
408
  options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
218
- tag("img", options)
219
- end
220
409
 
221
- # Returns a string suitable for an HTML image tag alt attribute.
222
- # The +src+ argument is meant to be an image file path.
223
- # The method removes the basename of the file path and the digest,
224
- # if any. It also removes hyphens and underscores from file names and
225
- # replaces them with spaces, returning a space-separated, titleized
226
- # string.
227
- #
228
- # ==== Examples
229
- #
230
- # image_alt('rails.png')
231
- # # => Rails
232
- #
233
- # image_alt('hyphenated-file-name.png')
234
- # # => Hyphenated file name
235
- #
236
- # image_alt('underscored_file_name.png')
237
- # # => Underscored file name
238
- def image_alt(src)
239
- File.basename(src, '.*').sub(/-[[:xdigit:]]{32,64}\z/, '').tr('-_', ' ').capitalize
410
+ options[:loading] ||= image_loading if image_loading
411
+ options[:decoding] ||= image_decoding if image_decoding
412
+
413
+ tag("img", options)
240
414
  end
241
415
 
242
416
  # Returns an HTML video tag for the +sources+. If +sources+ is a string,
243
417
  # a single video tag will be returned. If +sources+ is an array, a video
244
418
  # tag with nested source tags for each source will be returned. The
245
- # +sources+ can be full paths or files that exists in your public videos
419
+ # +sources+ can be full paths or files that exist in your public videos
246
420
  # directory.
247
421
  #
248
422
  # ==== Options
249
- # You can add HTML attributes using the +options+. The +options+ supports
250
- # two additional keys for convenience and conformance:
423
+ #
424
+ # When the last parameter is a hash you can add HTML attributes using that
425
+ # parameter. The following options are supported:
251
426
  #
252
427
  # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
253
428
  # before the video loads. The path is calculated like the +src+ of +image_tag+.
254
429
  # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
255
430
  # width="30" and height="45", and "50" becomes width="50" and height="50".
256
431
  # <tt>:size</tt> will be ignored if the value is not in the correct format.
432
+ # * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using
433
+ # the <tt>:poster</tt> option instead using an asset in the public folder.
257
434
  #
258
435
  # ==== Examples
259
436
  #
@@ -261,10 +438,12 @@ module ActionView
261
438
  # # => <video src="/videos/trailer"></video>
262
439
  # video_tag("trailer.ogg")
263
440
  # # => <video src="/videos/trailer.ogg"></video>
264
- # video_tag("trailer.ogg", controls: true, autobuffer: true)
265
- # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video>
441
+ # video_tag("trailer.ogg", controls: true, preload: 'none')
442
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
266
443
  # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
267
444
  # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
445
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
446
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video>
268
447
  # video_tag("/trailers/hd.avi", size: "16x16")
269
448
  # # => <video src="/trailers/hd.avi" width="16" height="16"></video>
270
449
  # video_tag("/trailers/hd.avi", size: "16")
@@ -278,15 +457,23 @@ module ActionView
278
457
  # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
279
458
  # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
280
459
  def video_tag(*sources)
281
- multiple_sources_tag('video', sources) do |options|
282
- options[:poster] = path_to_image(options[:poster]) if options[:poster]
283
- options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
460
+ options = sources.extract_options!.symbolize_keys
461
+ public_poster_folder = options.delete(:poster_skip_pipeline)
462
+ sources << options
463
+ multiple_sources_tag_builder("video", sources) do |tag_options|
464
+ tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster]
465
+ tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size]
284
466
  end
285
467
  end
286
468
 
287
- # Returns an HTML audio tag for the +source+.
288
- # The +source+ can be full path or file that exists in
289
- # your public audios directory.
469
+ # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
470
+ # a single audio tag will be returned. If +sources+ is an array, an audio
471
+ # tag with nested source tags for each source will be returned. The
472
+ # +sources+ can be full paths or files that exist in your public audios
473
+ # directory.
474
+ #
475
+ # When the last parameter is a hash you can add HTML attributes using that
476
+ # parameter.
290
477
  #
291
478
  # audio_tag("sound")
292
479
  # # => <audio src="/audios/sound"></audio>
@@ -297,33 +484,101 @@ module ActionView
297
484
  # audio_tag("sound.wav", "sound.mid")
298
485
  # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
299
486
  def audio_tag(*sources)
300
- multiple_sources_tag('audio', sources)
487
+ multiple_sources_tag_builder("audio", sources)
301
488
  end
302
489
 
303
490
  private
304
- def multiple_sources_tag(type, sources)
305
- options = sources.extract_options!.symbolize_keys
491
+ def multiple_sources_tag_builder(type, sources)
492
+ options = sources.extract_options!.symbolize_keys
493
+ skip_pipeline = options.delete(:skip_pipeline)
306
494
  sources.flatten!
307
495
 
308
496
  yield options if block_given?
309
497
 
310
498
  if sources.size > 1
311
499
  content_tag(type, options) do
312
- safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) }
500
+ safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
313
501
  end
314
502
  else
315
- options[:src] = send("path_to_#{type}", sources.first)
503
+ options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
316
504
  content_tag(type, nil, options)
317
505
  end
318
506
  end
319
507
 
508
+ def resolve_image_source(source, skip_pipeline)
509
+ if source.is_a?(Symbol) || source.is_a?(String)
510
+ path_to_image(source, skip_pipeline: skip_pipeline)
511
+ else
512
+ polymorphic_url(source)
513
+ end
514
+ rescue NoMethodError => e
515
+ raise ArgumentError, "Can't resolve image into URL: #{e}"
516
+ end
517
+
320
518
  def extract_dimensions(size)
321
- if size =~ %r{\A\d+x\d+\z}
322
- size.split('x')
323
- elsif size =~ %r{\A\d+\z}
519
+ size = size.to_s
520
+ if /\A\d+x\d+\z/.match?(size)
521
+ size.split("x")
522
+ elsif /\A\d+\z/.match?(size)
324
523
  [size, size]
325
524
  end
326
525
  end
526
+
527
+ def check_for_image_tag_errors(options)
528
+ if options[:size] && (options[:height] || options[:width])
529
+ raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
530
+ end
531
+ end
532
+
533
+ def resolve_link_as(extname, mime_type)
534
+ case extname
535
+ when "js" then "script"
536
+ when "css" then "style"
537
+ when "vtt" then "track"
538
+ else
539
+ mime_type.to_s.split("/").first.presence_in(%w(audio video font image))
540
+ end
541
+ end
542
+
543
+ MAX_HEADER_SIZE = 8_000 # Some HTTP client and proxies have a 8kiB header limit
544
+ def send_preload_links_header(preload_links, max_header_size: MAX_HEADER_SIZE)
545
+ return if preload_links.empty?
546
+ return if respond_to?(:response) && response&.sending?
547
+
548
+ if respond_to?(:request) && request
549
+ request.send_early_hints("Link" => preload_links.join("\n"))
550
+ end
551
+
552
+ if respond_to?(:response) && response
553
+ header = response.headers["Link"]
554
+ header = header ? header.dup : +""
555
+
556
+ # rindex count characters not bytes, but we assume non-ascii characters
557
+ # are rare in urls, and we have a 192 bytes margin.
558
+ last_line_offset = header.rindex("\n")
559
+ last_line_size = if last_line_offset
560
+ header.bytesize - last_line_offset
561
+ else
562
+ header.bytesize
563
+ end
564
+
565
+ preload_links.each do |link|
566
+ if link.bytesize + last_line_size + 1 < max_header_size
567
+ unless header.empty?
568
+ header << ","
569
+ last_line_size += 1
570
+ end
571
+ else
572
+ header << "\n"
573
+ last_line_size = 0
574
+ end
575
+ header << link
576
+ last_line_size += link.bytesize
577
+ end
578
+
579
+ response.headers["Link"] = header
580
+ end
581
+ end
327
582
  end
328
583
  end
329
584
  end