actionview 4.2.11.1 → 6.1.5

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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +232 -186
  3. data/MIT-LICENSE +1 -2
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +115 -39
  6. data/lib/action_view/buffers.rb +18 -1
  7. data/lib/action_view/cache_expiry.rb +52 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker.rb +61 -21
  10. data/lib/action_view/digestor.rb +89 -85
  11. data/lib/action_view/flows.rb +11 -12
  12. data/lib/action_view/gem_version.rb +6 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +16 -11
  14. data/lib/action_view/helpers/asset_tag_helper.rb +282 -83
  15. data/lib/action_view/helpers/asset_url_helper.rb +175 -69
  16. data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
  17. data/lib/action_view/helpers/cache_helper.rb +107 -43
  18. data/lib/action_view/helpers/capture_helper.rb +20 -13
  19. data/lib/action_view/helpers/controller_helper.rb +15 -4
  20. data/lib/action_view/helpers/csp_helper.rb +26 -0
  21. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  22. data/lib/action_view/helpers/date_helper.rb +232 -130
  23. data/lib/action_view/helpers/debug_helper.rb +7 -6
  24. data/lib/action_view/helpers/form_helper.rb +808 -146
  25. data/lib/action_view/helpers/form_options_helper.rb +124 -78
  26. data/lib/action_view/helpers/form_tag_helper.rb +120 -74
  27. data/lib/action_view/helpers/javascript_helper.rb +33 -17
  28. data/lib/action_view/helpers/number_helper.rb +87 -62
  29. data/lib/action_view/helpers/output_safety_helper.rb +36 -4
  30. data/lib/action_view/helpers/rendering_helper.rb +21 -10
  31. data/lib/action_view/helpers/sanitize_helper.rb +30 -31
  32. data/lib/action_view/helpers/tag_helper.rb +269 -68
  33. data/lib/action_view/helpers/tags/base.rb +141 -97
  34. data/lib/action_view/helpers/tags/check_box.rb +20 -19
  35. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  37. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  38. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  39. data/lib/action_view/helpers/tags/collection_select.rb +4 -2
  40. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  41. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  42. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  43. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  47. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  48. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  49. data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
  50. data/lib/action_view/helpers/tags/label.rb +7 -2
  51. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  52. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  54. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  55. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  56. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  58. data/lib/action_view/helpers/tags/select.rb +11 -10
  59. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  61. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  62. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  63. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  64. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  65. data/lib/action_view/helpers/tags/translator.rb +15 -16
  66. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  67. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  68. data/lib/action_view/helpers/tags.rb +3 -1
  69. data/lib/action_view/helpers/text_helper.rb +56 -38
  70. data/lib/action_view/helpers/translation_helper.rb +150 -68
  71. data/lib/action_view/helpers/url_helper.rb +284 -117
  72. data/lib/action_view/helpers.rb +5 -3
  73. data/lib/action_view/layouts.rb +68 -63
  74. data/lib/action_view/log_subscriber.rb +77 -10
  75. data/lib/action_view/lookup_context.rb +134 -91
  76. data/lib/action_view/model_naming.rb +3 -1
  77. data/lib/action_view/path_set.rb +26 -24
  78. data/lib/action_view/railtie.rb +62 -13
  79. data/lib/action_view/record_identifier.rb +53 -26
  80. data/lib/action_view/renderer/abstract_renderer.rb +151 -14
  81. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  82. data/lib/action_view/renderer/object_renderer.rb +34 -0
  83. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +55 -303
  85. data/lib/action_view/renderer/renderer.rb +66 -9
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
  87. data/lib/action_view/renderer/template_renderer.rb +82 -73
  88. data/lib/action_view/rendering.rb +71 -45
  89. data/lib/action_view/routing_url_for.rb +34 -23
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template/error.rb +44 -29
  92. data/lib/action_view/template/handlers/builder.rb +12 -13
  93. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  94. data/lib/action_view/template/handlers/erb.rb +23 -89
  95. data/lib/action_view/template/handlers/html.rb +11 -0
  96. data/lib/action_view/template/handlers/raw.rb +4 -4
  97. data/lib/action_view/template/handlers.rb +12 -8
  98. data/lib/action_view/template/html.rb +10 -11
  99. data/lib/action_view/template/inline.rb +22 -0
  100. data/lib/action_view/template/raw_file.rb +25 -0
  101. data/lib/action_view/template/renderable.rb +24 -0
  102. data/lib/action_view/template/resolver.rb +263 -197
  103. data/lib/action_view/template/sources/file.rb +17 -0
  104. data/lib/action_view/template/sources.rb +13 -0
  105. data/lib/action_view/template/text.rb +8 -10
  106. data/lib/action_view/template/types.rb +18 -18
  107. data/lib/action_view/template.rb +108 -92
  108. data/lib/action_view/test_case.rb +66 -53
  109. data/lib/action_view/testing/resolvers.rb +24 -33
  110. data/lib/action_view/unbound_template.rb +31 -0
  111. data/lib/action_view/version.rb +3 -1
  112. data/lib/action_view/view_paths.rb +73 -58
  113. data/lib/action_view.rb +14 -8
  114. data/lib/assets/compiled/rails-ujs.js +746 -0
  115. metadata +42 -29
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  117. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pathname"
2
4
  require "active_support/core_ext/class"
3
5
  require "active_support/core_ext/module/attribute_accessors"
4
- require 'active_support/core_ext/string/filters'
5
6
  require "action_view/template"
6
7
  require "thread"
7
- require "thread_safe"
8
+ require "concurrent/map"
8
9
 
9
10
  module ActionView
10
11
  # = Action View Resolver
@@ -15,7 +16,7 @@ module ActionView
15
16
  alias_method :partial?, :partial
16
17
 
17
18
  def self.build(name, prefix, partial)
18
- virtual = ""
19
+ virtual = +""
19
20
  virtual << "#{prefix}/" unless prefix.empty?
20
21
  virtual << (partial ? "_#{name}" : name)
21
22
  new name, prefix, partial, virtual
@@ -34,69 +35,105 @@ module ActionView
34
35
  alias :to_s :to_str
35
36
  end
36
37
 
38
+ class PathParser # :nodoc:
39
+ 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})?"
43
+ variants = "[^.]*"
44
+
45
+ %r{
46
+ \A
47
+ (?:(?<prefix>.*)/)?
48
+ (?<partial>_)?
49
+ (?<action>.*?)
50
+ (?:\.(?<locale>#{locales}))??
51
+ (?:\.(?<format>#{formats}))??
52
+ (?:\+(?<variant>#{variants}))??
53
+ (?:\.(?<handler>#{handlers}))?
54
+ \z
55
+ }x
56
+ end
57
+
58
+ def parse(path)
59
+ @regex ||= build_path_regex
60
+ 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
+
37
73
  # Threadsafe template cache
38
74
  class Cache #:nodoc:
39
- class SmallCache < ThreadSafe::Cache
75
+ class SmallCache < Concurrent::Map
40
76
  def initialize(options = {})
41
- super(options.merge(:initial_capacity => 2))
77
+ super(options.merge(initial_capacity: 2))
42
78
  end
43
79
  end
44
80
 
45
- # preallocate all the default blocks for performance/memory consumption reasons
46
- PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new}
47
- PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)}
48
- NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)}
49
- KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)}
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) }
50
86
 
51
- # usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
87
+ # Usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
52
88
  NO_TEMPLATES = [].freeze
53
89
 
54
90
  def initialize
55
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}>"
56
97
  end
57
98
 
58
99
  # Cache the templates returned by the block
59
100
  def cache(key, name, prefix, partial, locals)
60
- if Resolver.caching?
61
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
62
- else
63
- fresh_templates = yield
64
- cached_templates = @data[key][name][prefix][partial][locals]
65
-
66
- if templates_have_changed?(cached_templates, fresh_templates)
67
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
68
- else
69
- cached_templates || NO_TEMPLATES
70
- end
71
- end
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)
72
106
  end
73
107
 
74
108
  def clear
75
109
  @data.clear
110
+ @query_cache.clear
76
111
  end
77
112
 
78
- private
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
79
126
 
80
- def canonical_no_templates(templates)
81
- templates.empty? ? NO_TEMPLATES : templates
127
+ size + @query_cache.size
82
128
  end
83
129
 
84
- def templates_have_changed?(cached_templates, fresh_templates)
85
- # if either the old or new template list is empty, we don't need to (and can't)
86
- # compare modification times, and instead just check whether the lists are different
87
- if cached_templates.blank? || fresh_templates.blank?
88
- return fresh_templates.blank? != cached_templates.blank?
130
+ private
131
+ def canonical_no_templates(templates)
132
+ templates.empty? ? NO_TEMPLATES : templates
89
133
  end
90
-
91
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
92
-
93
- # if a template has changed, it will be now be newer than all the cached templates
94
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
95
- end
96
134
  end
97
135
 
98
- cattr_accessor :caching
99
- self.caching = true
136
+ cattr_accessor :caching, default: true
100
137
 
101
138
  class << self
102
139
  alias :caching? :caching
@@ -111,220 +148,183 @@ module ActionView
111
148
  end
112
149
 
113
150
  # Normalizes the arguments and passes it on to find_templates.
114
- def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
151
+ def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
152
+ locals = locals.map(&:to_s).sort!.freeze
153
+
115
154
  cached(key, [name, prefix, partial], details, locals) do
116
- find_templates(name, prefix, partial, details, false)
155
+ _find_all(name, prefix, partial, details, key, locals)
117
156
  end
118
157
  end
119
158
 
120
- def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[])
121
- cached(key, [name, prefix, partial], details, locals) do
122
- find_templates(name, prefix, partial, details, true)
123
- end
159
+ def find_all_with_query(query) # :nodoc:
160
+ @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
124
161
  end
125
162
 
126
163
  private
164
+ def _find_all(name, prefix, partial, details, key, locals)
165
+ find_templates(name, prefix, partial, details, locals)
166
+ end
127
167
 
128
168
  delegate :caching?, to: :class
129
169
 
130
170
  # This is what child classes implement. No defaults are needed
131
171
  # because Resolver guarantees that the arguments are present and
132
172
  # normalized.
133
- def find_templates(name, prefix, partial, details, outside_app_allowed)
134
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed) method"
135
- end
136
-
137
- # Helpers that builds a path. Useful for building virtual paths.
138
- def build_path(name, prefix, partial)
139
- Path.build(name, prefix, partial)
173
+ def find_templates(name, prefix, partial, details, locals = [])
174
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
140
175
  end
141
176
 
142
177
  # Handles templates caching. If a key is given and caching is on
143
178
  # always check the cache before hitting the resolver. Otherwise,
144
179
  # it always hits the resolver but if the key is present, check if the
145
180
  # resolver is fresher before returning it.
146
- def cached(key, path_info, details, locals) #:nodoc:
181
+ def cached(key, path_info, details, locals)
147
182
  name, prefix, partial = path_info
148
- locals = locals.map { |x| x.to_s }.sort!
149
183
 
150
184
  if key
151
185
  @cache.cache(key, name, prefix, partial, locals) do
152
- decorate(yield, path_info, details, locals)
186
+ yield
153
187
  end
154
188
  else
155
- decorate(yield, path_info, details, locals)
156
- end
157
- end
158
-
159
- # Ensures all the resolver information is set in the template.
160
- def decorate(templates, path_info, details, locals) #:nodoc:
161
- cached = nil
162
- templates.each do |t|
163
- t.locals = locals
164
- t.formats = details[:formats] || [:html] if t.formats.empty?
165
- t.variants = details[:variants] || [] if t.variants.empty?
166
- t.virtual_path ||= (cached ||= build_path(*path_info))
189
+ yield
167
190
  end
168
191
  end
169
192
  end
170
193
 
171
194
  # An abstract class that implements a Resolver with path semantics.
172
195
  class PathResolver < Resolver #:nodoc:
173
- EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." }
196
+ EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
174
197
  DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
175
198
 
176
- def initialize(pattern=nil)
177
- @pattern = pattern || DEFAULT_PATTERN
178
- super()
199
+ def initialize
200
+ @pattern = DEFAULT_PATTERN
201
+ @unbound_templates = Concurrent::Map.new
202
+ @path_parser = PathParser.new
203
+ super
179
204
  end
180
205
 
181
- private
182
-
183
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
184
- path = Path.build(name, prefix, partial)
185
- query(path, details, details[:formats], outside_app_allowed)
206
+ def clear_cache
207
+ @unbound_templates.clear
208
+ @path_parser = PathParser.new
209
+ super
186
210
  end
187
211
 
188
- def query(path, details, formats, outside_app_allowed)
189
- query = build_query(path, details)
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)
216
+ end
190
217
 
191
- template_paths = find_template_paths query
192
- template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
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)
221
+
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
231
+
232
+ unbound_template.bind_locals(locals)
233
+ end
234
+ end
193
235
 
194
- template_paths.map { |template|
195
- handler, format, variant = extract_handler_and_format_and_variant(template, formats)
196
- contents = File.binread(template)
236
+ def source_for_template(template)
237
+ Template::Sources::File.new(template)
238
+ end
197
239
 
198
- Template.new(contents, File.expand_path(template), handler,
199
- :virtual_path => path.virtual,
200
- :format => format,
201
- :variant => variant,
202
- :updated_at => mtime(template)
240
+ def build_unbound_template(template, virtual_path)
241
+ handler, format, variant = extract_handler_and_format_and_variant(template)
242
+ source = source_for_template(template)
243
+
244
+ UnboundTemplate.new(
245
+ source,
246
+ template,
247
+ handler,
248
+ virtual_path: virtual_path,
249
+ format: format,
250
+ variant: variant,
203
251
  )
204
- }
205
- end
252
+ end
206
253
 
207
- def reject_files_external_to_app(files)
208
- files.reject { |filename| !inside_path?(@path, filename) }
209
- end
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)
259
+ 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
210
266
 
211
- if RUBY_VERSION >= '2.2.0'
212
267
  def find_template_paths(query)
213
- Dir[query].reject { |filename|
268
+ Dir[query].uniq.reject do |filename|
214
269
  File.directory?(filename) ||
215
270
  # deals with case-insensitive file systems.
216
271
  !File.fnmatch(query, filename, File::FNM_EXTGLOB)
217
- }
272
+ end
218
273
  end
219
- else
220
- def find_template_paths(query)
221
- # deals with case-insensitive file systems.
222
- sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
223
274
 
224
- Dir[query].reject { |filename|
225
- File.directory?(filename) ||
226
- !sanitizer[File.dirname(filename)].include?(filename)
227
- }
275
+ def inside_path?(path, filename)
276
+ filename = File.expand_path(filename)
277
+ path = File.join(path, "")
278
+ filename.start_with?(path)
228
279
  end
229
- end
230
280
 
231
- def inside_path?(path, filename)
232
- filename = File.expand_path(filename)
233
- path = File.join(path, '')
234
- filename.start_with?(path)
235
- end
281
+ # Helper for building query glob string based on resolver's pattern.
282
+ def build_query(path, details)
283
+ query = @pattern.dup
236
284
 
237
- # Helper for building query glob string based on resolver's pattern.
238
- def build_query(path, details)
239
- query = @pattern.dup
285
+ prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
286
+ query.gsub!(/:prefix(\/)?/, prefix)
240
287
 
241
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
242
- query.gsub!(/\:prefix(\/)?/, prefix)
288
+ partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
289
+ query.gsub!(":action", partial)
243
290
 
244
- partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
245
- query.gsub!(/\:action/, partial)
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
297
+ end
246
298
 
247
- details.each do |ext, variants|
248
- query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
299
+ File.expand_path(query, @path)
249
300
  end
250
301
 
251
- File.expand_path(query, @path)
252
- end
302
+ def escape_entry(entry)
303
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
304
+ end
253
305
 
254
- def escape_entry(entry)
255
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
256
- end
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)
257
311
 
258
- # Returns the file mtime from the filesystem.
259
- def mtime(p)
260
- File.mtime(p)
261
- end
312
+ handler = Template.handler_for_extension(details[:handler])
313
+ format = details[:format] || handler.try(:default_format)
314
+ variant = details[:variant]
262
315
 
263
- # Extract handler, formats and variant from path. If a format cannot be found neither
264
- # from the path, or the handler, we should return the array of formats given
265
- # to the resolver.
266
- def extract_handler_and_format_and_variant(path, default_formats)
267
- pieces = File.basename(path).split(".")
268
- pieces.shift
269
-
270
- extension = pieces.pop
271
- unless extension
272
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
273
- The file #{path} did not specify a template handler. The default is
274
- currently ERB, but will change to RAW in the future.
275
- MSG
316
+ # Template::Types[format] and handler.default_format can return nil
317
+ [handler, format, variant]
276
318
  end
277
-
278
- handler = Template.handler_for_extension(extension)
279
- format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
280
- format &&= Template::Types[format]
281
-
282
- [handler, format, variant]
283
- end
284
319
  end
285
320
 
286
- # A resolver that loads files from the filesystem. It allows setting your own
287
- # resolving pattern. Such pattern can be a glob string supported by some variables.
288
- #
289
- # ==== Examples
290
- #
291
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
292
- # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
293
- #
294
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
295
- #
296
- # This one allows you to keep files with different formats in separate subdirectories,
297
- # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
298
- # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
299
- #
300
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
301
- #
302
- # If you don't specify a pattern then the default will be used.
303
- #
304
- # In order to use any of the customized resolvers above in a Rails application, you just need
305
- # to configure ActionController::Base.view_paths in an initializer, for example:
306
- #
307
- # ActionController::Base.view_paths = FileSystemResolver.new(
308
- # Rails.root.join("app/views"),
309
- # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}"
310
- # )
311
- #
312
- # ==== Pattern format and variables
313
- #
314
- # Pattern has to be a valid glob string, and it allows you to use the
315
- # following variables:
316
- #
317
- # * <tt>:prefix</tt> - usually the controller path
318
- # * <tt>:action</tt> - name of the action
319
- # * <tt>:locale</tt> - possible locale versions
320
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
321
- # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
322
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
323
- #
321
+ # A resolver that loads files from the filesystem.
324
322
  class FileSystemResolver < PathResolver
325
- def initialize(path, pattern=nil)
323
+ attr_reader :path
324
+
325
+ def initialize(path)
326
326
  raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
327
- super(pattern)
327
+ super()
328
328
  @path = File.expand_path(path)
329
329
  end
330
330
 
@@ -341,26 +341,92 @@ module ActionView
341
341
 
342
342
  # An Optimized resolver for Rails' most common case.
343
343
  class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
344
- def build_query(path, details)
345
- query = escape_entry(File.join(@path, path))
344
+ def initialize(path)
345
+ super(path)
346
+ end
346
347
 
347
- exts = EXTENSIONS.map do |ext, prefix|
348
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
349
- end.join
348
+ private
349
+ def find_candidate_template_paths(path)
350
+ # Instead of checking for every possible path, as our other globs would
351
+ # do, scan the directory for files with the right prefix.
352
+ query = "#{escape_entry(File.join(@path, path))}*"
350
353
 
351
- query + exts
352
- end
354
+ Dir[query].reject do |filename|
355
+ File.directory?(filename)
356
+ end
357
+ end
358
+
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
363
+ end
364
+
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
391
+ end
392
+ end
393
+ end
394
+
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}
412
+ end
353
413
  end
354
414
 
355
415
  # The same as FileSystemResolver but does not allow templates to store
356
416
  # a virtual path since it is invalid for such resolvers.
357
417
  class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
418
+ private_class_method :new
419
+
358
420
  def self.instances
359
421
  [new(""), new("/")]
360
422
  end
361
423
 
362
- def decorate(*)
363
- super.each { |t| t.virtual_path = nil }
424
+ def build_unbound_template(template, _)
425
+ super(template, nil)
426
+ end
427
+
428
+ def reject_files_external_to_app(files)
429
+ files
364
430
  end
365
431
  end
366
432
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ class Template
5
+ module Sources
6
+ class File
7
+ def initialize(filename)
8
+ @filename = filename
9
+ end
10
+
11
+ def to_s
12
+ ::File.binread @filename
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ class Template
5
+ module Sources
6
+ extend ActiveSupport::Autoload
7
+
8
+ eager_autoload do
9
+ autoload :File
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,22 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView #:nodoc:
2
4
  # = Action View Text Template
3
- class Template
5
+ class Template #:nodoc:
4
6
  class Text #:nodoc:
5
7
  attr_accessor :type
6
8
 
7
- def initialize(string, type = nil)
9
+ def initialize(string)
8
10
  @string = string.to_s
9
- @type = Types[type] || type if type
10
- @type ||= Types[:text]
11
11
  end
12
12
 
13
13
  def identifier
14
- 'text template'
14
+ "text template"
15
15
  end
16
16
 
17
- def inspect
18
- 'text template'
19
- end
17
+ alias_method :inspect, :identifier
20
18
 
21
19
  def to_str
22
20
  @string
@@ -26,8 +24,8 @@ module ActionView #:nodoc:
26
24
  to_str
27
25
  end
28
26
 
29
- def formats
30
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
27
+ def format
28
+ :text
31
29
  end
32
30
  end
33
31
  end