actionview 6.1.4.4 → 7.0.0.alpha1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -297
  3. data/MIT-LICENSE +1 -1
  4. data/lib/action_view/base.rb +3 -3
  5. data/lib/action_view/buffers.rb +2 -2
  6. data/lib/action_view/cache_expiry.rb +46 -32
  7. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  8. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  9. data/lib/action_view/dependency_tracker.rb +6 -147
  10. data/lib/action_view/digestor.rb +7 -4
  11. data/lib/action_view/flows.rb +4 -4
  12. data/lib/action_view/gem_version.rb +4 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  14. data/lib/action_view/helpers/asset_tag_helper.rb +84 -29
  15. data/lib/action_view/helpers/asset_url_helper.rb +7 -7
  16. data/lib/action_view/helpers/atom_feed_helper.rb +3 -4
  17. data/lib/action_view/helpers/cache_helper.rb +51 -3
  18. data/lib/action_view/helpers/capture_helper.rb +2 -2
  19. data/lib/action_view/helpers/controller_helper.rb +2 -2
  20. data/lib/action_view/helpers/csp_helper.rb +1 -1
  21. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  22. data/lib/action_view/helpers/date_helper.rb +5 -5
  23. data/lib/action_view/helpers/debug_helper.rb +3 -1
  24. data/lib/action_view/helpers/form_helper.rb +72 -12
  25. data/lib/action_view/helpers/form_options_helper.rb +65 -33
  26. data/lib/action_view/helpers/form_tag_helper.rb +73 -30
  27. data/lib/action_view/helpers/javascript_helper.rb +3 -5
  28. data/lib/action_view/helpers/number_helper.rb +3 -4
  29. data/lib/action_view/helpers/output_safety_helper.rb +2 -2
  30. data/lib/action_view/helpers/rendering_helper.rb +1 -1
  31. data/lib/action_view/helpers/sanitize_helper.rb +2 -2
  32. data/lib/action_view/helpers/tag_helper.rb +17 -4
  33. data/lib/action_view/helpers/tags/base.rb +2 -14
  34. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  35. data/lib/action_view/helpers/tags/collection_select.rb +1 -1
  36. data/lib/action_view/helpers/tags/time_field.rb +10 -1
  37. data/lib/action_view/helpers/tags/weekday_select.rb +27 -0
  38. data/lib/action_view/helpers/tags.rb +3 -2
  39. data/lib/action_view/helpers/text_helper.rb +24 -13
  40. data/lib/action_view/helpers/translation_helper.rb +1 -2
  41. data/lib/action_view/helpers/url_helper.rb +110 -81
  42. data/lib/action_view/helpers.rb +25 -25
  43. data/lib/action_view/lookup_context.rb +33 -52
  44. data/lib/action_view/model_naming.rb +1 -1
  45. data/lib/action_view/path_set.rb +16 -22
  46. data/lib/action_view/railtie.rb +15 -2
  47. data/lib/action_view/render_parser.rb +188 -0
  48. data/lib/action_view/renderer/abstract_renderer.rb +2 -2
  49. data/lib/action_view/renderer/partial_renderer.rb +0 -34
  50. data/lib/action_view/renderer/renderer.rb +4 -4
  51. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -3
  52. data/lib/action_view/renderer/template_renderer.rb +6 -2
  53. data/lib/action_view/rendering.rb +2 -2
  54. data/lib/action_view/ripper_ast_parser.rb +198 -0
  55. data/lib/action_view/routing_url_for.rb +1 -1
  56. data/lib/action_view/template/error.rb +108 -13
  57. data/lib/action_view/template/handlers/erb.rb +6 -0
  58. data/lib/action_view/template/handlers.rb +3 -3
  59. data/lib/action_view/template/html.rb +3 -3
  60. data/lib/action_view/template/inline.rb +3 -3
  61. data/lib/action_view/template/raw_file.rb +3 -3
  62. data/lib/action_view/template/resolver.rb +84 -311
  63. data/lib/action_view/template/text.rb +3 -3
  64. data/lib/action_view/template/types.rb +14 -12
  65. data/lib/action_view/template.rb +10 -1
  66. data/lib/action_view/template_details.rb +66 -0
  67. data/lib/action_view/template_path.rb +64 -0
  68. data/lib/action_view/test_case.rb +6 -2
  69. data/lib/action_view/testing/resolvers.rb +11 -12
  70. data/lib/action_view/unbound_template.rb +33 -7
  71. data/lib/action_view.rb +3 -4
  72. metadata +22 -15
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ class DependencyTracker # :nodoc:
5
+ class ERBTracker # :nodoc:
6
+ EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
7
+
8
+ # A valid ruby identifier - suitable for class, method and specially variable names
9
+ IDENTIFIER = /
10
+ [[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
11
+ [[:word:]]* # followed by optional letters, numbers or underscores
12
+ /x
13
+
14
+ # Any kind of variable name. e.g. @instance, @@class, $global or local.
15
+ # Possibly following a method call chain
16
+ VARIABLE_OR_METHOD_CHAIN = /
17
+ (?:\$|@{1,2})? # optional global, instance or class variable indicator
18
+ (?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
19
+ (?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
20
+ /x
21
+
22
+ # A simple string literal. e.g. "School's out!"
23
+ STRING = /
24
+ (?<quote>['"]) # an opening quote
25
+ (?<static>.*?) # with anything inside, captured as STATIC
26
+ \k<quote> # and a matching closing quote
27
+ /x
28
+
29
+ # Part of any hash containing the :partial key
30
+ PARTIAL_HASH_KEY = /
31
+ (?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
32
+ \s* # followed by optional spaces
33
+ /x
34
+
35
+ # Part of any hash containing the :layout key
36
+ LAYOUT_HASH_KEY = /
37
+ (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
38
+ \s* # followed by optional spaces
39
+ /x
40
+
41
+ # Matches:
42
+ # partial: "comments/comment", collection: @all_comments => "comments/comment"
43
+ # (object: @single_comment, partial: "comments/comment") => "comments/comment"
44
+ #
45
+ # "comments/comments"
46
+ # 'comments/comments'
47
+ # ('comments/comments')
48
+ #
49
+ # (@topic) => "topics/topic"
50
+ # topics => "topics/topic"
51
+ # (message.topics) => "topics/topic"
52
+ RENDER_ARGUMENTS = /\A
53
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
54
+ (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
55
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
56
+ /xm
57
+
58
+ LAYOUT_DEPENDENCY = /\A
59
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
60
+ (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
61
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
62
+ /xm
63
+
64
+ def self.supports_view_paths? # :nodoc:
65
+ true
66
+ end
67
+
68
+ def self.call(name, template, view_paths = nil)
69
+ new(name, template, view_paths).dependencies
70
+ end
71
+
72
+ def initialize(name, template, view_paths = nil)
73
+ @name, @template, @view_paths = name, template, view_paths
74
+ end
75
+
76
+ def dependencies
77
+ render_dependencies + explicit_dependencies
78
+ end
79
+
80
+ attr_reader :name, :template
81
+ private :name, :template
82
+
83
+ private
84
+ def source
85
+ template.source
86
+ end
87
+
88
+ def directory
89
+ name.split("/")[0..-2].join("/")
90
+ end
91
+
92
+ def render_dependencies
93
+ render_dependencies = []
94
+ render_calls = source.split(/\brender\b/).drop(1)
95
+
96
+ render_calls.each do |arguments|
97
+ add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
98
+ add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
99
+ end
100
+
101
+ render_dependencies.uniq
102
+ end
103
+
104
+ def add_dependencies(render_dependencies, arguments, pattern)
105
+ arguments.scan(pattern) do
106
+ match = Regexp.last_match
107
+ add_dynamic_dependency(render_dependencies, match[:dynamic])
108
+ add_static_dependency(render_dependencies, match[:static], match[:quote])
109
+ end
110
+ end
111
+
112
+ def add_dynamic_dependency(dependencies, dependency)
113
+ if dependency
114
+ dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
115
+ end
116
+ end
117
+
118
+ def add_static_dependency(dependencies, dependency, quote_type)
119
+ if quote_type == '"'
120
+ # Ignore if there is interpolation
121
+ return if dependency.include?('#{')
122
+ end
123
+
124
+ if dependency
125
+ if dependency.include?("/")
126
+ dependencies << dependency
127
+ else
128
+ dependencies << "#{directory}/#{dependency}"
129
+ end
130
+ end
131
+ end
132
+
133
+ def resolve_directories(wildcard_dependencies)
134
+ return [] unless @view_paths
135
+ return [] if wildcard_dependencies.empty?
136
+
137
+ # Remove trailing "/*"
138
+ prefixes = wildcard_dependencies.map { |query| query[0..-3] }
139
+
140
+ @view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
141
+ path.to_s if prefixes.include?(path.prefix)
142
+ }.sort
143
+ end
144
+
145
+ def explicit_dependencies
146
+ dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
147
+
148
+ wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") }
149
+
150
+ (explicits + resolve_directories(wildcards)).uniq
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ class DependencyTracker # :nodoc:
5
+ class RipperTracker # :nodoc:
6
+ EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
7
+
8
+ def self.call(name, template, view_paths = nil)
9
+ new(name, template, view_paths).dependencies
10
+ end
11
+
12
+ def dependencies
13
+ render_dependencies + explicit_dependencies
14
+ end
15
+
16
+ def self.supports_view_paths? # :nodoc:
17
+ true
18
+ end
19
+
20
+ def initialize(name, template, view_paths = nil)
21
+ @name, @template, @view_paths = name, template, view_paths
22
+ end
23
+
24
+ private
25
+ attr_reader :template, :name, :view_paths
26
+
27
+ def render_dependencies
28
+ return [] unless template.source.include?("render")
29
+
30
+ compiled_source = template.handler.call(template, template.source)
31
+
32
+ RenderParser.new(@name, compiled_source).render_calls.filter_map do |render_call|
33
+ next if render_call.end_with?("/_")
34
+ render_call.gsub(%r|/_|, "/")
35
+ end
36
+ end
37
+
38
+ def explicit_dependencies
39
+ dependencies = template.source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
40
+
41
+ wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("/*") }
42
+
43
+ (explicits + resolve_directories(wildcards)).uniq
44
+ end
45
+
46
+ def resolve_directories(wildcard_dependencies)
47
+ return [] unless view_paths
48
+ return [] if wildcard_dependencies.empty?
49
+
50
+ # Remove trailing "/*"
51
+ prefixes = wildcard_dependencies.map { |query| query[0..-3] }
52
+
53
+ view_paths.flat_map(&:all_template_paths).uniq.filter_map { |path|
54
+ path.to_s if prefixes.include?(path.prefix)
55
+ }.sort
56
+ end
57
+ end
58
+ end
59
+ end
@@ -2,9 +2,15 @@
2
2
 
3
3
  require "concurrent/map"
4
4
  require "action_view/path_set"
5
+ require "action_view/render_parser"
5
6
 
6
7
  module ActionView
7
8
  class DependencyTracker # :nodoc:
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :ERBTracker
12
+ autoload :RipperTracker
13
+
8
14
  @trackers = Concurrent::Map.new
9
15
 
10
16
  def self.find_dependencies(name, template, view_paths = nil)
@@ -29,153 +35,6 @@ module ActionView
29
35
  @trackers.delete(handler)
30
36
  end
31
37
 
32
- class ERBTracker # :nodoc:
33
- EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
34
-
35
- # A valid ruby identifier - suitable for class, method and specially variable names
36
- IDENTIFIER = /
37
- [[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
38
- [[:word:]]* # followed by optional letters, numbers or underscores
39
- /x
40
-
41
- # Any kind of variable name. e.g. @instance, @@class, $global or local.
42
- # Possibly following a method call chain
43
- VARIABLE_OR_METHOD_CHAIN = /
44
- (?:\$|@{1,2})? # optional global, instance or class variable indicator
45
- (?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
46
- (?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
47
- /x
48
-
49
- # A simple string literal. e.g. "School's out!"
50
- STRING = /
51
- (?<quote>['"]) # an opening quote
52
- (?<static>.*?) # with anything inside, captured as STATIC
53
- \k<quote> # and a matching closing quote
54
- /x
55
-
56
- # Part of any hash containing the :partial key
57
- PARTIAL_HASH_KEY = /
58
- (?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
59
- \s* # followed by optional spaces
60
- /x
61
-
62
- # Part of any hash containing the :layout key
63
- LAYOUT_HASH_KEY = /
64
- (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
65
- \s* # followed by optional spaces
66
- /x
67
-
68
- # Matches:
69
- # partial: "comments/comment", collection: @all_comments => "comments/comment"
70
- # (object: @single_comment, partial: "comments/comment") => "comments/comment"
71
- #
72
- # "comments/comments"
73
- # 'comments/comments'
74
- # ('comments/comments')
75
- #
76
- # (@topic) => "topics/topic"
77
- # topics => "topics/topic"
78
- # (message.topics) => "topics/topic"
79
- RENDER_ARGUMENTS = /\A
80
- (?:\s*\(?\s*) # optional opening paren surrounded by spaces
81
- (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
82
- (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
83
- /xm
84
-
85
- LAYOUT_DEPENDENCY = /\A
86
- (?:\s*\(?\s*) # optional opening paren surrounded by spaces
87
- (?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
88
- (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
89
- /xm
90
-
91
- def self.supports_view_paths? # :nodoc:
92
- true
93
- end
94
-
95
- def self.call(name, template, view_paths = nil)
96
- new(name, template, view_paths).dependencies
97
- end
98
-
99
- def initialize(name, template, view_paths = nil)
100
- @name, @template, @view_paths = name, template, view_paths
101
- end
102
-
103
- def dependencies
104
- render_dependencies + explicit_dependencies
105
- end
106
-
107
- attr_reader :name, :template
108
- private :name, :template
109
-
110
- private
111
- def source
112
- template.source
113
- end
114
-
115
- def directory
116
- name.split("/")[0..-2].join("/")
117
- end
118
-
119
- def render_dependencies
120
- render_dependencies = []
121
- render_calls = source.split(/\brender\b/).drop(1)
122
-
123
- render_calls.each do |arguments|
124
- add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
125
- add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
126
- end
127
-
128
- render_dependencies.uniq
129
- end
130
-
131
- def add_dependencies(render_dependencies, arguments, pattern)
132
- arguments.scan(pattern) do
133
- match = Regexp.last_match
134
- add_dynamic_dependency(render_dependencies, match[:dynamic])
135
- add_static_dependency(render_dependencies, match[:static], match[:quote])
136
- end
137
- end
138
-
139
- def add_dynamic_dependency(dependencies, dependency)
140
- if dependency
141
- dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
142
- end
143
- end
144
-
145
- def add_static_dependency(dependencies, dependency, quote_type)
146
- if quote_type == '"'
147
- # Ignore if there is interpolation
148
- return if dependency.include?('#{')
149
- end
150
-
151
- if dependency
152
- if dependency.include?("/")
153
- dependencies << dependency
154
- else
155
- dependencies << "#{directory}/#{dependency}"
156
- end
157
- end
158
- end
159
-
160
- def resolve_directories(wildcard_dependencies)
161
- return [] unless @view_paths
162
-
163
- wildcard_dependencies.flat_map { |query, templates|
164
- @view_paths.find_all_with_query(query).map do |template|
165
- "#{File.dirname(query)}/#{File.basename(template).split('.').first}"
166
- end
167
- }.sort
168
- end
169
-
170
- def explicit_dependencies
171
- dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
172
-
173
- wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("*") }
174
-
175
- (explicits + resolve_directories(wildcards)).uniq
176
- end
177
- end
178
-
179
38
  register_tracker :erb, ERBTracker
180
39
  end
181
40
  end
@@ -17,15 +17,16 @@ module ActionView
17
17
  if dependencies.nil? || dependencies.empty?
18
18
  cache_key = "#{name}.#{format}"
19
19
  else
20
- cache_key = [ name, format, dependencies ].flatten.compact.join(".")
20
+ dependencies_suffix = dependencies.flatten.tap(&:compact!).join(".")
21
+ cache_key = "#{name}.#{format}.#{dependencies_suffix}"
21
22
  end
22
23
 
23
24
  # this is a correctly done double-checked locking idiom
24
25
  # (Concurrent::Map's lookups have volatile semantics)
25
26
  finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
26
27
  finder.digest_cache.fetch(cache_key) do # re-check under lock
27
- partial = name.include?("/_")
28
- root = tree(name, finder, partial)
28
+ path = TemplatePath.parse(name)
29
+ root = tree(path.to_s, finder, path.partial?)
29
30
  dependencies.each do |injected_dep|
30
31
  root.children << Injected.new(injected_dep, nil, nil)
31
32
  end if dependencies
@@ -43,7 +44,9 @@ module ActionView
43
44
  logical_name = name.gsub(%r|/_|, "/")
44
45
  interpolated = name.include?("#")
45
46
 
46
- if !interpolated && (template = find_template(finder, logical_name, [], partial, []))
47
+ path = TemplatePath.parse(name)
48
+
49
+ if !interpolated && (template = find_template(finder, path.name, [path.prefix], partial, []))
47
50
  if node = seen[template.identifier] # handle cycles in the tree
48
51
  node
49
52
  else
@@ -3,7 +3,7 @@
3
3
  require "active_support/core_ext/string/output_safety"
4
4
 
5
5
  module ActionView
6
- class OutputFlow #:nodoc:
6
+ class OutputFlow # :nodoc:
7
7
  attr_reader :content
8
8
 
9
9
  def initialize
@@ -17,17 +17,17 @@ module ActionView
17
17
 
18
18
  # Called by each renderer object to set the layout contents.
19
19
  def set(key, value)
20
- @content[key] = ActiveSupport::SafeBuffer.new(value)
20
+ @content[key] = ActiveSupport::SafeBuffer.new(value.to_s)
21
21
  end
22
22
 
23
23
  # Called by content_for
24
24
  def append(key, value)
25
- @content[key] << value
25
+ @content[key] << value.to_s
26
26
  end
27
27
  alias_method :append!, :append
28
28
  end
29
29
 
30
- class StreamingFlow < OutputFlow #:nodoc:
30
+ class StreamingFlow < OutputFlow # :nodoc:
31
31
  def initialize(view, fiber)
32
32
  @view = view
33
33
  @parent = nil
@@ -7,10 +7,10 @@ module ActionView
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 4
13
- PRE = "4"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "alpha1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -5,7 +5,7 @@ require "active_support/core_ext/enumerable"
5
5
 
6
6
  module ActionView
7
7
  # = Active Model Helpers
8
- module Helpers #:nodoc:
8
+ module Helpers # :nodoc:
9
9
  module ActiveModelHelper
10
10
  end
11
11
 
@@ -8,7 +8,7 @@ require "action_view/helpers/tag_helper"
8
8
 
9
9
  module ActionView
10
10
  # = Action View Asset Tag Helpers
11
- module Helpers #:nodoc:
11
+ module Helpers # :nodoc:
12
12
  # This module provides methods for generating HTML that links views to assets such
13
13
  # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
14
14
  # the assets exist before linking to them:
@@ -16,14 +16,15 @@ module ActionView
16
16
  # image_tag("rails.png")
17
17
  # # => <img src="/assets/rails.png" />
18
18
  # stylesheet_link_tag("application")
19
- # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
19
+ # # => <link href="/assets/application.css?body=1" rel="stylesheet" />
20
20
  module AssetTagHelper
21
- extend ActiveSupport::Concern
22
-
23
21
  include AssetUrlHelper
24
22
  include TagHelper
25
23
 
24
+ mattr_accessor :image_loading
25
+ mattr_accessor :image_decoding
26
26
  mattr_accessor :preload_links_header
27
+ mattr_accessor :apply_stylesheet_media_default
27
28
 
28
29
  # Returns an HTML script tag for each of the +sources+ provided.
29
30
  #
@@ -93,11 +94,12 @@ module ActionView
93
94
  crossorigin = options.delete("crossorigin")
94
95
  crossorigin = "anonymous" if crossorigin == true
95
96
  integrity = options["integrity"]
97
+ rel = options["type"] == "module" ? "modulepreload" : "preload"
96
98
 
97
99
  sources_tags = sources.uniq.map { |source|
98
100
  href = path_to_javascript(source, path_options)
99
- if preload_links_header && !options["defer"]
100
- preload_link = "<#{href}>; rel=preload; as=script"
101
+ if preload_links_header && !options["defer"] && href.present? && !href.start_with?("data:")
102
+ preload_link = "<#{href}>; rel=#{rel}; as=script"
101
103
  preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
102
104
  preload_link += "; integrity=#{integrity}" unless integrity.nil?
103
105
  preload_link += "; nopush" if nopush
@@ -120,24 +122,41 @@ module ActionView
120
122
  sources_tags
121
123
  end
122
124
 
123
- # Returns a stylesheet link tag for the sources specified as arguments. If
124
- # you don't specify an extension, <tt>.css</tt> will be appended automatically.
125
+ # Returns a stylesheet link tag for the sources specified as arguments.
126
+ #
127
+ # When passing paths, the <tt>.css</tt> extension is optional.
128
+ # If you don't specify an extension, <tt>.css</tt> will be appended automatically.
129
+ # If you do not want <tt>.css</tt> appended to the path,
130
+ # set <tt>extname: false</tt> in the options.
125
131
  # You can modify the link attributes by passing a hash as the last argument.
126
- # For historical reasons, the 'media' attribute will always be present and defaults
127
- # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
128
- # apply to all media types.
129
132
  #
130
133
  # If the server supports Early Hints header links for these assets will be
131
134
  # automatically pushed.
132
135
  #
136
+ # ==== Options
137
+ #
138
+ # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
139
+ # already exists. This only applies for relative URLs.
140
+ # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
141
+ # applies when a relative URL and +host+ options are provided.
142
+ # * <tt>:host</tt> - When a relative URL is provided the host is added to the
143
+ # that path.
144
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
145
+ # when it is set to true.
146
+ #
147
+ # ==== Examples
148
+ #
133
149
  # stylesheet_link_tag "style"
134
- # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
150
+ # # => <link href="/assets/style.css" rel="stylesheet" />
135
151
  #
136
152
  # stylesheet_link_tag "style.css"
137
- # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
153
+ # # => <link href="/assets/style.css" rel="stylesheet" />
138
154
  #
139
155
  # stylesheet_link_tag "http://www.example.com/style.css"
140
- # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />
156
+ # # => <link href="http://www.example.com/style.css" rel="stylesheet" />
157
+ #
158
+ # stylesheet_link_tag "style.less", extname: false, skip_pipeline: true, rel: "stylesheet/less"
159
+ # # => <link href="/stylesheets/style.less" rel="stylesheet/less">
141
160
  #
142
161
  # stylesheet_link_tag "style", media: "all"
143
162
  # # => <link href="/assets/style.css" media="all" rel="stylesheet" />
@@ -146,11 +165,11 @@ module ActionView
146
165
  # # => <link href="/assets/style.css" media="print" rel="stylesheet" />
147
166
  #
148
167
  # stylesheet_link_tag "random.styles", "/css/stylish"
149
- # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" />
150
- # # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
168
+ # # => <link href="/assets/random.styles" rel="stylesheet" />
169
+ # # <link href="/css/stylish.css" rel="stylesheet" />
151
170
  def stylesheet_link_tag(*sources)
152
171
  options = sources.extract_options!.stringify_keys
153
- path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
172
+ path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
154
173
  preload_links = []
155
174
  crossorigin = options.delete("crossorigin")
156
175
  crossorigin = "anonymous" if crossorigin == true
@@ -159,7 +178,7 @@ module ActionView
159
178
 
160
179
  sources_tags = sources.uniq.map { |source|
161
180
  href = path_to_stylesheet(source, path_options)
162
- if preload_links_header
181
+ if preload_links_header && href.present? && !href.start_with?("data:")
163
182
  preload_link = "<#{href}>; rel=preload; as=style"
164
183
  preload_link += "; crossorigin=#{crossorigin}" unless crossorigin.nil?
165
184
  preload_link += "; integrity=#{integrity}" unless integrity.nil?
@@ -168,10 +187,14 @@ module ActionView
168
187
  end
169
188
  tag_options = {
170
189
  "rel" => "stylesheet",
171
- "media" => "screen",
172
190
  "crossorigin" => crossorigin,
173
191
  "href" => href
174
192
  }.merge!(options)
193
+
194
+ if apply_stylesheet_media_default && tag_options["media"].blank?
195
+ tag_options["media"] = "screen"
196
+ end
197
+
175
198
  tag(:link, tag_options)
176
199
  }.join("\n").html_safe
177
200
 
@@ -382,6 +405,10 @@ module ActionView
382
405
  end
383
406
 
384
407
  options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
408
+
409
+ options[:loading] ||= image_loading if image_loading
410
+ options[:decoding] ||= image_decoding if image_decoding
411
+
385
412
  tag("img", options)
386
413
  end
387
414
 
@@ -503,24 +530,52 @@ module ActionView
503
530
  end
504
531
 
505
532
  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
533
+ case extname
534
+ when "js" then "script"
535
+ when "css" then "style"
536
+ when "vtt" then "track"
537
+ else
538
+ mime_type.to_s.split("/").first.presence_in(%w(audio video font image))
514
539
  end
515
540
  end
516
541
 
517
- def send_preload_links_header(preload_links)
542
+ MAX_HEADER_SIZE = 8_000 # Some HTTP client and proxies have a 8kiB header limit
543
+ def send_preload_links_header(preload_links, max_header_size: MAX_HEADER_SIZE)
544
+ return if preload_links.empty?
545
+ return if response.sending?
546
+
518
547
  if respond_to?(:request) && request
519
548
  request.send_early_hints("Link" => preload_links.join("\n"))
520
549
  end
521
550
 
522
551
  if respond_to?(:response) && response
523
- response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
552
+ header = response.headers["Link"]
553
+ header = header ? header.dup : +""
554
+
555
+ # rindex count characters not bytes, but we assume non-ascii characters
556
+ # are rare in urls, and we have a 192 bytes margin.
557
+ last_line_offset = header.rindex("\n")
558
+ last_line_size = if last_line_offset
559
+ header.bytesize - last_line_offset
560
+ else
561
+ header.bytesize
562
+ end
563
+
564
+ preload_links.each do |link|
565
+ if link.bytesize + last_line_size + 1 < max_header_size
566
+ unless header.empty?
567
+ header << ","
568
+ last_line_size += 1
569
+ end
570
+ else
571
+ header << "\n"
572
+ last_line_size = 0
573
+ end
574
+ header << link
575
+ last_line_size += link.bytesize
576
+ end
577
+
578
+ response.headers["Link"] = header
524
579
  end
525
580
  end
526
581
  end