actionview 6.1.7.2 → 7.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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