actionview 6.1.7.2 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -277
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +3 -3
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +37 -19
  8. data/lib/action_view/buffers.rb +107 -9
  9. data/lib/action_view/cache_expiry.rb +48 -37
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  12. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  13. data/lib/action_view/dependency_tracker.rb +6 -147
  14. data/lib/action_view/deprecator.rb +7 -0
  15. data/lib/action_view/digestor.rb +8 -5
  16. data/lib/action_view/flows.rb +4 -4
  17. data/lib/action_view/gem_version.rb +4 -4
  18. data/lib/action_view/helpers/active_model_helper.rb +3 -3
  19. data/lib/action_view/helpers/asset_tag_helper.rb +200 -60
  20. data/lib/action_view/helpers/asset_url_helper.rb +22 -21
  21. data/lib/action_view/helpers/atom_feed_helper.rb +8 -9
  22. data/lib/action_view/helpers/cache_helper.rb +55 -12
  23. data/lib/action_view/helpers/capture_helper.rb +34 -14
  24. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  25. data/lib/action_view/helpers/controller_helper.rb +8 -2
  26. data/lib/action_view/helpers/csp_helper.rb +3 -3
  27. data/lib/action_view/helpers/csrf_helper.rb +4 -4
  28. data/lib/action_view/helpers/date_helper.rb +123 -57
  29. data/lib/action_view/helpers/debug_helper.rb +6 -4
  30. data/lib/action_view/helpers/form_helper.rb +253 -97
  31. data/lib/action_view/helpers/form_options_helper.rb +72 -34
  32. data/lib/action_view/helpers/form_tag_helper.rb +189 -58
  33. data/lib/action_view/helpers/javascript_helper.rb +4 -5
  34. data/lib/action_view/helpers/number_helper.rb +43 -335
  35. data/lib/action_view/helpers/output_safety_helper.rb +6 -6
  36. data/lib/action_view/helpers/rendering_helper.rb +6 -7
  37. data/lib/action_view/helpers/sanitize_helper.rb +54 -24
  38. data/lib/action_view/helpers/tag_helper.rb +42 -35
  39. data/lib/action_view/helpers/tags/base.rb +16 -77
  40. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  41. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  42. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  43. data/lib/action_view/helpers/tags/collection_select.rb +4 -1
  44. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  48. data/lib/action_view/helpers/tags/file_field.rb +16 -0
  49. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  50. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  51. data/lib/action_view/helpers/tags/select.rb +4 -1
  52. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  53. data/lib/action_view/helpers/tags/time_field.rb +11 -2
  54. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  55. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  56. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  57. data/lib/action_view/helpers/tags.rb +5 -2
  58. data/lib/action_view/helpers/text_helper.rb +180 -97
  59. data/lib/action_view/helpers/translation_helper.rb +14 -45
  60. data/lib/action_view/helpers/url_helper.rb +230 -132
  61. data/lib/action_view/helpers.rb +27 -25
  62. data/lib/action_view/layouts.rb +15 -10
  63. data/lib/action_view/log_subscriber.rb +49 -32
  64. data/lib/action_view/lookup_context.rb +58 -61
  65. data/lib/action_view/model_naming.rb +2 -2
  66. data/lib/action_view/path_registry.rb +57 -0
  67. data/lib/action_view/path_set.rb +28 -35
  68. data/lib/action_view/railtie.rb +44 -9
  69. data/lib/action_view/record_identifier.rb +16 -9
  70. data/lib/action_view/render_parser.rb +188 -0
  71. data/lib/action_view/renderer/abstract_renderer.rb +3 -3
  72. data/lib/action_view/renderer/collection_renderer.rb +10 -2
  73. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +21 -3
  74. data/lib/action_view/renderer/partial_renderer.rb +3 -36
  75. data/lib/action_view/renderer/renderer.rb +6 -4
  76. data/lib/action_view/renderer/streaming_template_renderer.rb +6 -5
  77. data/lib/action_view/renderer/template_renderer.rb +9 -4
  78. data/lib/action_view/rendering.rb +25 -7
  79. data/lib/action_view/ripper_ast_parser.rb +198 -0
  80. data/lib/action_view/routing_url_for.rb +8 -5
  81. data/lib/action_view/template/error.rb +122 -14
  82. data/lib/action_view/template/handlers/builder.rb +4 -4
  83. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  84. data/lib/action_view/template/handlers/erb.rb +79 -1
  85. data/lib/action_view/template/handlers.rb +4 -4
  86. data/lib/action_view/template/html.rb +4 -4
  87. data/lib/action_view/template/inline.rb +3 -3
  88. data/lib/action_view/template/raw_file.rb +4 -4
  89. data/lib/action_view/template/renderable.rb +1 -1
  90. data/lib/action_view/template/resolver.rb +96 -313
  91. data/lib/action_view/template/text.rb +4 -4
  92. data/lib/action_view/template/types.rb +25 -32
  93. data/lib/action_view/template.rb +245 -41
  94. data/lib/action_view/template_details.rb +66 -0
  95. data/lib/action_view/template_path.rb +66 -0
  96. data/lib/action_view/test_case.rb +182 -23
  97. data/lib/action_view/testing/resolvers.rb +11 -12
  98. data/lib/action_view/unbound_template.rb +43 -7
  99. data/lib/action_view/version.rb +1 -1
  100. data/lib/action_view/view_paths.rb +19 -28
  101. data/lib/action_view.rb +6 -4
  102. data/lib/assets/compiled/rails-ujs.js +36 -5
  103. metadata +32 -25
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "strscan"
4
+ require "active_support/core_ext/erb/util"
5
+
3
6
  module ActionView
4
7
  class Template
5
8
  module Handlers
@@ -16,8 +19,13 @@ module ActionView
16
19
  # Do not escape templates of these mime types.
17
20
  class_attribute :escape_ignore_list, default: ["text/plain"]
18
21
 
22
+ # Strip trailing newlines from rendered output
23
+ class_attribute :strip_trailing_newlines, default: false
24
+
19
25
  ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
20
26
 
27
+ LocationParsingError = Class.new(StandardError) # :nodoc:
28
+
21
29
  def self.call(template, source)
22
30
  new.call(template, source)
23
31
  end
@@ -30,6 +38,26 @@ module ActionView
30
38
  true
31
39
  end
32
40
 
41
+ # Translate an error location returned by ErrorHighlight to the correct
42
+ # source location inside the template.
43
+ def translate_location(spot, backtrace_location, source)
44
+ # Tokenize the source line
45
+ tokens = ::ERB::Util.tokenize(source.lines[backtrace_location.lineno - 1])
46
+ new_first_column = find_offset(spot[:snippet], tokens, spot[:first_column])
47
+ lineno_delta = spot[:first_lineno] - backtrace_location.lineno
48
+ spot[:first_lineno] -= lineno_delta
49
+ spot[:last_lineno] -= lineno_delta
50
+
51
+ column_delta = spot[:first_column] - new_first_column
52
+ spot[:first_column] -= column_delta
53
+ spot[:last_column] -= column_delta
54
+ spot[:script_lines] = source.lines
55
+
56
+ spot
57
+ rescue NotImplementedError, LocationParsingError
58
+ nil
59
+ end
60
+
33
61
  def call(template, source)
34
62
  # First, convert to BINARY, so in case the encoding is
35
63
  # wrong, we can still find an encoding tag
@@ -45,6 +73,9 @@ module ActionView
45
73
  # Always make sure we return a String in the default_internal
46
74
  erb.encode!
47
75
 
76
+ # Strip trailing newlines from the template if enabled
77
+ erb.chomp! if strip_trailing_newlines
78
+
48
79
  options = {
49
80
  escape: (self.class.escape_ignore_list.include? template.type),
50
81
  trim: (self.class.erb_trim_mode == "-")
@@ -52,7 +83,7 @@ module ActionView
52
83
 
53
84
  if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
54
85
  options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->';"
55
- options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer.to_s"
86
+ options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
56
87
  end
57
88
 
58
89
  self.class.erb_implementation.new(erb, options).src
@@ -73,6 +104,53 @@ module ActionView
73
104
  # Otherwise, raise an exception
74
105
  raise WrongEncodingError.new(string, string.encoding)
75
106
  end
107
+
108
+ def find_offset(compiled, source_tokens, error_column)
109
+ compiled = StringScanner.new(compiled)
110
+
111
+ passed_tokens = []
112
+
113
+ while tok = source_tokens.shift
114
+ tok_name, str = *tok
115
+ case tok_name
116
+ when :TEXT
117
+ loop do
118
+ break if compiled.match?(str)
119
+ compiled.getch
120
+ end
121
+ raise LocationParsingError unless compiled.scan(str)
122
+ when :CODE
123
+ if compiled.pos > error_column
124
+ raise LocationParsingError, "We went too far"
125
+ end
126
+
127
+ if compiled.pos + str.bytesize >= error_column
128
+ offset = error_column - compiled.pos
129
+ return passed_tokens.map(&:last).join.bytesize + offset
130
+ else
131
+ unless compiled.scan(str)
132
+ raise LocationParsingError, "Couldn't find code snippet"
133
+ end
134
+ end
135
+ when :OPEN
136
+ next_tok = source_tokens.first.last
137
+ loop do
138
+ break if compiled.match?(next_tok)
139
+ compiled.getch
140
+ end
141
+ when :CLOSE
142
+ next_tok = source_tokens.first.last
143
+ loop do
144
+ break if compiled.match?(next_tok)
145
+ compiled.getch
146
+ end
147
+ else
148
+ raise LocationParsingError, "Not implemented: #{tok.first}"
149
+ end
150
+
151
+ passed_tokens << tok
152
+ end
153
+ end
76
154
  end
77
155
  end
78
156
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
4
- # = Action View Template Handlers
5
- class Template #:nodoc:
6
- module Handlers #:nodoc:
3
+ module ActionView # :nodoc:
4
+ class Template # :nodoc:
5
+ # = Action View Template Handlers
6
+ module Handlers # :nodoc:
7
7
  autoload :Raw, "action_view/template/handlers/raw"
8
8
  autoload :ERB, "action_view/template/handlers/erb"
9
9
  autoload :Html, "action_view/template/handlers/html"
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
4
- # = Action View HTML Template
5
- class Template #:nodoc:
6
- class HTML #:nodoc:
3
+ module ActionView # :nodoc:
4
+ class Template # :nodoc:
5
+ # = Action View HTML Template
6
+ class HTML # :nodoc:
7
7
  attr_reader :type
8
8
 
9
9
  def initialize(string, type)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
4
- class Template #:nodoc:
5
- class Inline < Template #:nodoc:
3
+ module ActionView # :nodoc:
4
+ class Template # :nodoc:
5
+ class Inline < Template # :nodoc:
6
6
  # This finalizer is needed (and exactly with a proc inside another proc)
7
7
  # otherwise templates leak in development.
8
8
  Finalizer = proc do |method_name, mod| # :nodoc:
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
4
- # = Action View RawFile Template
5
- class Template #:nodoc:
6
- class RawFile #:nodoc:
3
+ module ActionView # :nodoc:
4
+ class Template # :nodoc:
5
+ # = Action View RawFile Template
6
+ class RawFile # :nodoc:
7
7
  attr_accessor :type, :format
8
8
 
9
9
  def initialize(filename)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
- # = Action View Renderable Template for objects that respond to #render_in
5
4
  class Template
5
+ # = Action View Renderable Template for objects that respond to #render_in
6
6
  class Renderable # :nodoc:
7
7
  def initialize(renderable)
8
8
  @renderable = renderable
@@ -10,36 +10,17 @@ require "concurrent/map"
10
10
  module ActionView
11
11
  # = Action View Resolver
12
12
  class Resolver
13
- # Keeps all information about view path and builds virtual path.
14
- class Path
15
- attr_reader :name, :prefix, :partial, :virtual
16
- alias_method :partial?, :partial
17
-
18
- def self.build(name, prefix, partial)
19
- virtual = +""
20
- virtual << "#{prefix}/" unless prefix.empty?
21
- virtual << (partial ? "_#{name}" : name)
22
- new name, prefix, partial, virtual
23
- end
24
-
25
- def initialize(name, prefix, partial, virtual)
26
- @name = name
27
- @prefix = prefix
28
- @partial = partial
29
- @virtual = virtual
30
- end
31
-
32
- def to_str
33
- @virtual
34
- end
35
- alias :to_s :to_str
36
- end
13
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
37
14
 
38
15
  class PathParser # :nodoc:
16
+ ParsedPath = Struct.new(:path, :details)
17
+
39
18
  def build_path_regex
40
- handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
41
- formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
42
- locales = "[a-z]{2}(?:-[A-Z]{2})?"
19
+ handlers = Regexp.union(Template::Handlers.extensions.map(&:to_s))
20
+ formats = Regexp.union(Template::Types.symbols.map(&:to_s))
21
+ available_locales = I18n.available_locales.map(&:to_s)
22
+ regular_locales = [/[a-z]{2}(?:[-_][A-Z]{2})?/]
23
+ locales = Regexp.union(available_locales + regular_locales)
43
24
  variants = "[^.]*"
44
25
 
45
26
  %r{
@@ -58,79 +39,15 @@ module ActionView
58
39
  def parse(path)
59
40
  @regex ||= build_path_regex
60
41
  match = @regex.match(path)
61
- {
62
- prefix: match[:prefix] || "",
63
- action: match[:action],
64
- partial: !!match[:partial],
65
- locale: match[:locale]&.to_sym,
66
- handler: match[:handler]&.to_sym,
67
- format: match[:format]&.to_sym,
68
- variant: match[:variant]
69
- }
70
- end
71
- end
72
-
73
- # Threadsafe template cache
74
- class Cache #:nodoc:
75
- class SmallCache < Concurrent::Map
76
- def initialize(options = {})
77
- super(options.merge(initial_capacity: 2))
78
- end
79
- end
80
-
81
- # Preallocate all the default blocks for performance/memory consumption reasons
82
- PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
83
- PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
84
- NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
85
- KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
86
-
87
- # Usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
88
- NO_TEMPLATES = [].freeze
89
-
90
- def initialize
91
- @data = SmallCache.new(&KEY_BLOCK)
92
- @query_cache = SmallCache.new
93
- end
94
-
95
- def inspect
96
- "#{to_s[0..-2]} keys=#{@data.size} queries=#{@query_cache.size}>"
97
- end
98
-
99
- # Cache the templates returned by the block
100
- def cache(key, name, prefix, partial, locals)
101
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
102
- end
103
-
104
- def cache_query(query) # :nodoc:
105
- @query_cache[query] ||= canonical_no_templates(yield)
106
- end
107
-
108
- def clear
109
- @data.clear
110
- @query_cache.clear
111
- end
112
-
113
- # Get the cache size. Do not call this
114
- # method. This method is not guaranteed to be here ever.
115
- def size # :nodoc:
116
- size = 0
117
- @data.each_value do |v1|
118
- v1.each_value do |v2|
119
- v2.each_value do |v3|
120
- v3.each_value do |v4|
121
- size += v4.size
122
- end
123
- end
124
- end
125
- end
126
-
127
- size + @query_cache.size
42
+ path = TemplatePath.build(match[:action], match[:prefix] || "", !!match[:partial])
43
+ details = TemplateDetails.new(
44
+ match[:locale]&.to_sym,
45
+ match[:handler]&.to_sym,
46
+ match[:format]&.to_sym,
47
+ match[:variant]&.to_sym
48
+ )
49
+ ParsedPath.new(path, details)
128
50
  end
129
-
130
- private
131
- def canonical_no_templates(templates)
132
- templates.empty? ? NO_TEMPLATES : templates
133
- end
134
51
  end
135
52
 
136
53
  cattr_accessor :caching, default: true
@@ -139,25 +56,22 @@ module ActionView
139
56
  alias :caching? :caching
140
57
  end
141
58
 
142
- def initialize
143
- @cache = Cache.new
144
- end
145
-
146
59
  def clear_cache
147
- @cache.clear
148
60
  end
149
61
 
150
62
  # Normalizes the arguments and passes it on to find_templates.
151
63
  def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
152
- locals = locals.map(&:to_s).sort!.freeze
64
+ _find_all(name, prefix, partial, details, key, locals)
65
+ end
153
66
 
154
- cached(key, [name, prefix, partial], details, locals) do
155
- _find_all(name, prefix, partial, details, key, locals)
156
- end
67
+ def built_templates # :nodoc:
68
+ # Used for error pages
69
+ []
157
70
  end
158
71
 
159
- def find_all_with_query(query) # :nodoc:
160
- @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
72
+ def all_template_paths # :nodoc:
73
+ # Not implemented by default
74
+ []
161
75
  end
162
76
 
163
77
  private
@@ -173,34 +87,18 @@ module ActionView
173
87
  def find_templates(name, prefix, partial, details, locals = [])
174
88
  raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
175
89
  end
176
-
177
- # Handles templates caching. If a key is given and caching is on
178
- # always check the cache before hitting the resolver. Otherwise,
179
- # it always hits the resolver but if the key is present, check if the
180
- # resolver is fresher before returning it.
181
- def cached(key, path_info, details, locals)
182
- name, prefix, partial = path_info
183
-
184
- if key
185
- @cache.cache(key, name, prefix, partial, locals) do
186
- yield
187
- end
188
- else
189
- yield
190
- end
191
- end
192
90
  end
193
91
 
194
- # An abstract class that implements a Resolver with path semantics.
195
- class PathResolver < Resolver #:nodoc:
196
- EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
197
- DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
92
+ # A resolver that loads files from the filesystem.
93
+ class FileSystemResolver < Resolver
94
+ attr_reader :path
198
95
 
199
- def initialize
200
- @pattern = DEFAULT_PATTERN
96
+ def initialize(path)
97
+ raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
201
98
  @unbound_templates = Concurrent::Map.new
202
99
  @path_parser = PathParser.new
203
- super
100
+ @path = File.expand_path(path)
101
+ super()
204
102
  end
205
103
 
206
104
  def clear_cache
@@ -209,26 +107,41 @@ module ActionView
209
107
  super
210
108
  end
211
109
 
212
- private
213
- def _find_all(name, prefix, partial, details, key, locals)
214
- path = Path.build(name, prefix, partial)
215
- query(path, details, details[:formats], locals, cache: !!key)
110
+ def to_s
111
+ @path.to_s
112
+ end
113
+ alias :to_path :to_s
114
+
115
+ def eql?(resolver)
116
+ self.class.equal?(resolver.class) && to_path == resolver.to_path
117
+ end
118
+ alias :== :eql?
119
+
120
+ def all_template_paths # :nodoc:
121
+ paths = template_glob("**/*")
122
+ paths.map do |filename|
123
+ filename.from(@path.size + 1).remove(/\.[^\/]*\z/)
124
+ end.uniq.map do |filename|
125
+ TemplatePath.parse(filename)
216
126
  end
127
+ end
217
128
 
218
- def query(path, details, formats, locals, cache:)
219
- template_paths = find_template_paths_from_details(path, details)
220
- template_paths = reject_files_external_to_app(template_paths)
129
+ def built_templates # :nodoc:
130
+ @unbound_templates.values.flatten.flat_map(&:built_templates)
131
+ end
132
+
133
+ private
134
+ def _find_all(name, prefix, partial, details, key, locals)
135
+ requested_details = key || TemplateDetails::Requested.new(**details)
136
+ cache = key ? @unbound_templates : Concurrent::Map.new
221
137
 
222
- template_paths.map do |template|
223
- unbound_template =
224
- if cache
225
- @unbound_templates.compute_if_absent([template, path.virtual]) do
226
- build_unbound_template(template, path.virtual)
227
- end
228
- else
229
- build_unbound_template(template, path.virtual)
230
- end
138
+ unbound_templates =
139
+ cache.compute_if_absent(TemplatePath.virtual(name, prefix, partial)) do
140
+ path = TemplatePath.build(name, prefix, partial)
141
+ unbound_templates_from_path(path)
142
+ end
231
143
 
144
+ filter_and_sort_by_details(unbound_templates, requested_details).map do |unbound_template|
232
145
  unbound_template.bind_locals(locals)
233
146
  end
234
147
  end
@@ -237,196 +150,66 @@ module ActionView
237
150
  Template::Sources::File.new(template)
238
151
  end
239
152
 
240
- def build_unbound_template(template, virtual_path)
241
- handler, format, variant = extract_handler_and_format_and_variant(template)
153
+ def build_unbound_template(template)
154
+ parsed = @path_parser.parse(template.from(@path.size + 1))
155
+ details = parsed.details
242
156
  source = source_for_template(template)
243
157
 
244
158
  UnboundTemplate.new(
245
159
  source,
246
160
  template,
247
- handler,
248
- virtual_path: virtual_path,
249
- format: format,
250
- variant: variant,
161
+ details: details,
162
+ virtual_path: parsed.path.virtual,
251
163
  )
252
164
  end
253
165
 
254
- def reject_files_external_to_app(files)
255
- files.reject { |filename| !inside_path?(@path, filename) }
256
- end
257
-
258
- def find_template_paths_from_details(path, details)
166
+ def unbound_templates_from_path(path)
259
167
  if path.name.include?(".")
260
- ActiveSupport::Deprecation.warn("Rendering actions with '.' in the name is deprecated: #{path}")
261
- end
262
-
263
- query = build_query(path, details)
264
- find_template_paths(query)
265
- end
266
-
267
- def find_template_paths(query)
268
- Dir[query].uniq.reject do |filename|
269
- File.directory?(filename) ||
270
- # deals with case-insensitive file systems.
271
- !File.fnmatch(query, filename, File::FNM_EXTGLOB)
272
- end
273
- end
274
-
275
- def inside_path?(path, filename)
276
- filename = File.expand_path(filename)
277
- path = File.join(path, "")
278
- filename.start_with?(path)
279
- end
280
-
281
- # Helper for building query glob string based on resolver's pattern.
282
- def build_query(path, details)
283
- query = @pattern.dup
284
-
285
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
286
- query.gsub!(/:prefix(\/)?/, prefix)
287
-
288
- partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
289
- query.gsub!(":action", partial)
290
-
291
- details.each do |ext, candidates|
292
- if ext == :variants && candidates == :any
293
- query.gsub!(/:#{ext}/, "*")
294
- else
295
- query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
296
- end
168
+ return []
297
169
  end
298
170
 
299
- File.expand_path(query, @path)
300
- end
301
-
302
- def escape_entry(entry)
303
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
304
- end
305
-
306
- # Extract handler, formats and variant from path. If a format cannot be found neither
307
- # from the path, or the handler, we should return the array of formats given
308
- # to the resolver.
309
- def extract_handler_and_format_and_variant(path)
310
- details = @path_parser.parse(path)
311
-
312
- handler = Template.handler_for_extension(details[:handler])
313
- format = details[:format] || handler.try(:default_format)
314
- variant = details[:variant]
315
-
316
- # Template::Types[format] and handler.default_format can return nil
317
- [handler, format, variant]
318
- end
319
- end
320
-
321
- # A resolver that loads files from the filesystem.
322
- class FileSystemResolver < PathResolver
323
- attr_reader :path
324
-
325
- def initialize(path)
326
- raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
327
- super()
328
- @path = File.expand_path(path)
329
- end
330
-
331
- def to_s
332
- @path.to_s
333
- end
334
- alias :to_path :to_s
335
-
336
- def eql?(resolver)
337
- self.class.equal?(resolver.class) && to_path == resolver.to_path
338
- end
339
- alias :== :eql?
340
- end
341
-
342
- # An Optimized resolver for Rails' most common case.
343
- class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
344
- def initialize(path)
345
- super(path)
346
- end
347
-
348
- private
349
- def find_candidate_template_paths(path)
350
171
  # Instead of checking for every possible path, as our other globs would
351
172
  # do, scan the directory for files with the right prefix.
352
- query = "#{escape_entry(File.join(@path, path))}*"
173
+ paths = template_glob("#{escape_entry(path.to_s)}*")
353
174
 
354
- Dir[query].reject do |filename|
355
- File.directory?(filename)
175
+ paths.map do |path|
176
+ build_unbound_template(path)
177
+ end.select do |template|
178
+ # Select for exact virtual path match, including case sensitivity
179
+ template.virtual_path == path.virtual
356
180
  end
357
181
  end
358
182
 
359
- def find_template_paths_from_details(path, details)
360
- if path.name.include?(".")
361
- # Fall back to the unoptimized resolver, which will warn
362
- return super
183
+ def filter_and_sort_by_details(templates, requested_details)
184
+ filtered_templates = templates.select do |template|
185
+ template.details.matches?(requested_details)
363
186
  end
364
187
 
365
- candidates = find_candidate_template_paths(path)
366
-
367
- regex = build_regex(path, details)
368
-
369
- candidates.uniq.reject do |filename|
370
- # This regex match does double duty of finding only files which match
371
- # details (instead of just matching the prefix) and also filtering for
372
- # case-insensitive file systems.
373
- !regex.match?(filename) ||
374
- File.directory?(filename)
375
- end.sort_by do |filename|
376
- # Because we scanned the directory, instead of checking for files
377
- # one-by-one, they will be returned in an arbitrary order.
378
- # We can use the matches found by the regex and sort by their index in
379
- # details.
380
- match = filename.match(regex)
381
- EXTENSIONS.keys.map do |ext|
382
- if ext == :variants && details[ext] == :any
383
- match[ext].nil? ? 0 : 1
384
- elsif match[ext].nil?
385
- # No match should be last
386
- details[ext].length
387
- else
388
- found = match[ext].to_sym
389
- details[ext].index(found)
390
- end
188
+ if filtered_templates.count > 1
189
+ filtered_templates.sort_by! do |template|
190
+ template.details.sort_key_for(requested_details)
391
191
  end
392
192
  end
393
- end
394
193
 
395
- def build_regex(path, details)
396
- query = Regexp.escape(File.join(@path, path))
397
- exts = EXTENSIONS.map do |ext, prefix|
398
- match =
399
- if ext == :variants && details[ext] == :any
400
- ".*?"
401
- else
402
- arr = details[ext].compact
403
- arr.uniq!
404
- arr.map! { |e| Regexp.escape(e) }
405
- arr.join("|")
406
- end
407
- prefix = Regexp.escape(prefix)
408
- "(#{prefix}(?<#{ext}>#{match}))?"
409
- end.join
410
-
411
- %r{\A#{query}#{exts}\z}
194
+ filtered_templates
412
195
  end
413
- end
414
196
 
415
- # The same as FileSystemResolver but does not allow templates to store
416
- # a virtual path since it is invalid for such resolvers.
417
- class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
418
- private_class_method :new
197
+ # Safe glob within @path
198
+ def template_glob(glob)
199
+ query = File.join(escape_entry(@path), glob)
200
+ path_with_slash = File.join(@path, "")
419
201
 
420
- def self.instances
421
- [new(""), new("/")]
422
- end
202
+ Dir.glob(query).filter_map do |filename|
203
+ filename = File.expand_path(filename)
204
+ next if File.directory?(filename)
205
+ next unless filename.start_with?(path_with_slash)
423
206
 
424
- def build_unbound_template(template, _)
425
- super(template, nil)
426
- end
207
+ filename
208
+ end
209
+ end
427
210
 
428
- def reject_files_external_to_app(files)
429
- files
430
- end
211
+ def escape_entry(entry)
212
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
213
+ end
431
214
  end
432
215
  end