actionview 4.1.13 → 6.1.3.1

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 (124) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +181 -359
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +12 -6
  5. data/lib/action_view/base.rb +115 -43
  6. data/lib/action_view/buffers.rb +22 -4
  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 -84
  11. data/lib/action_view/flows.rb +12 -13
  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 +311 -105
  15. data/lib/action_view/helpers/asset_url_helper.rb +197 -80
  16. data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
  17. data/lib/action_view/helpers/cache_helper.rb +109 -45
  18. data/lib/action_view/helpers/capture_helper.rb +20 -22
  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 +245 -140
  23. data/lib/action_view/helpers/debug_helper.rb +14 -17
  24. data/lib/action_view/helpers/form_helper.rb +875 -148
  25. data/lib/action_view/helpers/form_options_helper.rb +128 -82
  26. data/lib/action_view/helpers/form_tag_helper.rb +253 -91
  27. data/lib/action_view/helpers/javascript_helper.rb +37 -15
  28. data/lib/action_view/helpers/number_helper.rb +100 -77
  29. data/lib/action_view/helpers/output_safety_helper.rb +42 -10
  30. data/lib/action_view/helpers/rendering_helper.rb +26 -15
  31. data/lib/action_view/helpers/sanitize_helper.rb +79 -164
  32. data/lib/action_view/helpers/tag_helper.rb +277 -64
  33. data/lib/action_view/helpers/tags/base.rb +143 -92
  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 -30
  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 +14 -5
  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 +2 -0
  50. data/lib/action_view/helpers/tags/label.rb +41 -22
  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 +24 -0
  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 +3 -0
  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 +7 -1
  61. data/lib/action_view/helpers/tags/text_field.rb +11 -7
  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 +39 -0
  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 +4 -1
  69. data/lib/action_view/helpers/text_helper.rb +80 -45
  70. data/lib/action_view/helpers/translation_helper.rb +148 -67
  71. data/lib/action_view/helpers/url_helper.rb +289 -147
  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 +80 -13
  75. data/lib/action_view/lookup_context.rb +137 -92
  76. data/lib/action_view/model_naming.rb +4 -2
  77. data/lib/action_view/path_set.rb +30 -16
  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 +152 -13
  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 +61 -261
  85. data/lib/action_view/renderer/renderer.rb +67 -6
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
  87. data/lib/action_view/renderer/template_renderer.rb +83 -75
  88. data/lib/action_view/rendering.rb +73 -46
  89. data/lib/action_view/routing_url_for.rb +54 -17
  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 +22 -9
  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 +267 -181
  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 +109 -99
  108. data/lib/action_view/test_case.rb +73 -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 +74 -44
  113. data/lib/action_view.rb +14 -9
  114. data/lib/assets/compiled/rails-ujs.js +746 -0
  115. metadata +71 -26
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  117. data/lib/action_view/tasks/dependencies.rake +0 -23
  118. data/lib/action_view/vendor/html-scanner/html/document.rb +0 -68
  119. data/lib/action_view/vendor/html-scanner/html/node.rb +0 -532
  120. data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +0 -188
  121. data/lib/action_view/vendor/html-scanner/html/selector.rb +0 -830
  122. data/lib/action_view/vendor/html-scanner/html/tokenizer.rb +0 -107
  123. data/lib/action_view/vendor/html-scanner/html/version.rb +0 -11
  124. data/lib/action_view/vendor/html-scanner.rb +0 -20
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Template::Handlers
5
+ class Html < Raw
6
+ def call(template, source)
7
+ "ActionView::OutputBuffer.new #{super}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,10 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Template::Handlers
3
5
  class Raw
4
- def call(template)
5
- escaped = template.source.gsub(':', '\:')
6
-
7
- '%q:' + escaped + ':;'
6
+ def call(template, source)
7
+ "#{source.inspect}.html_safe;"
8
8
  end
9
9
  end
10
10
  end
@@ -1,16 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView #:nodoc:
2
4
  # = Action View Template Handlers
3
- class Template
5
+ class Template #:nodoc:
4
6
  module Handlers #:nodoc:
5
- autoload :ERB, 'action_view/template/handlers/erb'
6
- autoload :Builder, 'action_view/template/handlers/builder'
7
- autoload :Raw, 'action_view/template/handlers/raw'
7
+ autoload :Raw, "action_view/template/handlers/raw"
8
+ autoload :ERB, "action_view/template/handlers/erb"
9
+ autoload :Html, "action_view/template/handlers/html"
10
+ autoload :Builder, "action_view/template/handlers/builder"
8
11
 
9
12
  def self.extended(base)
10
- base.register_default_template_handler :erb, ERB.new
13
+ base.register_default_template_handler :raw, Raw.new
14
+ base.register_template_handler :erb, ERB.new
15
+ base.register_template_handler :html, Html.new
11
16
  base.register_template_handler :builder, Builder.new
12
- base.register_template_handler :raw, Raw.new
13
- base.register_template_handler :ruby, :source.to_proc
17
+ base.register_template_handler :ruby, lambda { |_, source| source }
14
18
  end
15
19
 
16
20
  @@template_handlers = {}
@@ -22,7 +26,7 @@ module ActionView #:nodoc:
22
26
 
23
27
  # Register an object that knows how to handle template files with the given
24
28
  # extensions. This can be used to implement new template types.
25
- # The handler must respond to `:call`, which will be passed the template
29
+ # The handler must respond to +:call+, which will be passed the template
26
30
  # and should return the rendered template as a String.
27
31
  def register_template_handler(*extensions, handler)
28
32
  raise(ArgumentError, "Extension is required") if extensions.empty?
@@ -32,8 +36,17 @@ module ActionView #:nodoc:
32
36
  @@template_extensions = nil
33
37
  end
34
38
 
39
+ # Opposite to register_template_handler.
40
+ def unregister_template_handler(*extensions)
41
+ extensions.each do |extension|
42
+ handler = @@template_handlers.delete extension.to_sym
43
+ @@default_template_handlers = nil if @@default_template_handlers == handler
44
+ end
45
+ @@template_extensions = nil
46
+ end
47
+
35
48
  def template_handler_extensions
36
- @@template_handlers.keys.map {|key| key.to_s }.sort
49
+ @@template_handlers.keys.map(&:to_s).sort
37
50
  end
38
51
 
39
52
  def registered_template_handler(extension)
@@ -1,22 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView #:nodoc:
2
4
  # = Action View HTML Template
3
- class Template
5
+ class Template #:nodoc:
4
6
  class HTML #:nodoc:
5
- attr_accessor :type
7
+ attr_reader :type
6
8
 
7
- def initialize(string, type = nil)
9
+ def initialize(string, type)
8
10
  @string = string.to_s
9
- @type = Types[type] || type if type
10
- @type ||= Types[:html]
11
+ @type = type
11
12
  end
12
13
 
13
14
  def identifier
14
- 'html template'
15
+ "html template"
15
16
  end
16
17
 
17
- def inspect
18
- 'html template'
19
- end
18
+ alias_method :inspect, :identifier
20
19
 
21
20
  def to_str
22
21
  ERB::Util.h(@string)
@@ -26,8 +25,8 @@ module ActionView #:nodoc:
26
25
  to_str
27
26
  end
28
27
 
29
- def formats
30
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
28
+ def format
29
+ @type
31
30
  end
32
31
  end
33
32
  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,25 @@
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
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ # = Action View Renderable Template for objects that respond to #render_in
5
+ class Template
6
+ class Renderable # :nodoc:
7
+ def initialize(renderable)
8
+ @renderable = renderable
9
+ end
10
+
11
+ def identifier
12
+ @renderable.class.name
13
+ end
14
+
15
+ def render(context, *args)
16
+ @renderable.render_in(context)
17
+ end
18
+
19
+ def format
20
+ @renderable.format
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,9 +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
6
  require "action_view/template"
5
7
  require "thread"
6
- require "thread_safe"
8
+ require "concurrent/map"
7
9
 
8
10
  module ActionView
9
11
  # = Action View Resolver
@@ -14,7 +16,7 @@ module ActionView
14
16
  alias_method :partial?, :partial
15
17
 
16
18
  def self.build(name, prefix, partial)
17
- virtual = ""
19
+ virtual = +""
18
20
  virtual << "#{prefix}/" unless prefix.empty?
19
21
  virtual << (partial ? "_#{name}" : name)
20
22
  new name, prefix, partial, virtual
@@ -33,69 +35,105 @@ module ActionView
33
35
  alias :to_s :to_str
34
36
  end
35
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
+
36
73
  # Threadsafe template cache
37
74
  class Cache #:nodoc:
38
- class SmallCache < ThreadSafe::Cache
75
+ class SmallCache < Concurrent::Map
39
76
  def initialize(options = {})
40
- super(options.merge(:initial_capacity => 2))
77
+ super(options.merge(initial_capacity: 2))
41
78
  end
42
79
  end
43
80
 
44
- # preallocate all the default blocks for performance/memory consumption reasons
45
- PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new}
46
- PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)}
47
- NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)}
48
- 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) }
49
86
 
50
- # 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
51
88
  NO_TEMPLATES = [].freeze
52
89
 
53
90
  def initialize
54
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}>"
55
97
  end
56
98
 
57
99
  # Cache the templates returned by the block
58
100
  def cache(key, name, prefix, partial, locals)
59
- if Resolver.caching?
60
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
61
- else
62
- fresh_templates = yield
63
- cached_templates = @data[key][name][prefix][partial][locals]
64
-
65
- if templates_have_changed?(cached_templates, fresh_templates)
66
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
67
- else
68
- cached_templates || NO_TEMPLATES
69
- end
70
- 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)
71
106
  end
72
107
 
73
108
  def clear
74
109
  @data.clear
110
+ @query_cache.clear
75
111
  end
76
112
 
77
- 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
78
126
 
79
- def canonical_no_templates(templates)
80
- templates.empty? ? NO_TEMPLATES : templates
127
+ size + @query_cache.size
81
128
  end
82
129
 
83
- def templates_have_changed?(cached_templates, fresh_templates)
84
- # if either the old or new template list is empty, we don't need to (and can't)
85
- # compare modification times, and instead just check whether the lists are different
86
- if cached_templates.blank? || fresh_templates.blank?
87
- return fresh_templates.blank? != cached_templates.blank?
130
+ private
131
+ def canonical_no_templates(templates)
132
+ templates.empty? ? NO_TEMPLATES : templates
88
133
  end
89
-
90
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
91
-
92
- # if a template has changed, it will be now be newer than all the cached templates
93
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
94
- end
95
134
  end
96
135
 
97
- cattr_accessor :caching
98
- self.caching = true
136
+ cattr_accessor :caching, default: true
99
137
 
100
138
  class << self
101
139
  alias :caching? :caching
@@ -110,201 +148,183 @@ module ActionView
110
148
  end
111
149
 
112
150
  # Normalizes the arguments and passes it on to find_templates.
113
- 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
+
114
154
  cached(key, [name, prefix, partial], details, locals) do
115
- find_templates(name, prefix, partial, details)
155
+ _find_all(name, prefix, partial, details, key, locals)
116
156
  end
117
157
  end
118
158
 
159
+ def find_all_with_query(query) # :nodoc:
160
+ @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
161
+ end
162
+
119
163
  private
164
+ def _find_all(name, prefix, partial, details, key, locals)
165
+ find_templates(name, prefix, partial, details, locals)
166
+ end
120
167
 
121
168
  delegate :caching?, to: :class
122
169
 
123
170
  # This is what child classes implement. No defaults are needed
124
171
  # because Resolver guarantees that the arguments are present and
125
172
  # normalized.
126
- def find_templates(name, prefix, partial, details)
127
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
128
- end
129
-
130
- # Helpers that builds a path. Useful for building virtual paths.
131
- def build_path(name, prefix, partial)
132
- 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"
133
175
  end
134
176
 
135
177
  # Handles templates caching. If a key is given and caching is on
136
178
  # always check the cache before hitting the resolver. Otherwise,
137
179
  # it always hits the resolver but if the key is present, check if the
138
180
  # resolver is fresher before returning it.
139
- def cached(key, path_info, details, locals) #:nodoc:
181
+ def cached(key, path_info, details, locals)
140
182
  name, prefix, partial = path_info
141
- locals = locals.map { |x| x.to_s }.sort!
142
183
 
143
184
  if key
144
185
  @cache.cache(key, name, prefix, partial, locals) do
145
- decorate(yield, path_info, details, locals)
186
+ yield
146
187
  end
147
188
  else
148
- decorate(yield, path_info, details, locals)
149
- end
150
- end
151
-
152
- # Ensures all the resolver information is set in the template.
153
- def decorate(templates, path_info, details, locals) #:nodoc:
154
- cached = nil
155
- templates.each do |t|
156
- t.locals = locals
157
- t.formats = details[:formats] || [:html] if t.formats.empty?
158
- t.variants = details[:variants] || [] if t.variants.empty?
159
- t.virtual_path ||= (cached ||= build_path(*path_info))
189
+ yield
160
190
  end
161
191
  end
162
192
  end
163
193
 
164
194
  # An abstract class that implements a Resolver with path semantics.
165
195
  class PathResolver < Resolver #:nodoc:
166
- EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." }
196
+ EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
167
197
  DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
168
198
 
169
- def initialize(pattern=nil)
170
- @pattern = pattern || DEFAULT_PATTERN
171
- super()
199
+ def initialize
200
+ @pattern = DEFAULT_PATTERN
201
+ @unbound_templates = Concurrent::Map.new
202
+ @path_parser = PathParser.new
203
+ super
172
204
  end
173
205
 
174
- private
175
-
176
- def find_templates(name, prefix, partial, details)
177
- path = Path.build(name, prefix, partial)
178
- query(path, details, details[:formats])
206
+ def clear_cache
207
+ @unbound_templates.clear
208
+ @path_parser = PathParser.new
209
+ super
179
210
  end
180
211
 
181
- def query(path, details, formats)
182
- 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
183
217
 
184
- template_paths = find_template_paths query
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
185
235
 
186
- template_paths.map { |template|
187
- handler, format, variant = extract_handler_and_format_and_variant(template, formats)
188
- contents = File.binread(template)
236
+ def source_for_template(template)
237
+ Template::Sources::File.new(template)
238
+ end
189
239
 
190
- Template.new(contents, File.expand_path(template), handler,
191
- :virtual_path => path.virtual,
192
- :format => format,
193
- :variant => variant,
194
- :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,
195
251
  )
196
- }
197
- end
252
+ end
253
+
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
198
266
 
199
- if RUBY_VERSION >= '2.2.0'
200
267
  def find_template_paths(query)
201
- Dir[query].reject { |filename|
268
+ Dir[query].uniq.reject do |filename|
202
269
  File.directory?(filename) ||
203
270
  # deals with case-insensitive file systems.
204
271
  !File.fnmatch(query, filename, File::FNM_EXTGLOB)
205
- }
272
+ end
206
273
  end
207
- else
208
- def find_template_paths(query)
209
- # deals with case-insensitive file systems.
210
- sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
211
274
 
212
- Dir[query].reject { |filename|
213
- File.directory?(filename) ||
214
- !sanitizer[File.dirname(filename)].include?(filename)
215
- }
275
+ def inside_path?(path, filename)
276
+ filename = File.expand_path(filename)
277
+ path = File.join(path, "")
278
+ filename.start_with?(path)
216
279
  end
217
- end
218
280
 
219
- # Helper for building query glob string based on resolver's pattern.
220
- def build_query(path, details)
221
- query = @pattern.dup
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)
222
287
 
223
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
224
- query.gsub!(/\:prefix(\/)?/, prefix)
288
+ partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
289
+ query.gsub!(":action", partial)
225
290
 
226
- partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
227
- 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
228
298
 
229
- details.each do |ext, variants|
230
- query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
299
+ File.expand_path(query, @path)
231
300
  end
232
301
 
233
- File.expand_path(query, @path)
234
- end
302
+ def escape_entry(entry)
303
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
304
+ end
235
305
 
236
- def escape_entry(entry)
237
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
238
- 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)
239
311
 
240
- # Returns the file mtime from the filesystem.
241
- def mtime(p)
242
- File.mtime(p)
243
- end
312
+ handler = Template.handler_for_extension(details[:handler])
313
+ format = details[:format] || handler.try(:default_format)
314
+ variant = details[:variant]
244
315
 
245
- # Extract handler and formats from path. If a format cannot be a found neither
246
- # from the path, or the handler, we should return the array of formats given
247
- # to the resolver.
248
- def extract_handler_and_format_and_variant(path, default_formats)
249
- pieces = File.basename(path).split(".")
250
- pieces.shift
251
-
252
- extension = pieces.pop
253
- unless extension
254
- message = "The file #{path} did not specify a template handler. The default is currently ERB, " \
255
- "but will change to RAW in the future."
256
- ActiveSupport::Deprecation.warn message
316
+ # Template::Types[format] and handler.default_format can return nil
317
+ [handler, format, variant]
257
318
  end
258
-
259
- handler = Template.handler_for_extension(extension)
260
- format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
261
- format &&= Template::Types[format]
262
-
263
- [handler, format, variant]
264
- end
265
319
  end
266
320
 
267
- # A resolver that loads files from the filesystem. It allows setting your own
268
- # resolving pattern. Such pattern can be a glob string supported by some variables.
269
- #
270
- # ==== Examples
271
- #
272
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
273
- # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
274
- #
275
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}")
276
- #
277
- # This one allows you to keep files with different formats in separate subdirectories,
278
- # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
279
- # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
280
- #
281
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}")
282
- #
283
- # If you don't specify a pattern then the default will be used.
284
- #
285
- # In order to use any of the customized resolvers above in a Rails application, you just need
286
- # to configure ActionController::Base.view_paths in an initializer, for example:
287
- #
288
- # ActionController::Base.view_paths = FileSystemResolver.new(
289
- # Rails.root.join("app/views"),
290
- # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}"
291
- # )
292
- #
293
- # ==== Pattern format and variables
294
- #
295
- # Pattern has to be a valid glob string, and it allows you to use the
296
- # following variables:
297
- #
298
- # * <tt>:prefix</tt> - usually the controller path
299
- # * <tt>:action</tt> - name of the action
300
- # * <tt>:locale</tt> - possible locale versions
301
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
302
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
303
- #
321
+ # A resolver that loads files from the filesystem.
304
322
  class FileSystemResolver < PathResolver
305
- def initialize(path, pattern=nil)
323
+ attr_reader :path
324
+
325
+ def initialize(path)
306
326
  raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
307
- super(pattern)
327
+ super()
308
328
  @path = File.expand_path(path)
309
329
  end
310
330
 
@@ -321,26 +341,92 @@ module ActionView
321
341
 
322
342
  # An Optimized resolver for Rails' most common case.
323
343
  class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
324
- def build_query(path, details)
325
- query = escape_entry(File.join(@path, path))
344
+ def initialize(path)
345
+ super(path)
346
+ end
326
347
 
327
- exts = EXTENSIONS.map do |ext, prefix|
328
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
329
- 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))}*"
330
353
 
331
- query + exts
332
- 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
333
413
  end
334
414
 
335
415
  # The same as FileSystemResolver but does not allow templates to store
336
416
  # a virtual path since it is invalid for such resolvers.
337
417
  class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
418
+ private_class_method :new
419
+
338
420
  def self.instances
339
421
  [new(""), new("/")]
340
422
  end
341
423
 
342
- def decorate(*)
343
- 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
344
430
  end
345
431
  end
346
432
  end