actionview 7.0.8.6 → 7.1.0.beta1

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +235 -387
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/app/assets/javascripts/rails-ujs.esm.js +668 -0
  6. data/app/assets/javascripts/rails-ujs.js +606 -0
  7. data/lib/action_view/base.rb +28 -7
  8. data/lib/action_view/buffers.rb +106 -8
  9. data/lib/action_view/cache_expiry.rb +40 -43
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/deprecator.rb +7 -0
  12. data/lib/action_view/digestor.rb +1 -1
  13. data/lib/action_view/gem_version.rb +4 -4
  14. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  15. data/lib/action_view/helpers/asset_tag_helper.rb +130 -46
  16. data/lib/action_view/helpers/asset_url_helper.rb +6 -5
  17. data/lib/action_view/helpers/atom_feed_helper.rb +5 -5
  18. data/lib/action_view/helpers/cache_helper.rb +3 -9
  19. data/lib/action_view/helpers/capture_helper.rb +24 -10
  20. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  21. data/lib/action_view/helpers/controller_helper.rb +6 -0
  22. data/lib/action_view/helpers/csp_helper.rb +2 -2
  23. data/lib/action_view/helpers/csrf_helper.rb +2 -2
  24. data/lib/action_view/helpers/date_helper.rb +17 -19
  25. data/lib/action_view/helpers/debug_helper.rb +3 -3
  26. data/lib/action_view/helpers/form_helper.rb +43 -18
  27. data/lib/action_view/helpers/form_options_helper.rb +2 -1
  28. data/lib/action_view/helpers/form_tag_helper.rb +43 -9
  29. data/lib/action_view/helpers/javascript_helper.rb +1 -0
  30. data/lib/action_view/helpers/number_helper.rb +2 -1
  31. data/lib/action_view/helpers/output_safety_helper.rb +2 -2
  32. data/lib/action_view/helpers/rendering_helper.rb +1 -1
  33. data/lib/action_view/helpers/sanitize_helper.rb +33 -14
  34. data/lib/action_view/helpers/tag_helper.rb +5 -27
  35. data/lib/action_view/helpers/tags/base.rb +11 -52
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  37. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  38. data/lib/action_view/helpers/tags/collection_select.rb +3 -0
  39. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  40. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  41. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  42. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  43. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  44. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/select.rb +3 -0
  46. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  47. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  48. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  49. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  50. data/lib/action_view/helpers/tags/weekday_select.rb +3 -0
  51. data/lib/action_view/helpers/tags.rb +2 -0
  52. data/lib/action_view/helpers/text_helper.rb +32 -16
  53. data/lib/action_view/helpers/translation_helper.rb +3 -3
  54. data/lib/action_view/helpers/url_helper.rb +41 -14
  55. data/lib/action_view/helpers.rb +2 -0
  56. data/lib/action_view/layouts.rb +4 -2
  57. data/lib/action_view/log_subscriber.rb +49 -32
  58. data/lib/action_view/lookup_context.rb +29 -13
  59. data/lib/action_view/path_registry.rb +57 -0
  60. data/lib/action_view/path_set.rb +13 -14
  61. data/lib/action_view/railtie.rb +26 -3
  62. data/lib/action_view/record_identifier.rb +15 -8
  63. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  64. data/lib/action_view/renderer/collection_renderer.rb +9 -1
  65. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +2 -1
  66. data/lib/action_view/renderer/partial_renderer.rb +2 -1
  67. data/lib/action_view/renderer/renderer.rb +2 -0
  68. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -2
  69. data/lib/action_view/renderer/template_renderer.rb +3 -2
  70. data/lib/action_view/rendering.rb +22 -4
  71. data/lib/action_view/ripper_ast_parser.rb +6 -6
  72. data/lib/action_view/template/error.rb +14 -1
  73. data/lib/action_view/template/handlers/builder.rb +4 -4
  74. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  75. data/lib/action_view/template/handlers/erb.rb +73 -1
  76. data/lib/action_view/template/handlers.rb +1 -1
  77. data/lib/action_view/template/html.rb +1 -1
  78. data/lib/action_view/template/raw_file.rb +1 -1
  79. data/lib/action_view/template/renderable.rb +1 -1
  80. data/lib/action_view/template/resolver.rb +10 -2
  81. data/lib/action_view/template/text.rb +1 -1
  82. data/lib/action_view/template/types.rb +25 -34
  83. data/lib/action_view/template.rb +179 -52
  84. data/lib/action_view/template_path.rb +2 -0
  85. data/lib/action_view/test_case.rb +8 -5
  86. data/lib/action_view/unbound_template.rb +15 -5
  87. data/lib/action_view/version.rb +1 -1
  88. data/lib/action_view/view_paths.rb +15 -24
  89. data/lib/action_view.rb +4 -1
  90. metadata +29 -29
@@ -18,24 +18,91 @@ module ActionView
18
18
  # sbuf << 5
19
19
  # puts sbuf # => "hello\u0005"
20
20
  #
21
- class OutputBuffer < ActiveSupport::SafeBuffer # :nodoc:
22
- def initialize(*)
23
- super
24
- encode!
21
+ class OutputBuffer # :nodoc:
22
+ def initialize(buffer = "")
23
+ @raw_buffer = String.new(buffer)
24
+ @raw_buffer.encode!
25
+ end
26
+
27
+ delegate :length, :empty?, :blank?, :encoding, :encode!, :force_encoding, to: :@raw_buffer
28
+
29
+ def to_s
30
+ @raw_buffer.html_safe
31
+ end
32
+ alias_method :html_safe, :to_s
33
+
34
+ def to_str
35
+ @raw_buffer.dup
36
+ end
37
+
38
+ def html_safe?
39
+ true
25
40
  end
26
41
 
27
42
  def <<(value)
28
- return self if value.nil?
29
- super(value.to_s)
43
+ unless value.nil?
44
+ value = value.to_s
45
+ @raw_buffer << if value.html_safe?
46
+ value
47
+ else
48
+ CGI.escapeHTML(value)
49
+ end
50
+ end
51
+ self
30
52
  end
53
+ alias :concat :<<
31
54
  alias :append= :<<
32
55
 
56
+ def safe_concat(value)
57
+ @raw_buffer << value
58
+ self
59
+ end
60
+ alias :safe_append= :safe_concat
61
+
33
62
  def safe_expr_append=(val)
34
63
  return self if val.nil?
35
- safe_concat val.to_s
64
+ @raw_buffer << val.to_s
65
+ self
36
66
  end
37
67
 
38
- alias :safe_append= :safe_concat
68
+ def initialize_copy(other)
69
+ @raw_buffer = other.to_str
70
+ end
71
+
72
+ def capture(*args)
73
+ new_buffer = +""
74
+ old_buffer, @raw_buffer = @raw_buffer, new_buffer
75
+ yield(*args)
76
+ new_buffer.html_safe
77
+ ensure
78
+ @raw_buffer = old_buffer
79
+ end
80
+
81
+ def ==(other)
82
+ other.class == self.class && @raw_buffer == other.to_str
83
+ end
84
+
85
+ def raw
86
+ RawOutputBuffer.new(self)
87
+ end
88
+
89
+ attr_reader :raw_buffer
90
+ end
91
+
92
+ class RawOutputBuffer # :nodoc:
93
+ def initialize(buffer)
94
+ @buffer = buffer
95
+ end
96
+
97
+ def <<(value)
98
+ unless value.nil?
99
+ @buffer.raw_buffer << value.to_s
100
+ end
101
+ end
102
+
103
+ def raw
104
+ self
105
+ end
39
106
  end
40
107
 
41
108
  class StreamingBuffer # :nodoc:
@@ -56,6 +123,15 @@ module ActionView
56
123
  end
57
124
  alias :safe_append= :safe_concat
58
125
 
126
+ def capture
127
+ buffer = +""
128
+ old_block, @block = @block, ->(value) { buffer << value }
129
+ yield
130
+ buffer.html_safe
131
+ ensure
132
+ @block = old_block
133
+ end
134
+
59
135
  def html_safe?
60
136
  true
61
137
  end
@@ -63,5 +139,27 @@ module ActionView
63
139
  def html_safe
64
140
  self
65
141
  end
142
+
143
+ def raw
144
+ RawStreamingBuffer.new(self)
145
+ end
146
+
147
+ attr_reader :block
148
+ end
149
+
150
+ class RawStreamingBuffer # :nodoc:
151
+ def initialize(buffer)
152
+ @buffer = buffer
153
+ end
154
+
155
+ def <<(value)
156
+ unless value.nil?
157
+ @buffer.block.call(value.to_s)
158
+ end
159
+ end
160
+
161
+ def raw
162
+ self
163
+ end
66
164
  end
67
165
  end
@@ -1,65 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
- class CacheExpiry
5
- class Executor
6
- def initialize(watcher:)
7
- @execution_lock = Concurrent::ReentrantReadWriteLock.new
8
- @cache_expiry = ViewModificationWatcher.new(watcher: watcher) do
9
- clear_cache
10
- end
11
- end
4
+ module CacheExpiry # :nodoc: all
5
+ class ViewReloader
6
+ def initialize(watcher:, &block)
7
+ @mutex = Mutex.new
8
+ @watcher_class = watcher
9
+ @watched_dirs = nil
10
+ @watcher = nil
11
+ @previous_change = false
12
12
 
13
- def run
14
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
15
- @cache_expiry.execute_if_updated
16
- @execution_lock.acquire_read_lock
17
- end
13
+ rebuild_watcher
14
+
15
+ ActionView::PathRegistry.file_system_resolver_hooks << method(:rebuild_watcher)
18
16
  end
19
17
 
20
- def complete(_)
21
- @execution_lock.release_read_lock
18
+ def updated?
19
+ @previous_change || @watcher.updated?
22
20
  end
23
21
 
24
- private
25
- def clear_cache
26
- @execution_lock.with_write_lock do
27
- ActionView::LookupContext::DetailsKey.clear
28
- end
22
+ def execute
23
+ watcher = nil
24
+ @mutex.synchronize do
25
+ @previous_change = false
26
+ watcher = @watcher
29
27
  end
30
- end
31
-
32
- class ViewModificationWatcher
33
- def initialize(watcher:, &block)
34
- @watched_dirs = nil
35
- @watcher_class = watcher
36
- @watcher = nil
37
- @mutex = Mutex.new
38
- @block = block
28
+ watcher.execute
39
29
  end
40
30
 
41
- def execute_if_updated
42
- @mutex.synchronize do
43
- watched_dirs = dirs_to_watch
44
- return if watched_dirs.empty?
31
+ private
32
+ def reload!
33
+ ActionView::LookupContext::DetailsKey.clear
34
+ end
45
35
 
46
- if watched_dirs != @watched_dirs
47
- @watched_dirs = watched_dirs
48
- @watcher = @watcher_class.new([], watched_dirs, &@block)
49
- @watcher.execute
50
- else
51
- @watcher.execute_if_updated
36
+ def rebuild_watcher
37
+ @mutex.synchronize do
38
+ old_watcher = @watcher
39
+
40
+ if @watched_dirs != dirs_to_watch
41
+ @watched_dirs = dirs_to_watch
42
+ new_watcher = @watcher_class.new([], @watched_dirs) do
43
+ reload!
44
+ end
45
+ @watcher = new_watcher
46
+
47
+ # We must check the old watcher after initializing the new one to
48
+ # ensure we don't miss any events
49
+ @previous_change ||= old_watcher&.updated?
50
+ end
52
51
  end
53
52
  end
54
- end
55
53
 
56
- private
57
54
  def dirs_to_watch
58
- all_view_paths.grep(FileSystemResolver).map!(&:path).tap(&:uniq!).sort!
55
+ all_view_paths.uniq.sort
59
56
  end
60
57
 
61
58
  def all_view_paths
62
- ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
59
+ ActionView::PathRegistry.all_file_system_resolvers.map(&:path)
63
60
  end
64
61
  end
65
62
  end
@@ -17,7 +17,7 @@ module ActionView
17
17
  # Prepares the context by setting the appropriate instance variables.
18
18
  def _prepare_context
19
19
  @view_flow = OutputFlow.new
20
- @output_buffer = nil
20
+ @output_buffer = ActionView::OutputBuffer.new
21
21
  @virtual_path = nil
22
22
  end
23
23
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -11,7 +11,7 @@ module ActionView
11
11
  #
12
12
  # * <tt>name</tt> - Template name
13
13
  # * <tt>format</tt> - Template format
14
- # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
14
+ # * +finder+ - An instance of ActionView::LookupContext
15
15
  # * <tt>dependencies</tt> - An array of dependent views
16
16
  def digest(name:, format: nil, finder:, dependencies: nil)
17
17
  if dependencies.nil? || dependencies.empty?
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
- # Returns the currently loaded version of Action View as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of Action View as a +Gem::Version+.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 7
11
- MINOR = 0
12
- TINY = 8
13
- PRE = "6"
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -4,11 +4,11 @@ require "active_support/core_ext/module/attribute_accessors"
4
4
  require "active_support/core_ext/enumerable"
5
5
 
6
6
  module ActionView
7
- # = Active Model Helpers
8
7
  module Helpers # :nodoc:
9
8
  module ActiveModelHelper
10
9
  end
11
10
 
11
+ # = Active \Model Instance Tag \Helpers
12
12
  module ActiveModelInstanceTag
13
13
  def object
14
14
  @active_model_object ||= begin
@@ -7,8 +7,9 @@ require "action_view/helpers/asset_url_helper"
7
7
  require "action_view/helpers/tag_helper"
8
8
 
9
9
  module ActionView
10
- # = Action View Asset Tag Helpers
11
10
  module Helpers # :nodoc:
11
+ # = Action View Asset Tag \Helpers
12
+ #
12
13
  # This module provides methods for generating HTML that links views to assets such
13
14
  # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
14
15
  # the assets exist before linking to them:
@@ -41,13 +42,14 @@ module ActionView
41
42
  # When the Asset Pipeline is enabled, you can pass the name of your manifest as
42
43
  # source, and include other JavaScript or CoffeeScript files inside the manifest.
43
44
  #
44
- # If the server supports Early Hints, header links for these assets will be
45
- # automatically pushed.
45
+ # If the server supports HTTP Early Hints, and the +defer+ option is not
46
+ # enabled, \Rails will push a <tt>103 Early Hints</tt> response that links
47
+ # to the assets.
46
48
  #
47
49
  # ==== Options
48
50
  #
49
51
  # When the last parameter is a hash you can add HTML attributes using that
50
- # parameter. The following options are supported:
52
+ # parameter. This includes but is not limited to the following options:
51
53
  #
52
54
  # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
53
55
  # already exists. This only applies for relative URLs.
@@ -59,6 +61,20 @@ module ActionView
59
61
  # when it is set to true.
60
62
  # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
61
63
  # you have Content Security Policy enabled.
64
+ # * <tt>:async</tt> - When set to +true+, adds the +async+ HTML
65
+ # attribute, allowing the script to be fetched in parallel to be parsed
66
+ # and evaluated as soon as possible.
67
+ # * <tt>:defer</tt> - When set to +true+, adds the +defer+ HTML
68
+ # attribute, which indicates to the browser that the script is meant to
69
+ # be executed after the document has been parsed. Additionally, prevents
70
+ # sending the Preload Links header.
71
+ #
72
+ # Any other specified options will be treated as HTML attributes for the
73
+ # +script+ tag.
74
+ #
75
+ # For more information regarding how the <tt>:async</tt> and <tt>:defer</tt>
76
+ # options affect the <tt><script></tt> tag, please refer to the
77
+ # {MDN docs}[https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script].
62
78
  #
63
79
  # ==== Examples
64
80
  #
@@ -86,10 +102,17 @@ module ActionView
86
102
  #
87
103
  # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
88
104
  # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
105
+ #
106
+ # javascript_include_tag "http://www.example.com/xmlhr.js", async: true
107
+ # # => <script src="http://www.example.com/xmlhr.js" async="async"></script>
108
+ #
109
+ # javascript_include_tag "http://www.example.com/xmlhr.js", defer: true
110
+ # # => <script src="http://www.example.com/xmlhr.js" defer="defer"></script>
89
111
  def javascript_include_tag(*sources)
90
112
  options = sources.extract_options!.stringify_keys
91
113
  path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
92
114
  preload_links = []
115
+ use_preload_links_header = options["preload_links_header"].nil? ? preload_links_header : options.delete("preload_links_header")
93
116
  nopush = options["nopush"].nil? ? true : options.delete("nopush")
94
117
  crossorigin = options.delete("crossorigin")
95
118
  crossorigin = "anonymous" if crossorigin == true
@@ -98,7 +121,7 @@ module ActionView
98
121
 
99
122
  sources_tags = sources.uniq.map { |source|
100
123
  href = path_to_javascript(source, path_options)
101
- if preload_links_header && !options["defer"] && href.present? && !href.start_with?("data:")
124
+ if use_preload_links_header && !options["defer"] && href.present? && !href.start_with?("data:")
102
125
  preload_link = "<#{href}>; rel=#{rel}; as=script"
103
126
  preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
104
127
  preload_link += "; integrity=#{integrity}" unless integrity.nil?
@@ -115,7 +138,7 @@ module ActionView
115
138
  content_tag("script", "", tag_options)
116
139
  }.join("\n").html_safe
117
140
 
118
- if preload_links_header
141
+ if use_preload_links_header
119
142
  send_preload_links_header(preload_links)
120
143
  end
121
144
 
@@ -130,8 +153,8 @@ module ActionView
130
153
  # set <tt>extname: false</tt> in the options.
131
154
  # You can modify the link attributes by passing a hash as the last argument.
132
155
  #
133
- # If the server supports Early Hints, header links for these assets will be
134
- # automatically pushed.
156
+ # If the server supports HTTP Early Hints, \Rails will push a <tt>103 Early
157
+ # Hints</tt> response that links to the assets.
135
158
  #
136
159
  # ==== Options
137
160
  #
@@ -170,6 +193,7 @@ module ActionView
170
193
  def stylesheet_link_tag(*sources)
171
194
  options = sources.extract_options!.stringify_keys
172
195
  path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
196
+ use_preload_links_header = options["preload_links_header"].nil? ? preload_links_header : options.delete("preload_links_header")
173
197
  preload_links = []
174
198
  crossorigin = options.delete("crossorigin")
175
199
  crossorigin = "anonymous" if crossorigin == true
@@ -178,7 +202,7 @@ module ActionView
178
202
 
179
203
  sources_tags = sources.uniq.map { |source|
180
204
  href = path_to_stylesheet(source, path_options)
181
- if preload_links_header && href.present? && !href.start_with?("data:")
205
+ if use_preload_links_header && href.present? && !href.start_with?("data:")
182
206
  preload_link = "<#{href}>; rel=preload; as=style"
183
207
  preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
184
208
  preload_link += "; integrity=#{integrity}" unless integrity.nil?
@@ -198,7 +222,7 @@ module ActionView
198
222
  tag(:link, tag_options)
199
223
  }.join("\n").html_safe
200
224
 
201
- if preload_links_header
225
+ if use_preload_links_header
202
226
  send_preload_links_header(preload_links)
203
227
  end
204
228
 
@@ -396,7 +420,7 @@ module ActionView
396
420
  check_for_image_tag_errors(options)
397
421
  skip_pipeline = options.delete(:skip_pipeline)
398
422
 
399
- options[:src] = resolve_image_source(source, skip_pipeline)
423
+ options[:src] = resolve_asset_source("image", source, skip_pipeline)
400
424
 
401
425
  if options[:srcset] && !options[:srcset].is_a?(String)
402
426
  options[:srcset] = options[:srcset].map do |src_path, size|
@@ -413,11 +437,72 @@ module ActionView
413
437
  tag("img", options)
414
438
  end
415
439
 
440
+ # Returns an HTML picture tag for the +sources+. If +sources+ is a string,
441
+ # a single picture tag will be returned. If +sources+ is an array, a picture
442
+ # tag with nested source tags for each source will be returned. The
443
+ # +sources+ can be full paths, files that exist in your public images
444
+ # directory, or Active Storage attachments. Since the picture tag requires
445
+ # an img tag, the last element you provide will be used for the img tag.
446
+ # For complete control over the picture tag, a block can be passed, which
447
+ # will populate the contents of the tag accordingly.
448
+ #
449
+ # ==== Options
450
+ #
451
+ # When the last parameter is a hash you can add HTML attributes using that
452
+ # parameter. Apart from all the HTML supported options, the following are supported:
453
+ #
454
+ # * <tt>:image</tt> - Hash of options that are passed directly to the +image_tag+ helper.
455
+ #
456
+ # ==== Examples
457
+ #
458
+ # picture_tag("picture.webp")
459
+ # # => <picture><img src="/images/picture.webp" /></picture>
460
+ # picture_tag("gold.png", :image => { :size => "20" })
461
+ # # => <picture><img height="20" src="/images/gold.png" width="20" /></picture>
462
+ # picture_tag("gold.png", :image => { :size => "45x70" })
463
+ # # => <picture><img height="70" src="/images/gold.png" width="45" /></picture>
464
+ # picture_tag("picture.webp", "picture.png")
465
+ # # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img src="/images/picture.png" /></picture>
466
+ # picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })
467
+ # # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
468
+ # picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })
469
+ # # => <picture><source srcset="/images/picture.webp" /><source srcset="/images/picture.png" /><img alt="Image" src="/images/picture.png" /></picture>
470
+ # picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }
471
+ # # => <picture class="my-class"><source srcset="/images/picture.webp" /><img alt="Image" src="/images/picture.png" /></picture>
472
+ # picture_tag { tag(:source, :srcset => image_path("picture-small.webp"), :media => "(min-width: 600px)") + tag(:source, :srcset => image_path("picture-big.webp")) + image_tag("picture.png", :alt => "Image") }
473
+ # # => <picture><source srcset="/images/picture-small.webp" media="(min-width: 600px)" /><source srcset="/images/picture-big.webp" /><img alt="Image" src="/images/picture.png" /></picture>
474
+ #
475
+ # Active Storage blobs (images that are uploaded by the users of your app):
476
+ #
477
+ # picture_tag(user.profile_picture)
478
+ # # => <picture><img src="/rails/active_storage/blobs/.../profile_picture.webp" /></picture>
479
+ def picture_tag(*sources, &block)
480
+ sources.flatten!
481
+ options = sources.extract_options!.symbolize_keys
482
+ image_options = options.delete(:image) || {}
483
+ skip_pipeline = options.delete(:skip_pipeline)
484
+
485
+ content_tag("picture", options) do
486
+ if block.present?
487
+ capture(&block).html_safe
488
+ elsif sources.size <= 1
489
+ image_tag(sources.last, image_options)
490
+ else
491
+ source_tags = sources.map do |source|
492
+ tag("source",
493
+ srcset: resolve_asset_source("image", source, skip_pipeline),
494
+ type: Template::Types[File.extname(source)[1..]]&.to_s)
495
+ end
496
+ safe_join(source_tags << image_tag(sources.last, image_options))
497
+ end
498
+ end
499
+ end
500
+
416
501
  # Returns an HTML video tag for the +sources+. If +sources+ is a string,
417
502
  # a single video tag will be returned. If +sources+ is an array, a video
418
503
  # tag with nested source tags for each source will be returned. The
419
- # +sources+ can be full paths or files that exist in your public videos
420
- # directory.
504
+ # +sources+ can be full paths, files that exist in your public videos
505
+ # directory, or Active Storage attachments.
421
506
  #
422
507
  # ==== Options
423
508
  #
@@ -456,6 +541,11 @@ module ActionView
456
541
  # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
457
542
  # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
458
543
  # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
544
+ #
545
+ # Active Storage blobs (videos that are uploaded by the users of your app):
546
+ #
547
+ # video_tag(user.intro_video)
548
+ # # => <video src="/rails/active_storage/blobs/.../intro_video.mp4"></video>
459
549
  def video_tag(*sources)
460
550
  options = sources.extract_options!.symbolize_keys
461
551
  public_poster_folder = options.delete(:poster_skip_pipeline)
@@ -469,8 +559,8 @@ module ActionView
469
559
  # Returns an HTML audio tag for the +sources+. If +sources+ is a string,
470
560
  # a single audio tag will be returned. If +sources+ is an array, an audio
471
561
  # 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.
562
+ # +sources+ can be full paths, files that exist in your public audios
563
+ # directory, or Active Storage attachments.
474
564
  #
475
565
  # When the last parameter is a hash you can add HTML attributes using that
476
566
  # parameter.
@@ -483,6 +573,11 @@ module ActionView
483
573
  # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
484
574
  # audio_tag("sound.wav", "sound.mid")
485
575
  # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
576
+ #
577
+ # Active Storage blobs (audios that are uploaded by the users of your app):
578
+ #
579
+ # audio_tag(user.name_pronunciation_audio)
580
+ # # => <audio src="/rails/active_storage/blobs/.../name_pronunciation_audio.mp3"></audio>
486
581
  def audio_tag(*sources)
487
582
  multiple_sources_tag_builder("audio", sources)
488
583
  end
@@ -497,29 +592,29 @@ module ActionView
497
592
 
498
593
  if sources.size > 1
499
594
  content_tag(type, options) do
500
- safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
595
+ safe_join sources.map { |source| tag("source", src: resolve_asset_source(type, source, skip_pipeline)) }
501
596
  end
502
597
  else
503
- options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
598
+ options[:src] = resolve_asset_source(type, sources.first, skip_pipeline)
504
599
  content_tag(type, nil, options)
505
600
  end
506
601
  end
507
602
 
508
- def resolve_image_source(source, skip_pipeline)
603
+ def resolve_asset_source(asset_type, source, skip_pipeline)
509
604
  if source.is_a?(Symbol) || source.is_a?(String)
510
- path_to_image(source, skip_pipeline: skip_pipeline)
605
+ path_to_asset(source, type: asset_type.to_sym, skip_pipeline: skip_pipeline)
511
606
  else
512
607
  polymorphic_url(source)
513
608
  end
514
609
  rescue NoMethodError => e
515
- raise ArgumentError, "Can't resolve image into URL: #{e}"
610
+ raise ArgumentError, "Can't resolve #{asset_type} into URL: #{e}"
516
611
  end
517
612
 
518
613
  def extract_dimensions(size)
519
614
  size = size.to_s
520
- if /\A(\d+|\d+.\d+)x(\d+|\d+.\d+)\z/.match?(size)
615
+ if /\A\d+(?:\.\d+)?x\d+(?:\.\d+)?\z/.match?(size)
521
616
  size.split("x")
522
- elsif /\A(\d+|\d+.\d+)\z/.match?(size)
617
+ elsif /\A\d+(?:\.\d+)?\z/.match?(size)
523
618
  [size, size]
524
619
  end
525
620
  end
@@ -540,40 +635,29 @@ module ActionView
540
635
  end
541
636
  end
542
637
 
543
- MAX_HEADER_SIZE = 8_000 # Some HTTP client and proxies have a 8kiB header limit
638
+ # Some HTTP client and proxies have a 4kiB header limit, but more importantly
639
+ # including preload links has diminishing returns so it's best to not go overboard
640
+ MAX_HEADER_SIZE = 1_000 # :nodoc:
641
+
544
642
  def send_preload_links_header(preload_links, max_header_size: MAX_HEADER_SIZE)
545
643
  return if preload_links.empty?
546
- return if respond_to?(:response) && response&.sending?
644
+ response_present = respond_to?(:response) && response
645
+ return if response_present && response.sending?
547
646
 
548
647
  if respond_to?(:request) && request
549
648
  request.send_early_hints("Link" => preload_links.join("\n"))
550
649
  end
551
650
 
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
-
651
+ if response_present
652
+ header = +response.headers["Link"].to_s
565
653
  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
654
+ break if header.bytesize + link.bytesize > max_header_size
655
+
656
+ if header.empty?
657
+ header << link
571
658
  else
572
- header << "\n"
573
- last_line_size = 0
659
+ header << "," << link
574
660
  end
575
- header << link
576
- last_line_size += link.bytesize
577
661
  end
578
662
 
579
663
  response.headers["Link"] = header