actionview 5.2.4 → 6.0.0

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -77
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -2
  5. data/lib/action_view.rb +3 -2
  6. data/lib/action_view/base.rb +107 -10
  7. data/lib/action_view/buffers.rb +15 -0
  8. data/lib/action_view/cache_expiry.rb +54 -0
  9. data/lib/action_view/context.rb +5 -9
  10. data/lib/action_view/digestor.rb +12 -20
  11. data/lib/action_view/gem_version.rb +3 -3
  12. data/lib/action_view/helpers.rb +0 -2
  13. data/lib/action_view/helpers/asset_tag_helper.rb +7 -30
  14. data/lib/action_view/helpers/asset_url_helper.rb +4 -3
  15. data/lib/action_view/helpers/cache_helper.rb +18 -10
  16. data/lib/action_view/helpers/capture_helper.rb +4 -0
  17. data/lib/action_view/helpers/csp_helper.rb +4 -2
  18. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  19. data/lib/action_view/helpers/date_helper.rb +69 -25
  20. data/lib/action_view/helpers/form_helper.rb +238 -6
  21. data/lib/action_view/helpers/form_options_helper.rb +27 -18
  22. data/lib/action_view/helpers/form_tag_helper.rb +12 -11
  23. data/lib/action_view/helpers/javascript_helper.rb +9 -8
  24. data/lib/action_view/helpers/number_helper.rb +5 -0
  25. data/lib/action_view/helpers/output_safety_helper.rb +1 -1
  26. data/lib/action_view/helpers/rendering_helper.rb +6 -4
  27. data/lib/action_view/helpers/sanitize_helper.rb +12 -18
  28. data/lib/action_view/helpers/tag_helper.rb +7 -6
  29. data/lib/action_view/helpers/tags/base.rb +9 -5
  30. data/lib/action_view/helpers/tags/color_field.rb +1 -1
  31. data/lib/action_view/helpers/tags/translator.rb +1 -6
  32. data/lib/action_view/helpers/text_helper.rb +3 -3
  33. data/lib/action_view/helpers/translation_helper.rb +16 -12
  34. data/lib/action_view/helpers/url_helper.rb +14 -14
  35. data/lib/action_view/layouts.rb +5 -5
  36. data/lib/action_view/log_subscriber.rb +6 -6
  37. data/lib/action_view/lookup_context.rb +73 -31
  38. data/lib/action_view/path_set.rb +5 -10
  39. data/lib/action_view/railtie.rb +24 -1
  40. data/lib/action_view/record_identifier.rb +2 -2
  41. data/lib/action_view/renderer/abstract_renderer.rb +56 -3
  42. data/lib/action_view/renderer/partial_renderer.rb +66 -55
  43. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +62 -16
  44. data/lib/action_view/renderer/renderer.rb +16 -4
  45. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -5
  46. data/lib/action_view/renderer/template_renderer.rb +24 -18
  47. data/lib/action_view/rendering.rb +51 -31
  48. data/lib/action_view/routing_url_for.rb +12 -11
  49. data/lib/action_view/template.rb +102 -70
  50. data/lib/action_view/template/error.rb +21 -1
  51. data/lib/action_view/template/handlers.rb +27 -1
  52. data/lib/action_view/template/handlers/builder.rb +2 -2
  53. data/lib/action_view/template/handlers/erb.rb +17 -7
  54. data/lib/action_view/template/handlers/erb/erubi.rb +7 -3
  55. data/lib/action_view/template/handlers/html.rb +1 -1
  56. data/lib/action_view/template/handlers/raw.rb +2 -2
  57. data/lib/action_view/template/html.rb +14 -5
  58. data/lib/action_view/template/inline.rb +22 -0
  59. data/lib/action_view/template/raw_file.rb +28 -0
  60. data/lib/action_view/template/resolver.rb +136 -133
  61. data/lib/action_view/template/sources.rb +13 -0
  62. data/lib/action_view/template/sources/file.rb +17 -0
  63. data/lib/action_view/template/text.rb +5 -3
  64. data/lib/action_view/test_case.rb +1 -1
  65. data/lib/action_view/testing/resolvers.rb +33 -20
  66. data/lib/action_view/unbound_template.rb +32 -0
  67. data/lib/action_view/view_paths.rb +25 -1
  68. data/lib/assets/compiled/rails-ujs.js +30 -4
  69. metadata +23 -18
  70. data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -109,7 +109,7 @@ module ActionView
109
109
  end
110
110
  end
111
111
 
112
- def annoted_source_code
112
+ def annotated_source_code
113
113
  source_extract(4)
114
114
  end
115
115
 
@@ -138,4 +138,24 @@ module ActionView
138
138
  end
139
139
 
140
140
  TemplateError = Template::Error
141
+
142
+ class SyntaxErrorInTemplate < TemplateError #:nodoc
143
+ def initialize(template, offending_code_string)
144
+ @offending_code_string = offending_code_string
145
+ super(template)
146
+ end
147
+
148
+ def message
149
+ <<~MESSAGE
150
+ Encountered a syntax error while rendering template: check #{@offending_code_string}
151
+ MESSAGE
152
+ end
153
+
154
+ def annotated_source_code
155
+ @offending_code_string.split("\n").map.with_index(1) { |line, index|
156
+ indentation = " " * 4
157
+ "#{index}:#{indentation}#{line}"
158
+ }
159
+ end
160
+ end
141
161
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deprecation"
4
+
3
5
  module ActionView #:nodoc:
4
6
  # = Action View Template Handlers
5
7
  class Template #:nodoc:
@@ -14,7 +16,7 @@ module ActionView #:nodoc:
14
16
  base.register_template_handler :erb, ERB.new
15
17
  base.register_template_handler :html, Html.new
16
18
  base.register_template_handler :builder, Builder.new
17
- base.register_template_handler :ruby, :source.to_proc
19
+ base.register_template_handler :ruby, lambda { |_, source| source }
18
20
  end
19
21
 
20
22
  @@template_handlers = {}
@@ -24,11 +26,35 @@ module ActionView #:nodoc:
24
26
  @@template_extensions ||= @@template_handlers.keys
25
27
  end
26
28
 
29
+ class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
30
+ def call(view, source)
31
+ __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
32
+ end
33
+ end
34
+
27
35
  # Register an object that knows how to handle template files with the given
28
36
  # extensions. This can be used to implement new template types.
29
37
  # The handler must respond to +:call+, which will be passed the template
30
38
  # and should return the rendered template as a String.
31
39
  def register_template_handler(*extensions, handler)
40
+ params = if handler.is_a?(Proc)
41
+ handler.parameters
42
+ else
43
+ handler.method(:call).parameters
44
+ end
45
+
46
+ unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
47
+ ActiveSupport::Deprecation.warn <<~eowarn
48
+ Single arity template handlers are deprecated. Template handlers must
49
+ now accept two parameters, the view object and the source for the view object.
50
+ Change:
51
+ >> #{handler}.call(#{params.map(&:last).join(", ")})
52
+ To:
53
+ >> #{handler}.call(#{params.map(&:last).join(", ")}, source)
54
+ eowarn
55
+ handler = LegacyHandlerWrapper.new(handler)
56
+ end
57
+
32
58
  raise(ArgumentError, "Extension is required") if extensions.empty?
33
59
  extensions.each do |extension|
34
60
  @@template_handlers[extension.to_sym] = handler
@@ -5,11 +5,11 @@ module ActionView
5
5
  class Builder
6
6
  class_attribute :default_format, default: :xml
7
7
 
8
- def call(template)
8
+ def call(template, source)
9
9
  require_engine
10
10
  "xml = ::Builder::XmlMarkup.new(:indent => 2);" \
11
11
  "self.output_buffer = xml.target!;" +
12
- template.source +
12
+ source +
13
13
  ";xml.target!;"
14
14
  end
15
15
 
@@ -14,12 +14,22 @@ module ActionView
14
14
  class_attribute :erb_implementation, default: Erubi
15
15
 
16
16
  # Do not escape templates of these mime types.
17
- class_attribute :escape_whitelist, default: ["text/plain"]
17
+ class_attribute :escape_ignore_list, default: ["text/plain"]
18
+
19
+ [self, singleton_class].each do |base|
20
+ base.alias_method :escape_whitelist, :escape_ignore_list
21
+ base.alias_method :escape_whitelist=, :escape_ignore_list=
22
+
23
+ base.deprecate(
24
+ escape_whitelist: "use #escape_ignore_list instead",
25
+ :escape_whitelist= => "use #escape_ignore_list= instead"
26
+ )
27
+ end
18
28
 
19
29
  ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
20
30
 
21
- def self.call(template)
22
- new.call(template)
31
+ def self.call(template, source)
32
+ new.call(template, source)
23
33
  end
24
34
 
25
35
  def supports_streaming?
@@ -30,24 +40,24 @@ module ActionView
30
40
  true
31
41
  end
32
42
 
33
- def call(template)
43
+ def call(template, source)
34
44
  # First, convert to BINARY, so in case the encoding is
35
45
  # wrong, we can still find an encoding tag
36
46
  # (<%# encoding %>) inside the String using a regular
37
47
  # expression
38
- template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
48
+ template_source = source.dup.force_encoding(Encoding::ASCII_8BIT)
39
49
 
40
50
  erb = template_source.gsub(ENCODING_TAG, "")
41
51
  encoding = $2
42
52
 
43
- erb.force_encoding valid_encoding(template.source.dup, encoding)
53
+ erb.force_encoding valid_encoding(source.dup, encoding)
44
54
 
45
55
  # Always make sure we return a String in the default_internal
46
56
  erb.encode!
47
57
 
48
58
  self.class.erb_implementation.new(
49
59
  erb,
50
- escape: (self.class.escape_whitelist.include? template.type),
60
+ escape: (self.class.escape_ignore_list.include? template.type),
51
61
  trim: (self.class.erb_trim_mode == "-")
52
62
  ).src
53
63
  end
@@ -13,7 +13,7 @@ module ActionView
13
13
 
14
14
  # Dup properties so that we don't modify argument
15
15
  properties = Hash[properties]
16
- properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
16
+ properties[:preamble] = ""
17
17
  properties[:postamble] = "@output_buffer.to_s"
18
18
  properties[:bufvar] = "@output_buffer"
19
19
  properties[:escapefunc] = ""
@@ -22,8 +22,12 @@ module ActionView
22
22
  end
23
23
 
24
24
  def evaluate(action_view_erb_handler_context)
25
- pr = eval("proc { #{@src} }", binding, @filename || "(erubi)")
26
- action_view_erb_handler_context.instance_eval(&pr)
25
+ src = @src
26
+ view = Class.new(ActionView::Base) {
27
+ include action_view_erb_handler_context._routes.url_helpers
28
+ class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
29
+ }.empty
30
+ view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
27
31
  end
28
32
 
29
33
  private
@@ -3,7 +3,7 @@
3
3
  module ActionView
4
4
  module Template::Handlers
5
5
  class Html < Raw
6
- def call(template)
6
+ def call(template, source)
7
7
  "ActionView::OutputBuffer.new #{super}"
8
8
  end
9
9
  end
@@ -3,8 +3,8 @@
3
3
  module ActionView
4
4
  module Template::Handlers
5
5
  class Raw
6
- def call(template)
7
- "#{template.source.inspect}.html_safe;"
6
+ def call(template, source)
7
+ "#{source.inspect}.html_safe;"
8
8
  end
9
9
  end
10
10
  end
@@ -1,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deprecation"
4
+
3
5
  module ActionView #:nodoc:
4
6
  # = Action View HTML Template
5
7
  class Template #:nodoc:
6
8
  class HTML #:nodoc:
7
- attr_accessor :type
9
+ attr_reader :type
8
10
 
9
11
  def initialize(string, type = nil)
12
+ unless type
13
+ ActiveSupport::Deprecation.warn "ActionView::Template::HTML#initialize requires a type parameter"
14
+ type = :html
15
+ end
16
+
10
17
  @string = string.to_s
11
- @type = Types[type] || type if type
12
- @type ||= Types[:html]
18
+ @type = type
13
19
  end
14
20
 
15
21
  def identifier
@@ -26,9 +32,12 @@ module ActionView #:nodoc:
26
32
  to_str
27
33
  end
28
34
 
29
- def formats
30
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
35
+ def format
36
+ @type
31
37
  end
38
+
39
+ def formats; Array(format); end
40
+ deprecate :formats
32
41
  end
33
42
  end
34
43
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView #:nodoc:
4
+ class Template #:nodoc:
5
+ class Inline < Template #:nodoc:
6
+ # This finalizer is needed (and exactly with a proc inside another proc)
7
+ # otherwise templates leak in development.
8
+ Finalizer = proc do |method_name, mod| # :nodoc:
9
+ proc do
10
+ mod.module_eval do
11
+ remove_possible_method method_name
12
+ end
13
+ end
14
+ end
15
+
16
+ def compile(mod)
17
+ super
18
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView #:nodoc:
4
+ # = Action View RawFile Template
5
+ class Template #:nodoc:
6
+ class RawFile #:nodoc:
7
+ attr_accessor :type, :format
8
+
9
+ def initialize(filename)
10
+ @filename = filename.to_s
11
+ extname = ::File.extname(filename).delete(".")
12
+ @type = Template::Types[extname] || Template::Types[:text]
13
+ @format = @type.symbol
14
+ end
15
+
16
+ def identifier
17
+ @filename
18
+ end
19
+
20
+ def render(*args)
21
+ ::File.read(@filename)
22
+ end
23
+
24
+ def formats; Array(format); end
25
+ deprecate :formats
26
+ end
27
+ end
28
+ end
@@ -16,7 +16,7 @@ module ActionView
16
16
  alias_method :partial?, :partial
17
17
 
18
18
  def self.build(name, prefix, partial)
19
- virtual = "".dup
19
+ virtual = +""
20
20
  virtual << "#{prefix}/" unless prefix.empty?
21
21
  virtual << (partial ? "_#{name}" : name)
22
22
  new name, prefix, partial, virtual
@@ -63,26 +63,11 @@ module ActionView
63
63
 
64
64
  # Cache the templates returned by the block
65
65
  def cache(key, name, prefix, partial, locals)
66
- if Resolver.caching?
67
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
68
- else
69
- fresh_templates = yield
70
- cached_templates = @data[key][name][prefix][partial][locals]
71
-
72
- if templates_have_changed?(cached_templates, fresh_templates)
73
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
74
- else
75
- cached_templates || NO_TEMPLATES
76
- end
77
- end
66
+ @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
78
67
  end
79
68
 
80
69
  def cache_query(query) # :nodoc:
81
- if Resolver.caching?
82
- @query_cache[query] ||= canonical_no_templates(yield)
83
- else
84
- yield
85
- end
70
+ @query_cache[query] ||= canonical_no_templates(yield)
86
71
  end
87
72
 
88
73
  def clear
@@ -112,19 +97,6 @@ module ActionView
112
97
  def canonical_no_templates(templates)
113
98
  templates.empty? ? NO_TEMPLATES : templates
114
99
  end
115
-
116
- def templates_have_changed?(cached_templates, fresh_templates)
117
- # if either the old or new template list is empty, we don't need to (and can't)
118
- # compare modification times, and instead just check whether the lists are different
119
- if cached_templates.blank? || fresh_templates.blank?
120
- return fresh_templates.blank? != cached_templates.blank?
121
- end
122
-
123
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
124
-
125
- # if a template has changed, it will be now be newer than all the cached templates
126
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
127
- end
128
100
  end
129
101
 
130
102
  cattr_accessor :caching, default: true
@@ -143,35 +115,33 @@ module ActionView
143
115
 
144
116
  # Normalizes the arguments and passes it on to find_templates.
145
117
  def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
146
- cached(key, [name, prefix, partial], details, locals) do
147
- find_templates(name, prefix, partial, details)
148
- end
149
- end
118
+ locals = locals.map(&:to_s).sort!.freeze
150
119
 
151
- def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
152
120
  cached(key, [name, prefix, partial], details, locals) do
153
- find_templates(name, prefix, partial, details, true)
121
+ _find_all(name, prefix, partial, details, key, locals)
154
122
  end
155
123
  end
156
124
 
125
+ alias :find_all_anywhere :find_all
126
+ deprecate :find_all_anywhere
127
+
157
128
  def find_all_with_query(query) # :nodoc:
158
129
  @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
159
130
  end
160
131
 
161
132
  private
162
133
 
134
+ def _find_all(name, prefix, partial, details, key, locals)
135
+ find_templates(name, prefix, partial, details, locals)
136
+ end
137
+
163
138
  delegate :caching?, to: :class
164
139
 
165
140
  # This is what child classes implement. No defaults are needed
166
141
  # because Resolver guarantees that the arguments are present and
167
142
  # normalized.
168
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
169
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method"
170
- end
171
-
172
- # Helpers that builds a path. Useful for building virtual paths.
173
- def build_path(name, prefix, partial)
174
- Path.build(name, prefix, partial)
143
+ def find_templates(name, prefix, partial, details, locals = [])
144
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
175
145
  end
176
146
 
177
147
  # Handles templates caching. If a key is given and caching is on
@@ -180,25 +150,13 @@ module ActionView
180
150
  # resolver is fresher before returning it.
181
151
  def cached(key, path_info, details, locals)
182
152
  name, prefix, partial = path_info
183
- locals = locals.map(&:to_s).sort!
184
153
 
185
154
  if key
186
155
  @cache.cache(key, name, prefix, partial, locals) do
187
- decorate(yield, path_info, details, locals)
156
+ yield
188
157
  end
189
158
  else
190
- decorate(yield, path_info, details, locals)
191
- end
192
- end
193
-
194
- # Ensures all the resolver information is set in the template.
195
- def decorate(templates, path_info, details, locals)
196
- cached = nil
197
- templates.each do |t|
198
- t.locals = locals
199
- t.formats = details[:formats] || [:html] if t.formats.empty?
200
- t.variants = details[:variants] || [] if t.variants.empty?
201
- t.virtual_path ||= (cached ||= build_path(*path_info))
159
+ yield
202
160
  end
203
161
  end
204
162
  end
@@ -209,40 +167,69 @@ module ActionView
209
167
  DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
210
168
 
211
169
  def initialize(pattern = nil)
212
- @pattern = pattern || DEFAULT_PATTERN
170
+ if pattern
171
+ ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
172
+ @pattern = pattern
173
+ else
174
+ @pattern = DEFAULT_PATTERN
175
+ end
176
+ @unbound_templates = Concurrent::Map.new
177
+ super()
178
+ end
179
+
180
+ def clear_cache
181
+ @unbound_templates.clear
213
182
  super()
214
183
  end
215
184
 
216
185
  private
217
186
 
218
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
187
+ def _find_all(name, prefix, partial, details, key, locals)
219
188
  path = Path.build(name, prefix, partial)
220
- query(path, details, details[:formats], outside_app_allowed)
189
+ query(path, details, details[:formats], locals, cache: !!key)
221
190
  end
222
191
 
223
- def query(path, details, formats, outside_app_allowed)
224
- query = build_query(path, details)
225
-
226
- template_paths = find_template_paths(query)
227
- template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
192
+ def query(path, details, formats, locals, cache:)
193
+ template_paths = find_template_paths_from_details(path, details)
194
+ template_paths = reject_files_external_to_app(template_paths)
228
195
 
229
196
  template_paths.map do |template|
230
- handler, format, variant = extract_handler_and_format_and_variant(template)
231
- contents = File.binread(template)
232
-
233
- Template.new(contents, File.expand_path(template), handler,
234
- virtual_path: path.virtual,
235
- format: format,
236
- variant: variant,
237
- updated_at: mtime(template)
238
- )
197
+ unbound_template =
198
+ if cache
199
+ @unbound_templates.compute_if_absent([template, path.virtual]) do
200
+ build_unbound_template(template, path.virtual)
201
+ end
202
+ else
203
+ build_unbound_template(template, path.virtual)
204
+ end
205
+
206
+ unbound_template.bind_locals(locals)
239
207
  end
240
208
  end
241
209
 
210
+ def build_unbound_template(template, virtual_path)
211
+ handler, format, variant = extract_handler_and_format_and_variant(template)
212
+ source = Template::Sources::File.new(template)
213
+
214
+ UnboundTemplate.new(
215
+ source,
216
+ template,
217
+ handler,
218
+ virtual_path: virtual_path,
219
+ format: format,
220
+ variant: variant,
221
+ )
222
+ end
223
+
242
224
  def reject_files_external_to_app(files)
243
225
  files.reject { |filename| !inside_path?(@path, filename) }
244
226
  end
245
227
 
228
+ def find_template_paths_from_details(path, details)
229
+ query = build_query(path, details)
230
+ find_template_paths(query)
231
+ end
232
+
246
233
  def find_template_paths(query)
247
234
  Dir[query].uniq.reject do |filename|
248
235
  File.directory?(filename) ||
@@ -279,70 +266,39 @@ module ActionView
279
266
  end
280
267
 
281
268
  def escape_entry(entry)
282
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze)
283
- end
284
-
285
- # Returns the file mtime from the filesystem.
286
- def mtime(p)
287
- File.mtime(p)
269
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
288
270
  end
289
271
 
290
272
  # Extract handler, formats and variant from path. If a format cannot be found neither
291
273
  # from the path, or the handler, we should return the array of formats given
292
274
  # to the resolver.
293
275
  def extract_handler_and_format_and_variant(path)
294
- pieces = File.basename(path).split(".".freeze)
276
+ pieces = File.basename(path).split(".")
295
277
  pieces.shift
296
278
 
297
279
  extension = pieces.pop
298
280
 
299
281
  handler = Template.handler_for_extension(extension)
300
282
  format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
301
- format &&= Template::Types[format]
283
+ format = if format
284
+ Template::Types[format]&.ref
285
+ else
286
+ if handler.respond_to?(:default_format) # default_format can return nil
287
+ handler.default_format
288
+ else
289
+ nil
290
+ end
291
+ end
302
292
 
293
+ # Template::Types[format] and handler.default_format can return nil
303
294
  [handler, format, variant]
304
295
  end
305
296
  end
306
297
 
307
- # A resolver that loads files from the filesystem. It allows setting your own
308
- # resolving pattern. Such pattern can be a glob string supported by some variables.
309
- #
310
- # ==== Examples
311
- #
312
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
313
- # looking for <tt>users/new</tt> it will produce query glob: <tt>users/new{.{en},}{.{html,js},}{.{erb,haml},}</tt>
314
- #
315
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
316
- #
317
- # This one allows you to keep files with different formats in separate subdirectories,
318
- # eg. <tt>users/new.html</tt> will be loaded from <tt>users/html/new.erb</tt> or <tt>users/new.html.erb</tt>,
319
- # <tt>users/new.js</tt> from <tt>users/js/new.erb</tt> or <tt>users/new.js.erb</tt>, etc.
320
- #
321
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
322
- #
323
- # If you don't specify a pattern then the default will be used.
324
- #
325
- # In order to use any of the customized resolvers above in a Rails application, you just need
326
- # to configure ActionController::Base.view_paths in an initializer, for example:
327
- #
328
- # ActionController::Base.view_paths = FileSystemResolver.new(
329
- # Rails.root.join("app/views"),
330
- # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
331
- # )
332
- #
333
- # ==== Pattern format and variables
334
- #
335
- # Pattern has to be a valid glob string, and it allows you to use the
336
- # following variables:
337
- #
338
- # * <tt>:prefix</tt> - usually the controller path
339
- # * <tt>:action</tt> - name of the action
340
- # * <tt>:locale</tt> - possible locale versions
341
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
342
- # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
343
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
344
- #
298
+ # A resolver that loads files from the filesystem.
345
299
  class FileSystemResolver < PathResolver
300
+ attr_reader :path
301
+
346
302
  def initialize(path, pattern = nil)
347
303
  raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
348
304
  super(pattern)
@@ -362,30 +318,77 @@ module ActionView
362
318
 
363
319
  # An Optimized resolver for Rails' most common case.
364
320
  class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
365
- def build_query(path, details)
366
- query = escape_entry(File.join(@path, path))
321
+ def initialize(path)
322
+ super(path)
323
+ end
367
324
 
368
- exts = EXTENSIONS.map do |ext, prefix|
369
- if ext == :variants && details[ext] == :any
370
- "{#{prefix}*,}"
371
- else
372
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
325
+ private
326
+
327
+ def find_template_paths_from_details(path, details)
328
+ # Instead of checking for every possible path, as our other globs would
329
+ # do, scan the directory for files with the right prefix.
330
+ query = "#{escape_entry(File.join(@path, path))}*"
331
+
332
+ regex = build_regex(path, details)
333
+
334
+ Dir[query].uniq.reject do |filename|
335
+ # This regex match does double duty of finding only files which match
336
+ # details (instead of just matching the prefix) and also filtering for
337
+ # case-insensitive file systems.
338
+ !regex.match?(filename) ||
339
+ File.directory?(filename)
340
+ end.sort_by do |filename|
341
+ # Because we scanned the directory, instead of checking for files
342
+ # one-by-one, they will be returned in an arbitrary order.
343
+ # We can use the matches found by the regex and sort by their index in
344
+ # details.
345
+ match = filename.match(regex)
346
+ EXTENSIONS.keys.reverse.map do |ext|
347
+ if ext == :variants && details[ext] == :any
348
+ match[ext].nil? ? 0 : 1
349
+ elsif match[ext].nil?
350
+ # No match should be last
351
+ details[ext].length
352
+ else
353
+ found = match[ext].to_sym
354
+ details[ext].index(found)
355
+ end
356
+ end
373
357
  end
374
- end.join
358
+ end
375
359
 
376
- query + exts
377
- end
360
+ def build_regex(path, details)
361
+ query = escape_entry(File.join(@path, path))
362
+ exts = EXTENSIONS.map do |ext, prefix|
363
+ match =
364
+ if ext == :variants && details[ext] == :any
365
+ ".*?"
366
+ else
367
+ details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|")
368
+ end
369
+ prefix = Regexp.escape(prefix)
370
+ "(#{prefix}(?<#{ext}>#{match}))?"
371
+ end.join
372
+
373
+ %r{\A#{query}#{exts}\z}
374
+ end
378
375
  end
379
376
 
380
377
  # The same as FileSystemResolver but does not allow templates to store
381
378
  # a virtual path since it is invalid for such resolvers.
382
379
  class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
380
+ private_class_method :new
381
+
383
382
  def self.instances
384
383
  [new(""), new("/")]
385
384
  end
386
385
 
387
- def decorate(*)
388
- super.each { |t| t.virtual_path = nil }
386
+ def build_unbound_template(template, _)
387
+ super(template, nil)
388
+ end
389
+
390
+ def reject_files_external_to_app(files)
391
+ files
389
392
  end
390
393
  end
391
394
  end