actionview 5.1.7 → 5.2.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -224
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/action_view.rb +4 -3
  6. data/lib/action_view/base.rb +8 -10
  7. data/lib/action_view/buffers.rb +2 -0
  8. data/lib/action_view/context.rb +2 -2
  9. data/lib/action_view/dependency_tracker.rb +2 -0
  10. data/lib/action_view/digestor.rb +7 -7
  11. data/lib/action_view/flows.rb +2 -0
  12. data/lib/action_view/gem_version.rb +5 -3
  13. data/lib/action_view/helpers.rb +4 -0
  14. data/lib/action_view/helpers/active_model_helper.rb +9 -3
  15. data/lib/action_view/helpers/asset_tag_helper.rb +180 -34
  16. data/lib/action_view/helpers/asset_url_helper.rb +19 -17
  17. data/lib/action_view/helpers/atom_feed_helper.rb +3 -1
  18. data/lib/action_view/helpers/cache_helper.rb +24 -14
  19. data/lib/action_view/helpers/capture_helper.rb +9 -7
  20. data/lib/action_view/helpers/controller_helper.rb +3 -1
  21. data/lib/action_view/helpers/csp_helper.rb +24 -0
  22. data/lib/action_view/helpers/csrf_helper.rb +4 -2
  23. data/lib/action_view/helpers/date_helper.rb +7 -5
  24. data/lib/action_view/helpers/debug_helper.rb +4 -2
  25. data/lib/action_view/helpers/form_helper.rb +53 -70
  26. data/lib/action_view/helpers/form_options_helper.rb +23 -17
  27. data/lib/action_view/helpers/form_tag_helper.rb +23 -11
  28. data/lib/action_view/helpers/javascript_helper.rb +20 -5
  29. data/lib/action_view/helpers/number_helper.rb +2 -0
  30. data/lib/action_view/helpers/output_safety_helper.rb +2 -0
  31. data/lib/action_view/helpers/record_tag_helper.rb +3 -1
  32. data/lib/action_view/helpers/rendering_helper.rb +3 -1
  33. data/lib/action_view/helpers/sanitize_helper.rb +3 -1
  34. data/lib/action_view/helpers/tag_helper.rb +2 -2
  35. data/lib/action_view/helpers/tags.rb +3 -1
  36. data/lib/action_view/helpers/tags/base.rb +12 -10
  37. data/lib/action_view/helpers/tags/check_box.rb +3 -1
  38. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +2 -0
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -0
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +2 -0
  42. data/lib/action_view/helpers/tags/collection_select.rb +3 -1
  43. data/lib/action_view/helpers/tags/color_field.rb +3 -1
  44. data/lib/action_view/helpers/tags/date_field.rb +2 -0
  45. data/lib/action_view/helpers/tags/date_select.rb +3 -1
  46. data/lib/action_view/helpers/tags/datetime_field.rb +3 -1
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +2 -0
  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 +2 -4
  54. data/lib/action_view/helpers/tags/month_field.rb +2 -0
  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 -1
  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 -2
  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 -1
  65. data/lib/action_view/helpers/tags/time_field.rb +2 -0
  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 +2 -0
  69. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +2 -0
  71. data/lib/action_view/helpers/text_helper.rb +9 -7
  72. data/lib/action_view/helpers/translation_helper.rb +5 -4
  73. data/lib/action_view/helpers/url_helper.rb +28 -4
  74. data/lib/action_view/layouts.rb +7 -5
  75. data/lib/action_view/log_subscriber.rb +5 -3
  76. data/lib/action_view/lookup_context.rb +4 -4
  77. data/lib/action_view/model_naming.rb +2 -0
  78. data/lib/action_view/path_set.rb +2 -0
  79. data/lib/action_view/railtie.rb +11 -2
  80. data/lib/action_view/record_identifier.rb +2 -0
  81. data/lib/action_view/renderer/abstract_renderer.rb +2 -0
  82. data/lib/action_view/renderer/partial_renderer.rb +13 -11
  83. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +4 -2
  84. data/lib/action_view/renderer/renderer.rb +2 -0
  85. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -1
  86. data/lib/action_view/renderer/template_renderer.rb +2 -0
  87. data/lib/action_view/rendering.rb +3 -5
  88. data/lib/action_view/routing_url_for.rb +2 -0
  89. data/lib/action_view/tasks/cache_digests.rake +2 -0
  90. data/lib/action_view/template.rb +6 -4
  91. data/lib/action_view/template/error.rb +2 -3
  92. data/lib/action_view/template/handlers.rb +3 -1
  93. data/lib/action_view/template/handlers/builder.rb +3 -4
  94. data/lib/action_view/template/handlers/erb.rb +5 -9
  95. data/lib/action_view/template/handlers/erb/erubi.rb +2 -0
  96. data/lib/action_view/template/handlers/html.rb +2 -0
  97. data/lib/action_view/template/handlers/raw.rb +2 -0
  98. data/lib/action_view/template/html.rb +3 -1
  99. data/lib/action_view/template/resolver.rb +7 -6
  100. data/lib/action_view/template/text.rb +3 -1
  101. data/lib/action_view/template/types.rb +3 -1
  102. data/lib/action_view/test_case.rb +21 -5
  103. data/lib/action_view/testing/resolvers.rb +3 -1
  104. data/lib/action_view/version.rb +2 -0
  105. data/lib/action_view/view_paths.rb +3 -3
  106. data/lib/assets/compiled/rails-ujs.js +52 -15
  107. metadata +12 -13
  108. data/lib/action_view/template/handlers/erb/deprecated_erubis.rb +0 -9
  109. 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/module/attr_internal"
2
4
  require "active_support/core_ext/module/attribute_accessors"
3
5
  require "active_support/ordered_options"
@@ -140,30 +142,25 @@ module ActionView #:nodoc:
140
142
  include Helpers, ::ERB::Util, Context
141
143
 
142
144
  # Specify the proc used to decorate input tags that refer to attributes with errors.
143
- cattr_accessor :field_error_proc
144
- @@field_error_proc = Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
145
+ cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
145
146
 
146
147
  # How to complete the streaming when an exception occurs.
147
148
  # This is our best guess: first try to close the attribute, then the tag.
148
- cattr_accessor :streaming_completion_on_exception
149
- @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>)
149
+ cattr_accessor :streaming_completion_on_exception, default: %("><script>window.location = "/500.html"</script></html>)
150
150
 
151
151
  # Specify whether rendering within namespaced controllers should prefix
152
152
  # the partial paths for ActiveModel objects with the namespace.
153
153
  # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb)
154
- cattr_accessor :prefix_partial_path_with_controller_namespace
155
- @@prefix_partial_path_with_controller_namespace = true
154
+ cattr_accessor :prefix_partial_path_with_controller_namespace, default: true
156
155
 
157
156
  # Specify default_formats that can be rendered.
158
157
  cattr_accessor :default_formats
159
158
 
160
159
  # Specify whether an error should be raised for missing translations
161
- cattr_accessor :raise_on_missing_translations
162
- @@raise_on_missing_translations = false
160
+ cattr_accessor :raise_on_missing_translations, default: false
163
161
 
164
162
  # Specify whether submit_tag should automatically disable on click
165
- cattr_accessor :automatically_disable_submit_tag
166
- @@automatically_disable_submit_tag = true
163
+ cattr_accessor :automatically_disable_submit_tag, default: true
167
164
 
168
165
  class_attribute :_routes
169
166
  class_attribute :logger
@@ -207,6 +204,7 @@ module ActionView #:nodoc:
207
204
  @view_renderer = ActionView::Renderer.new(lookup_context)
208
205
  end
209
206
 
207
+ @cache_hit = {}
210
208
  assign(assigns)
211
209
  assign_controller(controller)
212
210
  _prepare_context
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module CompiledTemplates #:nodoc:
3
5
  # holds compiled template code
@@ -17,7 +19,6 @@ module ActionView
17
19
  attr_accessor :output_buffer, :view_flow
18
20
 
19
21
  # Prepares the context by setting the appropriate instance variables.
20
- # :api: plugin
21
22
  def _prepare_context
22
23
  @view_flow = OutputFlow.new
23
24
  @output_buffer = nil
@@ -27,7 +28,6 @@ module ActionView
27
28
  # Encapsulates the interaction with the view flow so it
28
29
  # returns the correct buffer on +yield+. This is usually
29
30
  # overwritten by helpers to add more behavior.
30
- # :api: plugin
31
31
  def _layout_for(name = nil)
32
32
  name ||= :layout
33
33
  view_flow.get(name).html_safe
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent/map"
2
4
  require "action_view/path_set"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent/map"
2
4
  require "action_view/dependency_tracker"
3
5
  require "monitor"
@@ -68,13 +70,11 @@ module ActionView
68
70
  end
69
71
 
70
72
  private
71
- def find_template(finder, *args)
73
+ def find_template(finder, name, prefixes, partial, keys)
72
74
  finder.disable_cache do
73
- if format = finder.rendered_format
74
- finder.find_all(*args, formats: [format]).first || finder.find_all(*args).first
75
- else
76
- finder.find_all(*args).first
77
- end
75
+ format = finder.rendered_format
76
+ result = finder.find_all(name, prefixes, partial, keys, formats: [format]).first if format
77
+ result || finder.find_all(name, prefixes, partial, keys).first
78
78
  end
79
79
  end
80
80
  end
@@ -95,7 +95,7 @@ module ActionView
95
95
  end
96
96
 
97
97
  def digest(finder, stack = [])
98
- Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
98
+ ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
99
99
  end
100
100
 
101
101
  def dependency_digest(finder, stack)
@@ -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
@@ -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
@@ -6,9 +8,9 @@ module ActionView
6
8
 
7
9
  module VERSION
8
10
  MAJOR = 5
9
- MINOR = 1
10
- TINY = 7
11
- PRE = nil
11
+ MINOR = 2
12
+ TINY = 4
13
+ PRE = "3"
12
14
 
13
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
16
  end
@@ -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
@@ -44,6 +47,7 @@ module ActionView #:nodoc:
44
47
  include CacheHelper
45
48
  include CaptureHelper
46
49
  include ControllerHelper
50
+ include CspHelper
47
51
  include CsrfHelper
48
52
  include DateHelper
49
53
  include DebugHelper
@@ -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, *)
@@ -41,6 +43,10 @@ module ActionView
41
43
  object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
42
44
  end
43
45
 
46
+ def select_markup_helper?(type)
47
+ ["optgroup", "option"].include?(type)
48
+ end
49
+
44
50
  def tag_generate_errors?(options)
45
51
  options["type"] != "hidden"
46
52
  end
@@ -1,5 +1,9 @@
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"
6
+ require "active_support/core_ext/object/try"
3
7
  require "action_view/helpers/asset_url_helper"
4
8
  require "action_view/helpers/tag_helper"
5
9
 
@@ -11,7 +15,7 @@ module ActionView
11
15
  # the assets exist before linking to them:
12
16
  #
13
17
  # image_tag("rails.png")
14
- # # => <img alt="Rails" src="/assets/rails.png" />
18
+ # # => <img src="/assets/rails.png" />
15
19
  # stylesheet_link_tag("application")
16
20
  # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
17
21
  module AssetTagHelper
@@ -35,19 +39,24 @@ module ActionView
35
39
  # When the Asset Pipeline is enabled, you can pass the name of your manifest as
36
40
  # source, and include other JavaScript or CoffeeScript files inside the manifest.
37
41
  #
42
+ # If the server supports Early Hints header links for these assets will be
43
+ # automatically pushed.
44
+ #
38
45
  # ==== Options
39
46
  #
40
47
  # When the last parameter is a hash you can add HTML attributes using that
41
48
  # parameter. The following options are supported:
42
49
  #
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
50
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
51
+ # already exists. This only applies for relative URLs.
52
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
53
+ # applies when a relative URL and +host+ options are provided.
54
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
48
55
  # that path.
49
56
  # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
50
57
  # when it is set to true.
58
+ # * <tt>:nonce<tt> - When set to true, adds an automatic nonce value if
59
+ # you have Content Security Policy enabled.
51
60
  #
52
61
  # ==== Examples
53
62
  #
@@ -72,15 +81,29 @@ module ActionView
72
81
  #
73
82
  # javascript_include_tag "http://www.example.com/xmlhr.js"
74
83
  # # => <script src="http://www.example.com/xmlhr.js"></script>
84
+ #
85
+ # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
86
+ # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
75
87
  def javascript_include_tag(*sources)
76
88
  options = sources.extract_options!.stringify_keys
77
89
  path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
78
- sources.uniq.map { |source|
90
+ early_hints_links = []
91
+
92
+ sources_tags = sources.uniq.map { |source|
93
+ href = path_to_javascript(source, path_options)
94
+ early_hints_links << "<#{href}>; rel=preload; as=script"
79
95
  tag_options = {
80
- "src" => path_to_javascript(source, path_options)
96
+ "src" => href
81
97
  }.merge!(options)
98
+ if tag_options["nonce"] == true
99
+ tag_options["nonce"] = content_security_policy_nonce
100
+ end
82
101
  content_tag("script".freeze, "", tag_options)
83
102
  }.join("\n").html_safe
103
+
104
+ request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
105
+
106
+ sources_tags
84
107
  end
85
108
 
86
109
  # Returns a stylesheet link tag for the sources specified as arguments. If
@@ -90,6 +113,9 @@ module ActionView
90
113
  # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
91
114
  # apply to all media types.
92
115
  #
116
+ # If the server supports Early Hints header links for these assets will be
117
+ # automatically pushed.
118
+ #
93
119
  # stylesheet_link_tag "style"
94
120
  # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
95
121
  #
@@ -111,20 +137,28 @@ module ActionView
111
137
  def stylesheet_link_tag(*sources)
112
138
  options = sources.extract_options!.stringify_keys
113
139
  path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
114
- sources.uniq.map { |source|
140
+ early_hints_links = []
141
+
142
+ sources_tags = sources.uniq.map { |source|
143
+ href = path_to_stylesheet(source, path_options)
144
+ early_hints_links << "<#{href}>; rel=preload; as=style"
115
145
  tag_options = {
116
146
  "rel" => "stylesheet",
117
147
  "media" => "screen",
118
- "href" => path_to_stylesheet(source, path_options)
148
+ "href" => href
119
149
  }.merge!(options)
120
150
  tag(:link, tag_options)
121
151
  }.join("\n").html_safe
152
+
153
+ request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request
154
+
155
+ sources_tags
122
156
  end
123
157
 
124
158
  # 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+.
159
+ # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
160
+ # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
161
+ # using the +url_options+. You can modify the LINK tag itself in +tag_options+.
128
162
  #
129
163
  # ==== Options
130
164
  #
@@ -138,6 +172,8 @@ module ActionView
138
172
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
139
173
  # auto_discovery_link_tag(:atom)
140
174
  # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
175
+ # auto_discovery_link_tag(:json)
176
+ # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
141
177
  # auto_discovery_link_tag(:rss, {action: "feed"})
142
178
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
143
179
  # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
@@ -147,8 +183,8 @@ module ActionView
147
183
  # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
148
184
  # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
149
185
  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.")
186
+ if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
187
+ raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
152
188
  end
153
189
 
154
190
  tag(
@@ -195,44 +231,124 @@ module ActionView
195
231
  }.merge!(options.symbolize_keys))
196
232
  end
197
233
 
234
+ # Returns a link tag that browsers can use to preload the +source+.
235
+ # The +source+ can be the path of a resource managed by asset pipeline,
236
+ # a full path, or an URI.
237
+ #
238
+ # ==== Options
239
+ #
240
+ # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
241
+ # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
242
+ # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
243
+ # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
244
+ #
245
+ # ==== Examples
246
+ #
247
+ # preload_link_tag("custom_theme.css")
248
+ # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
249
+ #
250
+ # preload_link_tag("/videos/video.webm")
251
+ # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
252
+ #
253
+ # preload_link_tag(post_path(format: :json), as: "fetch")
254
+ # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
255
+ #
256
+ # preload_link_tag("worker.js", as: "worker")
257
+ # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
258
+ #
259
+ # preload_link_tag("//example.com/font.woff2")
260
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
261
+ #
262
+ # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
263
+ # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
264
+ #
265
+ # preload_link_tag("/media/audio.ogg", nopush: true)
266
+ # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
267
+ #
268
+ def preload_link_tag(source, options = {})
269
+ href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
270
+ extname = File.extname(source).downcase.delete(".")
271
+ mime_type = options.delete(:type) || Template::Types[extname].try(:to_s)
272
+ as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
273
+ crossorigin = options.delete(:crossorigin)
274
+ crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
275
+ nopush = options.delete(:nopush) || false
276
+
277
+ link_tag = tag.link({
278
+ rel: "preload",
279
+ href: href,
280
+ as: as_type,
281
+ type: mime_type,
282
+ crossorigin: crossorigin
283
+ }.merge!(options.symbolize_keys))
284
+
285
+ early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
286
+ early_hints_link += "; type=#{mime_type}" if mime_type
287
+ early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
288
+ early_hints_link += "; nopush" if nopush
289
+
290
+ request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request
291
+
292
+ link_tag
293
+ end
294
+
198
295
  # Returns an HTML image tag for the +source+. The +source+ can be a full
199
- # path or a file.
296
+ # path, a file, or an Active Storage attachment.
200
297
  #
201
298
  # ==== Options
202
299
  #
203
300
  # You can add HTML attributes using the +options+. The +options+ supports
204
- # two additional keys for convenience and conformance:
301
+ # additional keys for convenience and conformance:
205
302
  #
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
303
  # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
209
304
  # width="30" and height="45", and "50" becomes width="50" and height="50".
210
305
  # <tt>:size</tt> will be ignored if the value is not in the correct format.
306
+ # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
307
+ # pairs, each image path will be expanded before the list is formatted as a string.
211
308
  #
212
309
  # ==== Examples
213
310
  #
311
+ # Assets (images that are part of your app):
312
+ #
214
313
  # image_tag("icon")
215
- # # => <img alt="Icon" src="/assets/icon" />
314
+ # # => <img src="/assets/icon" />
216
315
  # image_tag("icon.png")
217
- # # => <img alt="Icon" src="/assets/icon.png" />
316
+ # # => <img src="/assets/icon.png" />
218
317
  # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
219
318
  # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
220
319
  # image_tag("/icons/icon.gif", size: "16")
221
- # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
320
+ # # => <img src="/icons/icon.gif" width="16" height="16" />
222
321
  # image_tag("/icons/icon.gif", height: '32', width: '32')
223
- # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
322
+ # # => <img height="32" src="/icons/icon.gif" width="32" />
224
323
  # image_tag("/icons/icon.gif", class: "menu_icon")
225
- # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
324
+ # # => <img class="menu_icon" src="/icons/icon.gif" />
226
325
  # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
227
326
  # # => <img data-title="Rails Application" src="/icons/icon.gif" />
327
+ # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
328
+ # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
329
+ # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
330
+ # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
331
+ #
332
+ # Active Storage (images that are uploaded by the users of your app):
333
+ #
334
+ # image_tag(user.avatar)
335
+ # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
336
+ # image_tag(user.avatar.variant(resize: "100x100"))
337
+ # # => <img src="/rails/active_storage/variants/.../tiger.jpg" />
338
+ # image_tag(user.avatar.variant(resize: "100x100"), size: '100')
339
+ # # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" />
228
340
  def image_tag(source, options = {})
229
341
  options = options.symbolize_keys
230
342
  check_for_image_tag_errors(options)
343
+ skip_pipeline = options.delete(:skip_pipeline)
231
344
 
232
- src = options[:src] = path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
345
+ options[:src] = resolve_image_source(source, skip_pipeline)
233
346
 
234
- unless src.start_with?("cid:") || src.start_with?("data:") || src.blank?
235
- options[:alt] = options.fetch(:alt) { image_alt(src) }
347
+ if options[:srcset] && !options[:srcset].is_a?(String)
348
+ options[:srcset] = options[:srcset].map do |src_path, size|
349
+ src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
350
+ "#{src_path} #{size}"
351
+ end.join(", ")
236
352
  end
237
353
 
238
354
  options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
@@ -257,18 +373,21 @@ module ActionView
257
373
  # image_alt('underscored_file_name.png')
258
374
  # # => Underscored file name
259
375
  def image_alt(src)
376
+ ActiveSupport::Deprecation.warn("image_alt is deprecated and will be removed from Rails 6.0. You must explicitly set alt text on images.")
377
+
260
378
  File.basename(src, ".*".freeze).sub(/-[[:xdigit:]]{32,64}\z/, "".freeze).tr("-_".freeze, " ".freeze).capitalize
261
379
  end
262
380
 
263
381
  # Returns an HTML video tag for the +sources+. If +sources+ is a string,
264
382
  # a single video tag will be returned. If +sources+ is an array, a video
265
383
  # 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
384
+ # +sources+ can be full paths or files that exist in your public videos
267
385
  # directory.
268
386
  #
269
387
  # ==== Options
270
- # You can add HTML attributes using the +options+. The +options+ supports
271
- # two additional keys for convenience and conformance:
388
+ #
389
+ # When the last parameter is a hash you can add HTML attributes using that
390
+ # parameter. The following options are supported:
272
391
  #
273
392
  # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
274
393
  # before the video loads. The path is calculated like the +src+ of +image_tag+.
@@ -285,7 +404,7 @@ module ActionView
285
404
  # video_tag("trailer.ogg")
286
405
  # # => <video src="/videos/trailer.ogg"></video>
287
406
  # video_tag("trailer.ogg", controls: true, preload: 'none')
288
- # # => <video preload="none" controls="controls" src="/videos/trailer.ogg" ></video>
407
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
289
408
  # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
290
409
  # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
291
410
  # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
@@ -312,9 +431,14 @@ module ActionView
312
431
  end
313
432
  end
314
433
 
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.
434
+ # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
435
+ # a single audio tag will be returned. If +sources+ is an array, an audio
436
+ # tag with nested source tags for each source will be returned. The
437
+ # +sources+ can be full paths or files that exist in your public audios
438
+ # directory.
439
+ #
440
+ # When the last parameter is a hash you can add HTML attributes using that
441
+ # parameter.
318
442
  #
319
443
  # audio_tag("sound")
320
444
  # # => <audio src="/audios/sound"></audio>
@@ -346,6 +470,16 @@ module ActionView
346
470
  end
347
471
  end
348
472
 
473
+ def resolve_image_source(source, skip_pipeline)
474
+ if source.is_a?(Symbol) || source.is_a?(String)
475
+ path_to_image(source, skip_pipeline: skip_pipeline)
476
+ else
477
+ polymorphic_url(source)
478
+ end
479
+ rescue NoMethodError => e
480
+ raise ArgumentError, "Can't resolve image into URL: #{e}"
481
+ end
482
+
349
483
  def extract_dimensions(size)
350
484
  size = size.to_s
351
485
  if /\A\d+x\d+\z/.match?(size)
@@ -360,6 +494,18 @@ module ActionView
360
494
  raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
361
495
  end
362
496
  end
497
+
498
+ def resolve_link_as(extname, mime_type)
499
+ if extname == "js"
500
+ "script"
501
+ elsif extname == "css"
502
+ "style"
503
+ elsif extname == "vtt"
504
+ "track"
505
+ elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
506
+ type
507
+ end
508
+ end
363
509
  end
364
510
  end
365
511
  end