actionview 4.1.13 → 6.1.3.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 (124) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +181 -359
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +12 -6
  5. data/lib/action_view/base.rb +115 -43
  6. data/lib/action_view/buffers.rb +22 -4
  7. data/lib/action_view/cache_expiry.rb +52 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker.rb +61 -21
  10. data/lib/action_view/digestor.rb +89 -84
  11. data/lib/action_view/flows.rb +12 -13
  12. data/lib/action_view/gem_version.rb +6 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +16 -11
  14. data/lib/action_view/helpers/asset_tag_helper.rb +311 -105
  15. data/lib/action_view/helpers/asset_url_helper.rb +197 -80
  16. data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
  17. data/lib/action_view/helpers/cache_helper.rb +109 -45
  18. data/lib/action_view/helpers/capture_helper.rb +20 -22
  19. data/lib/action_view/helpers/controller_helper.rb +15 -4
  20. data/lib/action_view/helpers/csp_helper.rb +26 -0
  21. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  22. data/lib/action_view/helpers/date_helper.rb +245 -140
  23. data/lib/action_view/helpers/debug_helper.rb +14 -17
  24. data/lib/action_view/helpers/form_helper.rb +875 -148
  25. data/lib/action_view/helpers/form_options_helper.rb +128 -82
  26. data/lib/action_view/helpers/form_tag_helper.rb +253 -91
  27. data/lib/action_view/helpers/javascript_helper.rb +37 -15
  28. data/lib/action_view/helpers/number_helper.rb +100 -77
  29. data/lib/action_view/helpers/output_safety_helper.rb +42 -10
  30. data/lib/action_view/helpers/rendering_helper.rb +26 -15
  31. data/lib/action_view/helpers/sanitize_helper.rb +79 -164
  32. data/lib/action_view/helpers/tag_helper.rb +277 -64
  33. data/lib/action_view/helpers/tags/base.rb +143 -92
  34. data/lib/action_view/helpers/tags/check_box.rb +20 -19
  35. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -30
  37. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  38. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  39. data/lib/action_view/helpers/tags/collection_select.rb +4 -2
  40. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  41. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  42. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  43. data/lib/action_view/helpers/tags/datetime_field.rb +14 -5
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  47. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  48. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  49. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/label.rb +41 -22
  51. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  52. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  54. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  55. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  56. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/search_field.rb +3 -0
  58. data/lib/action_view/helpers/tags/select.rb +11 -10
  59. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/text_area.rb +7 -1
  61. data/lib/action_view/helpers/tags/text_field.rb +11 -7
  62. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  63. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  64. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  65. data/lib/action_view/helpers/tags/translator.rb +39 -0
  66. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  67. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  68. data/lib/action_view/helpers/tags.rb +4 -1
  69. data/lib/action_view/helpers/text_helper.rb +80 -45
  70. data/lib/action_view/helpers/translation_helper.rb +148 -67
  71. data/lib/action_view/helpers/url_helper.rb +289 -147
  72. data/lib/action_view/helpers.rb +5 -3
  73. data/lib/action_view/layouts.rb +68 -63
  74. data/lib/action_view/log_subscriber.rb +80 -13
  75. data/lib/action_view/lookup_context.rb +137 -92
  76. data/lib/action_view/model_naming.rb +4 -2
  77. data/lib/action_view/path_set.rb +30 -16
  78. data/lib/action_view/railtie.rb +62 -13
  79. data/lib/action_view/record_identifier.rb +53 -26
  80. data/lib/action_view/renderer/abstract_renderer.rb +152 -13
  81. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  82. data/lib/action_view/renderer/object_renderer.rb +34 -0
  83. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +61 -261
  85. data/lib/action_view/renderer/renderer.rb +67 -6
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
  87. data/lib/action_view/renderer/template_renderer.rb +83 -75
  88. data/lib/action_view/rendering.rb +73 -46
  89. data/lib/action_view/routing_url_for.rb +54 -17
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template/error.rb +44 -29
  92. data/lib/action_view/template/handlers/builder.rb +12 -13
  93. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  94. data/lib/action_view/template/handlers/erb.rb +23 -89
  95. data/lib/action_view/template/handlers/html.rb +11 -0
  96. data/lib/action_view/template/handlers/raw.rb +4 -4
  97. data/lib/action_view/template/handlers.rb +22 -9
  98. data/lib/action_view/template/html.rb +10 -11
  99. data/lib/action_view/template/inline.rb +22 -0
  100. data/lib/action_view/template/raw_file.rb +25 -0
  101. data/lib/action_view/template/renderable.rb +24 -0
  102. data/lib/action_view/template/resolver.rb +267 -181
  103. data/lib/action_view/template/sources/file.rb +17 -0
  104. data/lib/action_view/template/sources.rb +13 -0
  105. data/lib/action_view/template/text.rb +8 -10
  106. data/lib/action_view/template/types.rb +18 -18
  107. data/lib/action_view/template.rb +109 -99
  108. data/lib/action_view/test_case.rb +73 -53
  109. data/lib/action_view/testing/resolvers.rb +24 -33
  110. data/lib/action_view/unbound_template.rb +31 -0
  111. data/lib/action_view/version.rb +3 -1
  112. data/lib/action_view/view_paths.rb +74 -44
  113. data/lib/action_view.rb +14 -9
  114. data/lib/assets/compiled/rails-ujs.js +746 -0
  115. metadata +71 -26
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  117. data/lib/action_view/tasks/dependencies.rake +0 -23
  118. data/lib/action_view/vendor/html-scanner/html/document.rb +0 -68
  119. data/lib/action_view/vendor/html-scanner/html/node.rb +0 -532
  120. data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +0 -188
  121. data/lib/action_view/vendor/html-scanner/html/selector.rb +0 -830
  122. data/lib/action_view/vendor/html-scanner/html/tokenizer.rb +0 -107
  123. data/lib/action_view/vendor/html-scanner/html/version.rb +0 -11
  124. data/lib/action_view/vendor/html-scanner.rb +0 -20
@@ -1,122 +1,127 @@
1
- require 'thread_safe'
2
- require 'action_view/dependency_tracker'
3
- require 'monitor'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/dependency_tracker"
4
4
 
5
5
  module ActionView
6
6
  class Digestor
7
- cattr_reader(:cache)
8
- @@cache = ThreadSafe::Cache.new
9
- @@digest_monitor = Monitor.new
7
+ @@digest_mutex = Mutex.new
10
8
 
11
9
  class << self
12
10
  # Supported options:
13
11
  #
14
- # * <tt>name</tt> - Template name
15
- # * <tt>finder</tt> - An instance of ActionView::LookupContext
16
- # * <tt>dependencies</tt> - An array of dependent views
17
- # * <tt>partial</tt> - Specifies whether the template is a partial
18
- def digest(options)
19
- options.assert_valid_keys(:name, :finder, :dependencies, :partial)
20
-
21
- cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
12
+ # * <tt>name</tt> - Template name
13
+ # * <tt>format</tt> - Template format
14
+ # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
15
+ # * <tt>dependencies</tt> - An array of dependent views
16
+ def digest(name:, format: nil, finder:, dependencies: nil)
17
+ if dependencies.nil? || dependencies.empty?
18
+ cache_key = "#{name}.#{format}"
19
+ else
20
+ cache_key = [ name, format, dependencies ].flatten.compact.join(".")
21
+ end
22
22
 
23
23
  # this is a correctly done double-checked locking idiom
24
- # (ThreadSafe::Cache's lookups have volatile semantics)
25
- @@cache[cache_key] || @@digest_monitor.synchronize do
26
- @@cache.fetch(cache_key) do # re-check under lock
27
- compute_and_store_digest(cache_key, options)
24
+ # (Concurrent::Map's lookups have volatile semantics)
25
+ finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
26
+ finder.digest_cache.fetch(cache_key) do # re-check under lock
27
+ partial = name.include?("/_")
28
+ root = tree(name, finder, partial)
29
+ dependencies.each do |injected_dep|
30
+ root.children << Injected.new(injected_dep, nil, nil)
31
+ end if dependencies
32
+ finder.digest_cache[cache_key] = root.digest(finder)
28
33
  end
29
34
  end
30
35
  end
31
36
 
32
- private
33
- def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
34
- klass = if options[:partial] || options[:name].include?("/_")
35
- # Prevent re-entry or else recursive templates will blow the stack.
36
- # There is no need to worry about other threads seeing the +false+ value,
37
- # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
38
- pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
39
- PartialDigestor
40
- else
41
- Digestor
42
- end
37
+ def logger
38
+ ActionView::Base.logger || NullLogger
39
+ end
43
40
 
44
- digest = klass.new(options).digest
45
- # Store the actual digest if config.cache_template_loading is true
46
- @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
47
- digest
48
- ensure
49
- # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
50
- @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
51
- end
52
- end
41
+ # Create a dependency tree for template named +name+.
42
+ def tree(name, finder, partial = false, seen = {})
43
+ logical_name = name.gsub(%r|/_|, "/")
44
+ interpolated = name.include?("#")
53
45
 
54
- attr_reader :name, :finder, :options
46
+ if !interpolated && (template = find_template(finder, logical_name, [], partial, []))
47
+ if node = seen[template.identifier] # handle cycles in the tree
48
+ node
49
+ else
50
+ node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
55
51
 
56
- def initialize(options)
57
- @name, @finder = options.values_at(:name, :finder)
58
- @options = options.except(:name, :finder)
59
- end
52
+ deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
53
+ deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
54
+ node.children << tree(dep_file, finder, true, seen)
55
+ end
56
+ node
57
+ end
58
+ else
59
+ unless interpolated # Dynamic template partial names can never be tracked
60
+ logger.error " Couldn't find template for digesting: #{name}"
61
+ end
60
62
 
61
- def digest
62
- Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
63
- logger.try :info, " Cache digest for #{template.inspect}: #{digest}"
63
+ seen[name] ||= Missing.new(name, logical_name, nil)
64
+ end
64
65
  end
65
- rescue ActionView::MissingTemplate
66
- logger.try :error, " Couldn't find template for digesting: #{name}"
67
- ''
68
- end
69
66
 
70
- def dependencies
71
- DependencyTracker.find_dependencies(name, template)
72
- rescue ActionView::MissingTemplate
73
- [] # File doesn't exist, so no dependencies
67
+ private
68
+ def find_template(finder, name, prefixes, partial, keys)
69
+ finder.disable_cache do
70
+ finder.find_all(name, prefixes, partial, keys).first
71
+ end
72
+ end
74
73
  end
75
74
 
76
- def nested_dependencies
77
- dependencies.collect do |dependency|
78
- dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies
79
- dependencies.any? ? { dependency => dependencies } : dependency
80
- end
81
- end
75
+ class Node
76
+ attr_reader :name, :logical_name, :template, :children
82
77
 
83
- private
84
- def logger
85
- ActionView::Base.logger
78
+ def self.create(name, logical_name, template, partial)
79
+ klass = partial ? Partial : Node
80
+ klass.new(name, logical_name, template, [])
86
81
  end
87
82
 
88
- def logical_name
89
- name.gsub(%r|/_|, "/")
83
+ def initialize(name, logical_name, template, children = [])
84
+ @name = name
85
+ @logical_name = logical_name
86
+ @template = template
87
+ @children = children
90
88
  end
91
89
 
92
- def partial?
93
- false
90
+ def digest(finder, stack = [])
91
+ ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
94
92
  end
95
93
 
96
- def template
97
- @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
94
+ def dependency_digest(finder, stack)
95
+ children.map do |node|
96
+ if stack.include?(node)
97
+ false
98
+ else
99
+ finder.digest_cache[node.name] ||= begin
100
+ stack.push node
101
+ node.digest(finder, stack).tap { stack.pop }
102
+ end
103
+ end
104
+ end.join("-")
98
105
  end
99
106
 
100
- def source
101
- template.source
107
+ def to_dep_map
108
+ children.any? ? { name => children.map(&:to_dep_map) } : name
102
109
  end
110
+ end
103
111
 
104
- def dependency_digest
105
- template_digests = dependencies.collect do |template_name|
106
- Digestor.digest(name: template_name, finder: finder, partial: true)
107
- end
112
+ class Partial < Node; end
108
113
 
109
- (template_digests + injected_dependencies).join("-")
110
- end
114
+ class Missing < Node
115
+ def digest(finder, _ = []) "" end
116
+ end
111
117
 
112
- def injected_dependencies
113
- Array.wrap(options[:dependencies])
114
- end
115
- end
118
+ class Injected < Node
119
+ def digest(finder, _ = []) name end
120
+ end
116
121
 
117
- class PartialDigestor < Digestor # :nodoc:
118
- def partial?
119
- true
122
+ class NullLogger
123
+ def self.debug(_); end
124
+ def self.error(_); end
120
125
  end
121
126
  end
122
127
  end
@@ -1,11 +1,13 @@
1
- require 'active_support/core_ext/string/output_safety'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/output_safety"
2
4
 
3
5
  module ActionView
4
6
  class OutputFlow #:nodoc:
5
7
  attr_reader :content
6
8
 
7
9
  def initialize
8
- @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
10
+ @content = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new }
9
11
  end
10
12
 
11
13
  # Called by _layout_for to read stored values.
@@ -15,7 +17,7 @@ module ActionView
15
17
 
16
18
  # Called by each renderer object to set the layout contents.
17
19
  def set(key, value)
18
- @content[key] = value
20
+ @content[key] = ActiveSupport::SafeBuffer.new(value)
19
21
  end
20
22
 
21
23
  # Called by content_for
@@ -23,7 +25,6 @@ module ActionView
23
25
  @content[key] << value
24
26
  end
25
27
  alias_method :append!, :append
26
-
27
28
  end
28
29
 
29
30
  class StreamingFlow < OutputFlow #:nodoc:
@@ -37,9 +38,8 @@ module ActionView
37
38
  end
38
39
 
39
40
  # Try to get stored content. If the content
40
- # is not available and we are inside the layout
41
- # fiber, we set that we are waiting for the given
42
- # key and yield.
41
+ # is not available and we're inside the layout fiber,
42
+ # then it will begin waiting for the given key and yield.
43
43
  def get(key)
44
44
  return super if @content.key?(key)
45
45
 
@@ -60,17 +60,16 @@ module ActionView
60
60
  end
61
61
 
62
62
  # Appends the contents for the given key. This is called
63
- # by provides and resumes back to the fiber if it is
64
- # the key it is waiting for.
63
+ # by providing and resuming back to the fiber,
64
+ # if that's the key it's waiting for.
65
65
  def append!(key, value)
66
66
  super
67
67
  @fiber.resume if @waiting_for == key
68
68
  end
69
69
 
70
70
  private
71
-
72
- def inside_fiber?
73
- Fiber.current.object_id != @root
74
- end
71
+ def inside_fiber?
72
+ Fiber.current.object_id != @root
73
+ end
75
74
  end
76
75
  end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
- # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
4
+ # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
3
5
  def self.gem_version
4
6
  Gem::Version.new VERSION::STRING
5
7
  end
6
8
 
7
9
  module VERSION
8
- MAJOR = 4
10
+ MAJOR = 6
9
11
  MINOR = 1
10
- TINY = 13
11
- PRE = nil
12
+ TINY = 3
13
+ PRE = "1"
12
14
 
13
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
16
  end
@@ -1,9 +1,11 @@
1
- require 'active_support/core_ext/module/attribute_accessors'
2
- require 'active_support/core_ext/enumerable'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/attribute_accessors"
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,14 +38,17 @@ module ActionView
36
38
  end
37
39
 
38
40
  private
41
+ def object_has_errors?
42
+ object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
43
+ end
39
44
 
40
- def object_has_errors?
41
- object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
42
- end
45
+ def select_markup_helper?(type)
46
+ ["optgroup", "option"].include?(type)
47
+ end
43
48
 
44
- def tag_generate_errors?(options)
45
- options['type'] != 'hidden'
46
- end
49
+ def tag_generate_errors?(options)
50
+ options["type"] != "hidden"
51
+ end
47
52
  end
48
53
  end
49
54
  end
@@ -1,17 +1,20 @@
1
- require 'active_support/core_ext/array/extract_options'
2
- require 'active_support/core_ext/hash/keys'
3
- require 'action_view/helpers/asset_url_helper'
4
- require 'action_view/helpers/tag_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract_options"
4
+ require "active_support/core_ext/hash/keys"
5
+ require "active_support/core_ext/object/inclusion"
6
+ require "action_view/helpers/asset_url_helper"
7
+ require "action_view/helpers/tag_helper"
5
8
 
6
9
  module ActionView
7
10
  # = Action View Asset Tag Helpers
8
11
  module Helpers #:nodoc:
9
12
  # This module provides methods for generating HTML that links views to assets such
10
- # as images, javascripts, stylesheets, and feeds. These methods do not verify
13
+ # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
11
14
  # the assets exist before linking to them:
12
15
  #
13
16
  # image_tag("rails.png")
14
- # # => <img alt="Rails" src="/assets/rails.png" />
17
+ # # => <img src="/assets/rails.png" />
15
18
  # stylesheet_link_tag("application")
16
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,33 +40,84 @@ 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
+ #
46
+ # ==== Options
47
+ #
48
+ # When the last parameter is a hash you can add HTML attributes using that
49
+ # parameter. The following options are supported:
50
+ #
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
56
+ # that path.
57
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
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.
61
+ #
62
+ # ==== Examples
63
+ #
38
64
  # javascript_include_tag "xmlhr"
39
- # # => <script src="/assets/xmlhr.js?1284139606"></script>
65
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
66
+ #
67
+ # javascript_include_tag "xmlhr", host: "localhost", protocol: "https"
68
+ # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
40
69
  #
41
70
  # javascript_include_tag "template.jst", extname: false
42
- # # => <script src="/assets/template.jst?1284139606"></script>
71
+ # # => <script src="/assets/template.debug-1284139606.jst"></script>
43
72
  #
44
73
  # javascript_include_tag "xmlhr.js"
45
- # # => <script src="/assets/xmlhr.js?1284139606"></script>
74
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
46
75
  #
47
76
  # javascript_include_tag "common.javascript", "/elsewhere/cools"
48
- # # => <script src="/assets/common.javascript?1284139606"></script>
49
- # # <script src="/elsewhere/cools.js?1423139606"></script>
77
+ # # => <script src="/assets/common.javascript.debug-1284139606.js"></script>
78
+ # # <script src="/elsewhere/cools.debug-1284139606.js"></script>
50
79
  #
51
80
  # javascript_include_tag "http://www.example.com/xmlhr"
52
81
  # # => <script src="http://www.example.com/xmlhr"></script>
53
82
  #
54
83
  # javascript_include_tag "http://www.example.com/xmlhr.js"
55
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>
56
88
  def javascript_include_tag(*sources)
57
89
  options = sources.extract_options!.stringify_keys
58
- path_options = options.extract!('protocol', 'extname').symbolize_keys
59
- sources.uniq.map { |source|
90
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
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
60
106
  tag_options = {
61
- "src" => path_to_javascript(source, path_options)
107
+ "src" => href,
108
+ "crossorigin" => crossorigin
62
109
  }.merge!(options)
63
- content_tag(:script, "", tag_options)
110
+ if tag_options["nonce"] == true
111
+ tag_options["nonce"] = content_security_policy_nonce
112
+ end
113
+ content_tag("script", "", tag_options)
64
114
  }.join("\n").html_safe
115
+
116
+ if preload_links_header
117
+ send_preload_links_header(preload_links)
118
+ end
119
+
120
+ sources_tags
65
121
  end
66
122
 
67
123
  # Returns a stylesheet link tag for the sources specified as arguments. If
@@ -71,6 +127,9 @@ module ActionView
71
127
  # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
72
128
  # apply to all media types.
73
129
  #
130
+ # If the server supports Early Hints header links for these assets will be
131
+ # automatically pushed.
132
+ #
74
133
  # stylesheet_link_tag "style"
75
134
  # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
76
135
  #
@@ -91,22 +150,42 @@ module ActionView
91
150
  # # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
92
151
  def stylesheet_link_tag(*sources)
93
152
  options = sources.extract_options!.stringify_keys
94
- path_options = options.extract!('protocol').symbolize_keys
153
+ path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
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"]
95
159
 
96
- sources.uniq.map { |source|
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
97
169
  tag_options = {
98
170
  "rel" => "stylesheet",
99
171
  "media" => "screen",
100
- "href" => path_to_stylesheet(source, path_options)
172
+ "crossorigin" => crossorigin,
173
+ "href" => href
101
174
  }.merge!(options)
102
175
  tag(:link, tag_options)
103
176
  }.join("\n").html_safe
177
+
178
+ if preload_links_header
179
+ send_preload_links_header(preload_links)
180
+ end
181
+
182
+ sources_tags
104
183
  end
105
184
 
106
185
  # Returns a link tag that browsers and feed readers can use to auto-detect
107
- # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
108
- # <tt>:atom</tt>. Control the link options in url_for format using the
109
- # +url_options+. You can modify the LINK tag itself in +tag_options+.
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+.
110
189
  #
111
190
  # ==== Options
112
191
  #
@@ -120,6 +199,8 @@ module ActionView
120
199
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
121
200
  # auto_discovery_link_tag(:atom)
122
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" />
123
204
  # auto_discovery_link_tag(:rss, {action: "feed"})
124
205
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
125
206
  # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
@@ -127,143 +208,220 @@ module ActionView
127
208
  # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
128
209
  # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
129
210
  # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
130
- # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" />
211
+ # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
131
212
  def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
132
- if !(type == :rss || type == :atom) && tag_options[:type].blank?
133
- raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
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.")
134
215
  end
135
216
 
136
217
  tag(
137
218
  "link",
138
219
  "rel" => tag_options[:rel] || "alternate",
139
- "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
220
+ "type" => tag_options[:type] || Template::Types[type].to_s,
140
221
  "title" => tag_options[:title] || type.to_s.upcase,
141
- "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
222
+ "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options
142
223
  )
143
224
  end
144
225
 
145
- # Returns a link loading a favicon file. You may specify a different file
146
- # in the first argument. The helper accepts an additional options hash where
147
- # you can override "rel" and "type".
226
+ # Returns a link tag for a favicon managed by the asset pipeline.
148
227
  #
149
- # ==== Options
228
+ # If a page has no link like the one generated by this helper, browsers
229
+ # ask for <tt>/favicon.ico</tt> automatically, and cache the file if the
230
+ # request succeeds. If the favicon changes it is hard to get it updated.
150
231
  #
151
- # * <tt>:rel</tt> - Specify the relation of this link, defaults to 'shortcut icon'
152
- # * <tt>:type</tt> - Override the auto-generated mime type, defaults to 'image/vnd.microsoft.icon'
232
+ # To have better control applications may let the asset pipeline manage
233
+ # their favicon storing the file under <tt>app/assets/images</tt>, and
234
+ # using this helper to generate its corresponding link tag.
153
235
  #
154
- # ==== Examples
236
+ # The helper gets the name of the favicon file as first argument, which
237
+ # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
238
+ # to override their defaults, "shortcut icon" and "image/x-icon"
239
+ # respectively:
240
+ #
241
+ # favicon_link_tag
242
+ # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
155
243
  #
156
244
  # favicon_link_tag 'myicon.ico'
157
- # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
245
+ # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
158
246
  #
159
- # Mobile Safari looks for a different <link> tag, pointing to an image that
160
- # will be used if you add the page to the home screen of an iPod Touch, iPhone, or iPad.
247
+ # Mobile Safari looks for a different link tag, pointing to an image that
248
+ # will be used if you add the page to the home screen of an iOS device.
161
249
  # The following call would generate such a tag:
162
250
  #
163
251
  # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
164
252
  # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
165
- def favicon_link_tag(source='favicon.ico', options={})
166
- tag('link', {
167
- :rel => 'shortcut icon',
168
- :type => 'image/vnd.microsoft.icon',
169
- :href => path_to_image(source)
253
+ def favicon_link_tag(source = "favicon.ico", options = {})
254
+ tag("link", {
255
+ rel: "shortcut icon",
256
+ type: "image/x-icon",
257
+ href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
170
258
  }.merge!(options.symbolize_keys))
171
259
  end
172
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
+
173
325
  # Returns an HTML image tag for the +source+. The +source+ can be a full
174
- # path or a file.
326
+ # path, a file, or an Active Storage attachment.
175
327
  #
176
328
  # ==== Options
177
329
  #
178
330
  # You can add HTML attributes using the +options+. The +options+ supports
179
- # two additional keys for convenience and conformance:
331
+ # additional keys for convenience and conformance:
180
332
  #
181
- # * <tt>:alt</tt> - If no alt text is given, the file name part of the
182
- # +source+ is used (capitalized and without the extension)
183
333
  # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
184
334
  # width="30" and height="45", and "50" becomes width="50" and height="50".
185
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.
186
338
  #
187
339
  # ==== Examples
188
340
  #
341
+ # Assets (images that are part of your app):
342
+ #
189
343
  # image_tag("icon")
190
- # # => <img alt="Icon" src="/assets/icon" />
344
+ # # => <img src="/assets/icon" />
191
345
  # image_tag("icon.png")
192
- # # => <img alt="Icon" src="/assets/icon.png" />
346
+ # # => <img src="/assets/icon.png" />
193
347
  # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
194
348
  # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
195
349
  # image_tag("/icons/icon.gif", size: "16")
196
- # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
350
+ # # => <img src="/icons/icon.gif" width="16" height="16" />
197
351
  # image_tag("/icons/icon.gif", height: '32', width: '32')
198
- # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
352
+ # # => <img height="32" src="/icons/icon.gif" width="32" />
199
353
  # image_tag("/icons/icon.gif", class: "menu_icon")
200
- # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
201
- def image_tag(source, options={})
354
+ # # => <img class="menu_icon" src="/icons/icon.gif" />
355
+ # image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
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" />
370
+ def image_tag(source, options = {})
202
371
  options = options.symbolize_keys
372
+ check_for_image_tag_errors(options)
373
+ skip_pipeline = options.delete(:skip_pipeline)
203
374
 
204
- src = options[:src] = path_to_image(source)
375
+ options[:src] = resolve_image_source(source, skip_pipeline)
205
376
 
206
- unless src =~ /^(?:cid|data):/ || src.blank?
207
- 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(", ")
208
382
  end
209
383
 
210
384
  options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
211
385
  tag("img", options)
212
386
  end
213
387
 
214
- # Returns a string suitable for an html image tag alt attribute.
215
- # The +src+ argument is meant to be an image file path.
216
- # The method removes the basename of the file path and the digest,
217
- # if any. It also removes hyphens and underscores from file names and
218
- # replaces them with spaces, returning a space-separated, titleized
219
- # string.
220
- #
221
- # ==== Examples
222
- #
223
- # image_alt('rails.png')
224
- # # => Rails
225
- #
226
- # image_alt('hyphenated-file-name.png')
227
- # # => Hyphenated file name
228
- #
229
- # image_alt('underscored_file_name.png')
230
- # # => Underscored file name
231
- def image_alt(src)
232
- File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
233
- end
234
-
235
- # Returns an html video tag for the +sources+. If +sources+ is a string,
388
+ # Returns an HTML video tag for the +sources+. If +sources+ is a string,
236
389
  # a single video tag will be returned. If +sources+ is an array, a video
237
390
  # tag with nested source tags for each source will be returned. The
238
- # +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
239
392
  # directory.
240
393
  #
241
394
  # ==== Options
242
- # You can add HTML attributes using the +options+. The +options+ supports
243
- # 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:
244
398
  #
245
399
  # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
246
400
  # before the video loads. The path is calculated like the +src+ of +image_tag+.
247
401
  # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
248
402
  # width="30" and height="45", and "50" becomes width="50" and height="50".
249
403
  # <tt>:size</tt> will be ignored if the value is not in the correct format.
404
+ # * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using
405
+ # the <tt>:poster</tt> option instead using an asset in the public folder.
250
406
  #
251
407
  # ==== Examples
252
408
  #
253
409
  # video_tag("trailer")
254
- # # => <video src="/videos/trailer" />
410
+ # # => <video src="/videos/trailer"></video>
255
411
  # video_tag("trailer.ogg")
256
- # # => <video src="/videos/trailer.ogg" />
257
- # video_tag("trailer.ogg", controls: true, autobuffer: true)
258
- # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" />
412
+ # # => <video src="/videos/trailer.ogg"></video>
413
+ # video_tag("trailer.ogg", controls: true, preload: 'none')
414
+ # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
259
415
  # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
260
- # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" />
416
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
417
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
418
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video>
261
419
  # video_tag("/trailers/hd.avi", size: "16x16")
262
- # # => <video src="/trailers/hd.avi" width="16" height="16" />
420
+ # # => <video src="/trailers/hd.avi" width="16" height="16"></video>
263
421
  # video_tag("/trailers/hd.avi", size: "16")
264
- # # => <video height="16" src="/trailers/hd.avi" width="16" />
422
+ # # => <video height="16" src="/trailers/hd.avi" width="16"></video>
265
423
  # video_tag("/trailers/hd.avi", height: '32', width: '32')
266
- # # => <video height="32" src="/trailers/hd.avi" width="32" />
424
+ # # => <video height="32" src="/trailers/hd.avi" width="32"></video>
267
425
  # video_tag("trailer.ogg", "trailer.flv")
268
426
  # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
269
427
  # video_tag(["trailer.ogg", "trailer.flv"])
@@ -271,52 +429,100 @@ module ActionView
271
429
  # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
272
430
  # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
273
431
  def video_tag(*sources)
274
- multiple_sources_tag('video', sources) do |options|
275
- options[:poster] = path_to_image(options[:poster]) if options[:poster]
276
- options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
432
+ options = sources.extract_options!.symbolize_keys
433
+ public_poster_folder = options.delete(:poster_skip_pipeline)
434
+ sources << options
435
+ multiple_sources_tag_builder("video", sources) do |tag_options|
436
+ tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster]
437
+ tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size]
277
438
  end
278
439
  end
279
440
 
280
- # Returns an HTML audio tag for the +source+.
281
- # The +source+ can be full path or file that exists in
282
- # 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.
283
449
  #
284
450
  # audio_tag("sound")
285
- # # => <audio src="/audios/sound" />
451
+ # # => <audio src="/audios/sound"></audio>
286
452
  # audio_tag("sound.wav")
287
- # # => <audio src="/audios/sound.wav" />
453
+ # # => <audio src="/audios/sound.wav"></audio>
288
454
  # audio_tag("sound.wav", autoplay: true, controls: true)
289
- # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" />
455
+ # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
290
456
  # audio_tag("sound.wav", "sound.mid")
291
457
  # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
292
458
  def audio_tag(*sources)
293
- multiple_sources_tag('audio', sources)
459
+ multiple_sources_tag_builder("audio", sources)
294
460
  end
295
461
 
296
462
  private
297
- def multiple_sources_tag(type, sources)
298
- options = sources.extract_options!.symbolize_keys
463
+ def multiple_sources_tag_builder(type, sources)
464
+ options = sources.extract_options!.symbolize_keys
465
+ skip_pipeline = options.delete(:skip_pipeline)
299
466
  sources.flatten!
300
467
 
301
468
  yield options if block_given?
302
469
 
303
470
  if sources.size > 1
304
471
  content_tag(type, options) do
305
- safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) }
472
+ safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
306
473
  end
307
474
  else
308
- options[:src] = send("path_to_#{type}", sources.first)
475
+ options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
309
476
  content_tag(type, nil, options)
310
477
  end
311
478
  end
312
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
+
313
490
  def extract_dimensions(size)
314
- if size =~ %r{\A\d+x\d+\z}
315
- size.split('x')
316
- elsif size =~ %r{\A\d+\z}
491
+ size = size.to_s
492
+ if /\A\d+x\d+\z/.match?(size)
493
+ size.split("x")
494
+ elsif /\A\d+\z/.match?(size)
317
495
  [size, size]
318
496
  end
319
497
  end
498
+
499
+ def check_for_image_tag_errors(options)
500
+ if options[:size] && (options[:height] || options[:width])
501
+ raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
502
+ end
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
320
526
  end
321
527
  end
322
528
  end