actionview 5.1.4 → 6.1.1

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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +199 -168
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -5
  5. data/lib/action_view.rb +10 -4
  6. data/lib/action_view/base.rb +87 -23
  7. data/lib/action_view/buffers.rb +17 -0
  8. data/lib/action_view/cache_expiry.rb +52 -0
  9. data/lib/action_view/context.rb +7 -11
  10. data/lib/action_view/dependency_tracker.rb +12 -4
  11. data/lib/action_view/digestor.rb +24 -23
  12. data/lib/action_view/flows.rb +2 -1
  13. data/lib/action_view/gem_version.rb +4 -2
  14. data/lib/action_view/helpers.rb +4 -2
  15. data/lib/action_view/helpers/active_model_helper.rb +9 -4
  16. data/lib/action_view/helpers/asset_tag_helper.rb +220 -57
  17. data/lib/action_view/helpers/asset_url_helper.rb +28 -23
  18. data/lib/action_view/helpers/atom_feed_helper.rb +5 -2
  19. data/lib/action_view/helpers/cache_helper.rb +39 -28
  20. data/lib/action_view/helpers/capture_helper.rb +13 -7
  21. data/lib/action_view/helpers/controller_helper.rb +3 -1
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +5 -3
  24. data/lib/action_view/helpers/date_helper.rb +78 -33
  25. data/lib/action_view/helpers/debug_helper.rb +4 -2
  26. data/lib/action_view/helpers/form_helper.rb +357 -106
  27. data/lib/action_view/helpers/form_options_helper.rb +45 -39
  28. data/lib/action_view/helpers/form_tag_helper.rb +42 -27
  29. data/lib/action_view/helpers/javascript_helper.rb +28 -12
  30. data/lib/action_view/helpers/number_helper.rb +16 -8
  31. data/lib/action_view/helpers/output_safety_helper.rb +3 -1
  32. data/lib/action_view/helpers/rendering_helper.rb +20 -9
  33. data/lib/action_view/helpers/sanitize_helper.rb +15 -19
  34. data/lib/action_view/helpers/tag_helper.rb +100 -24
  35. data/lib/action_view/helpers/tags.rb +3 -1
  36. data/lib/action_view/helpers/tags/base.rb +30 -21
  37. data/lib/action_view/helpers/tags/check_box.rb +3 -2
  38. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +2 -1
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +2 -1
  42. data/lib/action_view/helpers/tags/collection_select.rb +3 -1
  43. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/date_select.rb +5 -4
  46. data/lib/action_view/helpers/tags/datetime_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  48. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -1
  52. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/label.rb +6 -5
  54. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  55. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +2 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +3 -2
  59. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +2 -0
  61. data/lib/action_view/helpers/tags/select.rb +4 -3
  62. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +3 -1
  64. data/lib/action_view/helpers/tags/text_field.rb +3 -2
  65. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  66. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  68. data/lib/action_view/helpers/tags/translator.rb +3 -6
  69. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  71. data/lib/action_view/helpers/text_helper.rb +11 -10
  72. data/lib/action_view/helpers/translation_helper.rb +102 -52
  73. data/lib/action_view/helpers/url_helper.rb +150 -32
  74. data/lib/action_view/layouts.rb +15 -15
  75. data/lib/action_view/log_subscriber.rb +32 -15
  76. data/lib/action_view/lookup_context.rb +67 -39
  77. data/lib/action_view/model_naming.rb +2 -0
  78. data/lib/action_view/path_set.rb +5 -12
  79. data/lib/action_view/railtie.rb +46 -21
  80. data/lib/action_view/record_identifier.rb +4 -3
  81. data/lib/action_view/renderer/abstract_renderer.rb +144 -11
  82. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  83. data/lib/action_view/renderer/object_renderer.rb +34 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +33 -283
  85. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +64 -17
  86. data/lib/action_view/renderer/renderer.rb +61 -4
  87. data/lib/action_view/renderer/streaming_template_renderer.rb +14 -8
  88. data/lib/action_view/renderer/template_renderer.rb +36 -26
  89. data/lib/action_view/rendering.rb +57 -38
  90. data/lib/action_view/routing_url_for.rb +15 -12
  91. data/lib/action_view/tasks/cache_digests.rake +2 -0
  92. data/lib/action_view/template.rb +69 -76
  93. data/lib/action_view/template/error.rb +32 -18
  94. data/lib/action_view/template/handlers.rb +4 -2
  95. data/lib/action_view/template/handlers/builder.rb +5 -6
  96. data/lib/action_view/template/handlers/erb.rb +20 -19
  97. data/lib/action_view/template/handlers/erb/erubi.rb +17 -9
  98. data/lib/action_view/template/handlers/html.rb +3 -1
  99. data/lib/action_view/template/handlers/raw.rb +4 -2
  100. data/lib/action_view/template/html.rb +8 -7
  101. data/lib/action_view/template/inline.rb +22 -0
  102. data/lib/action_view/template/raw_file.rb +25 -0
  103. data/lib/action_view/template/renderable.rb +24 -0
  104. data/lib/action_view/template/resolver.rb +194 -152
  105. data/lib/action_view/template/sources.rb +13 -0
  106. data/lib/action_view/template/sources/file.rb +17 -0
  107. data/lib/action_view/template/text.rb +5 -4
  108. data/lib/action_view/template/types.rb +3 -1
  109. data/lib/action_view/test_case.rb +38 -30
  110. data/lib/action_view/testing/resolvers.rb +20 -27
  111. data/lib/action_view/unbound_template.rb +31 -0
  112. data/lib/action_view/version.rb +2 -0
  113. data/lib/action_view/view_paths.rb +61 -40
  114. data/lib/assets/compiled/rails-ujs.js +84 -23
  115. metadata +34 -23
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -21
  117. data/lib/action_view/template/handlers/erb/deprecated_erubis.rb +0 -9
  118. data/lib/action_view/template/handlers/erb/erubis.rb +0 -81
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/string/output_safety"
2
4
 
3
5
  module ActionView
@@ -66,7 +68,6 @@ module ActionView
66
68
  end
67
69
 
68
70
  private
69
-
70
71
  def inside_fiber?
71
72
  Fiber.current.object_id != @root
72
73
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
3
5
  def self.gem_version
@@ -5,9 +7,9 @@ module ActionView
5
7
  end
6
8
 
7
9
  module VERSION
8
- MAJOR = 5
10
+ MAJOR = 6
9
11
  MINOR = 1
10
- TINY = 4
12
+ TINY = 1
11
13
  PRE = nil
12
14
 
13
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/benchmarkable"
2
4
 
3
5
  module ActionView #:nodoc:
@@ -11,6 +13,7 @@ module ActionView #:nodoc:
11
13
  autoload :CacheHelper
12
14
  autoload :CaptureHelper
13
15
  autoload :ControllerHelper
16
+ autoload :CspHelper
14
17
  autoload :CsrfHelper
15
18
  autoload :DateHelper
16
19
  autoload :DebugHelper
@@ -20,7 +23,6 @@ module ActionView #:nodoc:
20
23
  autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
21
24
  autoload :NumberHelper
22
25
  autoload :OutputSafetyHelper
23
- autoload :RecordTagHelper
24
26
  autoload :RenderingHelper
25
27
  autoload :SanitizeHelper
26
28
  autoload :TagHelper
@@ -44,6 +46,7 @@ module ActionView #:nodoc:
44
46
  include CacheHelper
45
47
  include CaptureHelper
46
48
  include ControllerHelper
49
+ include CspHelper
47
50
  include CsrfHelper
48
51
  include DateHelper
49
52
  include DebugHelper
@@ -53,7 +56,6 @@ module ActionView #:nodoc:
53
56
  include JavaScriptHelper
54
57
  include NumberHelper
55
58
  include OutputSafetyHelper
56
- include RecordTagHelper
57
59
  include RenderingHelper
58
60
  include SanitizeHelper
59
61
  include TagHelper
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/module/attribute_accessors"
2
4
  require "active_support/core_ext/enumerable"
3
5
 
4
6
  module ActionView
5
7
  # = Active Model Helpers
6
- module Helpers
8
+ module Helpers #:nodoc:
7
9
  module ActiveModelHelper
8
10
  end
9
11
 
@@ -15,8 +17,8 @@ module ActionView
15
17
  end
16
18
  end
17
19
 
18
- def content_tag(*)
19
- error_wrapping(super)
20
+ def content_tag(type, options, *)
21
+ select_markup_helper?(type) ? super : error_wrapping(super)
20
22
  end
21
23
 
22
24
  def tag(type, options, *)
@@ -36,11 +38,14 @@ module ActionView
36
38
  end
37
39
 
38
40
  private
39
-
40
41
  def object_has_errors?
41
42
  object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
42
43
  end
43
44
 
45
+ def select_markup_helper?(type)
46
+ ["optgroup", "option"].include?(type)
47
+ end
48
+
44
49
  def tag_generate_errors?(options)
45
50
  options["type"] != "hidden"
46
51
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/array/extract_options"
2
4
  require "active_support/core_ext/hash/keys"
5
+ require "active_support/core_ext/object/inclusion"
3
6
  require "action_view/helpers/asset_url_helper"
4
7
  require "action_view/helpers/tag_helper"
5
8
 
@@ -11,7 +14,7 @@ module ActionView
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
19
  # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
17
20
  module AssetTagHelper
@@ -20,13 +23,15 @@ module ActionView
20
23
  include AssetUrlHelper
21
24
  include TagHelper
22
25
 
26
+ mattr_accessor :preload_links_header
27
+
23
28
  # Returns an HTML script tag for each of the +sources+ provided.
24
29
  #
25
30
  # Sources may be paths to JavaScript files. Relative paths are assumed to be relative
26
31
  # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
27
32
  # root. Relative paths are idiomatic, use absolute paths only when needed.
28
33
  #
29
- # When passing paths, the ".js" extension is optional. If you do not want ".js"
34
+ # When passing paths, the ".js" extension is optional. If you do not want ".js"
30
35
  # appended to the path <tt>extname: false</tt> can be set on the options.
31
36
  #
32
37
  # You can modify the HTML attributes of the script tag by passing a hash as the
@@ -35,19 +40,24 @@ module ActionView
35
40
  # When the Asset Pipeline is enabled, you can pass the name of your manifest as
36
41
  # source, and include other JavaScript or CoffeeScript files inside the manifest.
37
42
  #
43
+ # If the server supports Early Hints header links for these assets will be
44
+ # automatically pushed.
45
+ #
38
46
  # ==== Options
39
47
  #
40
48
  # When the last parameter is a hash you can add HTML attributes using that
41
49
  # parameter. The following options are supported:
42
50
  #
43
- # * <tt>:extname</tt> - Append an extension to the generated url unless the extension
44
- # already exists. This only applies for relative urls.
45
- # * <tt>:protocol</tt> - Sets the protocol of the generated url, this option only
46
- # applies when a relative url and +host+ options are provided.
47
- # * <tt>:host</tt> - When a relative url is provided the host is added to the
51
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
52
+ # already exists. This only applies for relative URLs.
53
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
54
+ # applies when a relative URL and +host+ options are provided.
55
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
48
56
  # that path.
49
57
  # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
50
58
  # when it is set to true.
59
+ # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
60
+ # you have Content Security Policy enabled.
51
61
  #
52
62
  # ==== Examples
53
63
  #
@@ -72,15 +82,42 @@ module ActionView
72
82
  #
73
83
  # javascript_include_tag "http://www.example.com/xmlhr.js"
74
84
  # # => <script src="http://www.example.com/xmlhr.js"></script>
85
+ #
86
+ # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
87
+ # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
75
88
  def javascript_include_tag(*sources)
76
89
  options = sources.extract_options!.stringify_keys
77
90
  path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
78
- sources.uniq.map { |source|
91
+ preload_links = []
92
+ nopush = options["nopush"].nil? ? true : options.delete("nopush")
93
+ crossorigin = options.delete("crossorigin")
94
+ crossorigin = "anonymous" if crossorigin == true
95
+ integrity = options["integrity"]
96
+
97
+ sources_tags = sources.uniq.map { |source|
98
+ href = path_to_javascript(source, path_options)
99
+ if preload_links_header && !options["defer"]
100
+ preload_link = "<#{href}>; rel=preload; as=script"
101
+ preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
102
+ preload_link += "; integrity=#{integrity}" unless integrity.nil?
103
+ preload_link += "; nopush" if nopush
104
+ preload_links << preload_link
105
+ end
79
106
  tag_options = {
80
- "src" => path_to_javascript(source, path_options)
107
+ "src" => href,
108
+ "crossorigin" => crossorigin
81
109
  }.merge!(options)
82
- content_tag("script".freeze, "", tag_options)
110
+ if tag_options["nonce"] == true
111
+ tag_options["nonce"] = content_security_policy_nonce
112
+ end
113
+ content_tag("script", "", tag_options)
83
114
  }.join("\n").html_safe
115
+
116
+ if preload_links_header
117
+ send_preload_links_header(preload_links)
118
+ end
119
+
120
+ sources_tags
84
121
  end
85
122
 
86
123
  # Returns a stylesheet link tag for the sources specified as arguments. If
@@ -90,6 +127,9 @@ module ActionView
90
127
  # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
91
128
  # apply to all media types.
92
129
  #
130
+ # If the server supports Early Hints header links for these assets will be
131
+ # automatically pushed.
132
+ #
93
133
  # stylesheet_link_tag "style"
94
134
  # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
95
135
  #
@@ -111,20 +151,41 @@ module ActionView
111
151
  def stylesheet_link_tag(*sources)
112
152
  options = sources.extract_options!.stringify_keys
113
153
  path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
114
- sources.uniq.map { |source|
154
+ preload_links = []
155
+ crossorigin = options.delete("crossorigin")
156
+ crossorigin = "anonymous" if crossorigin == true
157
+ nopush = options["nopush"].nil? ? true : options.delete("nopush")
158
+ integrity = options["integrity"]
159
+
160
+ sources_tags = sources.uniq.map { |source|
161
+ href = path_to_stylesheet(source, path_options)
162
+ if preload_links_header
163
+ preload_link = "<#{href}>; rel=preload; as=style"
164
+ preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
165
+ preload_link += "; integrity=#{integrity}" unless integrity.nil?
166
+ preload_link += "; nopush" if nopush
167
+ preload_links << preload_link
168
+ end
115
169
  tag_options = {
116
170
  "rel" => "stylesheet",
117
171
  "media" => "screen",
118
- "href" => path_to_stylesheet(source, path_options)
172
+ "crossorigin" => crossorigin,
173
+ "href" => href
119
174
  }.merge!(options)
120
175
  tag(:link, tag_options)
121
176
  }.join("\n").html_safe
177
+
178
+ if preload_links_header
179
+ send_preload_links_header(preload_links)
180
+ end
181
+
182
+ sources_tags
122
183
  end
123
184
 
124
185
  # Returns a link tag that browsers and feed readers can use to auto-detect
125
- # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
126
- # <tt>:atom</tt>. Control the link options in url_for format using the
127
- # +url_options+. You can modify the LINK tag itself in +tag_options+.
186
+ # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
187
+ # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
188
+ # using the +url_options+. You can modify the LINK tag itself in +tag_options+.
128
189
  #
129
190
  # ==== Options
130
191
  #
@@ -138,6 +199,8 @@ module ActionView
138
199
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
139
200
  # auto_discovery_link_tag(:atom)
140
201
  # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
202
+ # auto_discovery_link_tag(:json)
203
+ # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
141
204
  # auto_discovery_link_tag(:rss, {action: "feed"})
142
205
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
143
206
  # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
@@ -147,8 +210,8 @@ module ActionView
147
210
  # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
148
211
  # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
149
212
  def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
150
- if !(type == :rss || type == :atom) && tag_options[:type].blank?
151
- raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
213
+ if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
214
+ raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
152
215
  end
153
216
 
154
217
  tag(
@@ -195,80 +258,143 @@ module ActionView
195
258
  }.merge!(options.symbolize_keys))
196
259
  end
197
260
 
261
+ # Returns a link tag that browsers can use to preload the +source+.
262
+ # The +source+ can be the path of a resource managed by asset pipeline,
263
+ # a full path, or an URI.
264
+ #
265
+ # ==== Options
266
+ #
267
+ # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
268
+ # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
269
+ # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
270
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
271
+ # * <tt>:integrity</tt> - Specify the integrity attribute.
272
+ #
273
+ # ==== Examples
274
+ #
275
+ # preload_link_tag("custom_theme.css")
276
+ # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
277
+ #
278
+ # preload_link_tag("/videos/video.webm")
279
+ # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
280
+ #
281
+ # preload_link_tag(post_path(format: :json), as: "fetch")
282
+ # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
283
+ #
284
+ # preload_link_tag("worker.js", as: "worker")
285
+ # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
286
+ #
287
+ # preload_link_tag("//example.com/font.woff2")
288
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
289
+ #
290
+ # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
291
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
292
+ #
293
+ # preload_link_tag("/media/audio.ogg", nopush: true)
294
+ # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
295
+ #
296
+ def preload_link_tag(source, options = {})
297
+ href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
298
+ extname = File.extname(source).downcase.delete(".")
299
+ mime_type = options.delete(:type) || Template::Types[extname]&.to_s
300
+ as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
301
+ crossorigin = options.delete(:crossorigin)
302
+ crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
303
+ integrity = options[:integrity]
304
+ nopush = options.delete(:nopush) || false
305
+
306
+ link_tag = tag.link(**{
307
+ rel: "preload",
308
+ href: href,
309
+ as: as_type,
310
+ type: mime_type,
311
+ crossorigin: crossorigin
312
+ }.merge!(options.symbolize_keys))
313
+
314
+ preload_link = "<#{href}>; rel=preload; as=#{as_type}"
315
+ preload_link += "; type=#{mime_type}" if mime_type
316
+ preload_link += "; crossorigin=#{crossorigin}" if crossorigin
317
+ preload_link += "; integrity=#{integrity}" if integrity
318
+ preload_link += "; nopush" if nopush
319
+
320
+ send_preload_links_header([preload_link])
321
+
322
+ link_tag
323
+ end
324
+
198
325
  # Returns an HTML image tag for the +source+. The +source+ can be a full
199
- # path or a file.
326
+ # path, a file, or an Active Storage attachment.
200
327
  #
201
328
  # ==== Options
202
329
  #
203
330
  # You can add HTML attributes using the +options+. The +options+ supports
204
- # two additional keys for convenience and conformance:
331
+ # additional keys for convenience and conformance:
205
332
  #
206
- # * <tt>:alt</tt> - If no alt text is given, the file name part of the
207
- # +source+ is used (capitalized and without the extension)
208
333
  # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
209
334
  # width="30" and height="45", and "50" becomes width="50" and height="50".
210
335
  # <tt>:size</tt> will be ignored if the value is not in the correct format.
336
+ # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
337
+ # pairs, each image path will be expanded before the list is formatted as a string.
211
338
  #
212
339
  # ==== Examples
213
340
  #
341
+ # Assets (images that are part of your app):
342
+ #
214
343
  # image_tag("icon")
215
- # # => <img alt="Icon" src="/assets/icon" />
344
+ # # => <img src="/assets/icon" />
216
345
  # image_tag("icon.png")
217
- # # => <img alt="Icon" src="/assets/icon.png" />
346
+ # # => <img src="/assets/icon.png" />
218
347
  # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
219
348
  # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
220
349
  # image_tag("/icons/icon.gif", size: "16")
221
- # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
350
+ # # => <img src="/icons/icon.gif" width="16" height="16" />
222
351
  # image_tag("/icons/icon.gif", height: '32', width: '32')
223
- # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
352
+ # # => <img height="32" src="/icons/icon.gif" width="32" />
224
353
  # image_tag("/icons/icon.gif", class: "menu_icon")
225
- # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
354
+ # # => <img class="menu_icon" src="/icons/icon.gif" />
226
355
  # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
227
356
  # # => <img data-title="Rails Application" src="/icons/icon.gif" />
357
+ # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
358
+ # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
359
+ # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
360
+ # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
361
+ #
362
+ # Active Storage blobs (images that are uploaded by the users of your app):
363
+ #
364
+ # image_tag(user.avatar)
365
+ # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
366
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
367
+ # # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
368
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
369
+ # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
228
370
  def image_tag(source, options = {})
229
371
  options = options.symbolize_keys
230
372
  check_for_image_tag_errors(options)
373
+ skip_pipeline = options.delete(:skip_pipeline)
231
374
 
232
- src = options[:src] = path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
375
+ options[:src] = resolve_image_source(source, skip_pipeline)
233
376
 
234
- unless src.start_with?("cid:") || src.start_with?("data:") || src.blank?
235
- options[:alt] = options.fetch(:alt) { image_alt(src) }
377
+ if options[:srcset] && !options[:srcset].is_a?(String)
378
+ options[:srcset] = options[:srcset].map do |src_path, size|
379
+ src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
380
+ "#{src_path} #{size}"
381
+ end.join(", ")
236
382
  end
237
383
 
238
384
  options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
239
385
  tag("img", options)
240
386
  end
241
387
 
242
- # Returns a string suitable for an HTML image tag alt attribute.
243
- # The +src+ argument is meant to be an image file path.
244
- # The method removes the basename of the file path and the digest,
245
- # if any. It also removes hyphens and underscores from file names and
246
- # replaces them with spaces, returning a space-separated, titleized
247
- # string.
248
- #
249
- # ==== Examples
250
- #
251
- # image_alt('rails.png')
252
- # # => Rails
253
- #
254
- # image_alt('hyphenated-file-name.png')
255
- # # => Hyphenated file name
256
- #
257
- # image_alt('underscored_file_name.png')
258
- # # => Underscored file name
259
- def image_alt(src)
260
- File.basename(src, ".*".freeze).sub(/-[[:xdigit:]]{32,64}\z/, "".freeze).tr("-_".freeze, " ".freeze).capitalize
261
- end
262
-
263
388
  # Returns an HTML video tag for the +sources+. If +sources+ is a string,
264
389
  # a single video tag will be returned. If +sources+ is an array, a video
265
390
  # tag with nested source tags for each source will be returned. The
266
- # +sources+ can be full paths or files that exists in your public videos
391
+ # +sources+ can be full paths or files that exist in your public videos
267
392
  # directory.
268
393
  #
269
394
  # ==== Options
270
- # You can add HTML attributes using the +options+. The +options+ supports
271
- # two additional keys for convenience and conformance:
395
+ #
396
+ # When the last parameter is a hash you can add HTML attributes using that
397
+ # parameter. The following options are supported:
272
398
  #
273
399
  # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
274
400
  # before the video loads. The path is calculated like the +src+ of +image_tag+.
@@ -285,7 +411,7 @@ module ActionView
285
411
  # video_tag("trailer.ogg")
286
412
  # # => <video src="/videos/trailer.ogg"></video>
287
413
  # video_tag("trailer.ogg", controls: true, preload: 'none')
288
- # # => <video preload="none" controls="controls" src="/videos/trailer.ogg" ></video>
414
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
289
415
  # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
290
416
  # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
291
417
  # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
@@ -312,9 +438,14 @@ module ActionView
312
438
  end
313
439
  end
314
440
 
315
- # Returns an HTML audio tag for the +source+.
316
- # The +source+ can be full path or file that exists in
317
- # your public audios directory.
441
+ # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
442
+ # a single audio tag will be returned. If +sources+ is an array, an audio
443
+ # tag with nested source tags for each source will be returned. The
444
+ # +sources+ can be full paths or files that exist in your public audios
445
+ # directory.
446
+ #
447
+ # When the last parameter is a hash you can add HTML attributes using that
448
+ # parameter.
318
449
  #
319
450
  # audio_tag("sound")
320
451
  # # => <audio src="/audios/sound"></audio>
@@ -346,6 +477,16 @@ module ActionView
346
477
  end
347
478
  end
348
479
 
480
+ def resolve_image_source(source, skip_pipeline)
481
+ if source.is_a?(Symbol) || source.is_a?(String)
482
+ path_to_image(source, skip_pipeline: skip_pipeline)
483
+ else
484
+ polymorphic_url(source)
485
+ end
486
+ rescue NoMethodError => e
487
+ raise ArgumentError, "Can't resolve image into URL: #{e}"
488
+ end
489
+
349
490
  def extract_dimensions(size)
350
491
  size = size.to_s
351
492
  if /\A\d+x\d+\z/.match?(size)
@@ -360,6 +501,28 @@ module ActionView
360
501
  raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
361
502
  end
362
503
  end
504
+
505
+ def resolve_link_as(extname, mime_type)
506
+ if extname == "js"
507
+ "script"
508
+ elsif extname == "css"
509
+ "style"
510
+ elsif extname == "vtt"
511
+ "track"
512
+ elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
513
+ type
514
+ end
515
+ end
516
+
517
+ def send_preload_links_header(preload_links)
518
+ if respond_to?(:request) && request
519
+ request.send_early_hints("Link" => preload_links.join("\n"))
520
+ end
521
+
522
+ if respond_to?(:response) && response
523
+ response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
524
+ end
525
+ end
363
526
  end
364
527
  end
365
528
  end