actionview 5.1.4 → 6.1.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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +199 -168
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -5
  5. data/lib/action_view.rb +10 -4
  6. data/lib/action_view/base.rb +87 -23
  7. data/lib/action_view/buffers.rb +17 -0
  8. data/lib/action_view/cache_expiry.rb +52 -0
  9. data/lib/action_view/context.rb +7 -11
  10. data/lib/action_view/dependency_tracker.rb +12 -4
  11. data/lib/action_view/digestor.rb +24 -23
  12. data/lib/action_view/flows.rb +2 -1
  13. data/lib/action_view/gem_version.rb +4 -2
  14. data/lib/action_view/helpers.rb +4 -2
  15. data/lib/action_view/helpers/active_model_helper.rb +9 -4
  16. data/lib/action_view/helpers/asset_tag_helper.rb +220 -57
  17. data/lib/action_view/helpers/asset_url_helper.rb +28 -23
  18. data/lib/action_view/helpers/atom_feed_helper.rb +5 -2
  19. data/lib/action_view/helpers/cache_helper.rb +39 -28
  20. data/lib/action_view/helpers/capture_helper.rb +13 -7
  21. data/lib/action_view/helpers/controller_helper.rb +3 -1
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +5 -3
  24. data/lib/action_view/helpers/date_helper.rb +78 -33
  25. data/lib/action_view/helpers/debug_helper.rb +4 -2
  26. data/lib/action_view/helpers/form_helper.rb +357 -106
  27. data/lib/action_view/helpers/form_options_helper.rb +45 -39
  28. data/lib/action_view/helpers/form_tag_helper.rb +42 -27
  29. data/lib/action_view/helpers/javascript_helper.rb +28 -12
  30. data/lib/action_view/helpers/number_helper.rb +16 -8
  31. data/lib/action_view/helpers/output_safety_helper.rb +3 -1
  32. data/lib/action_view/helpers/rendering_helper.rb +20 -9
  33. data/lib/action_view/helpers/sanitize_helper.rb +15 -19
  34. data/lib/action_view/helpers/tag_helper.rb +100 -24
  35. data/lib/action_view/helpers/tags.rb +3 -1
  36. data/lib/action_view/helpers/tags/base.rb +30 -21
  37. data/lib/action_view/helpers/tags/check_box.rb +3 -2
  38. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +2 -1
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +2 -1
  42. data/lib/action_view/helpers/tags/collection_select.rb +3 -1
  43. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/date_select.rb +5 -4
  46. data/lib/action_view/helpers/tags/datetime_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  48. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -1
  52. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/label.rb +6 -5
  54. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  55. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +2 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +3 -2
  59. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +2 -0
  61. data/lib/action_view/helpers/tags/select.rb +4 -3
  62. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +3 -1
  64. data/lib/action_view/helpers/tags/text_field.rb +3 -2
  65. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  66. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  68. data/lib/action_view/helpers/tags/translator.rb +3 -6
  69. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  71. data/lib/action_view/helpers/text_helper.rb +11 -10
  72. data/lib/action_view/helpers/translation_helper.rb +102 -52
  73. data/lib/action_view/helpers/url_helper.rb +150 -32
  74. data/lib/action_view/layouts.rb +15 -15
  75. data/lib/action_view/log_subscriber.rb +32 -15
  76. data/lib/action_view/lookup_context.rb +67 -39
  77. data/lib/action_view/model_naming.rb +2 -0
  78. data/lib/action_view/path_set.rb +5 -12
  79. data/lib/action_view/railtie.rb +46 -21
  80. data/lib/action_view/record_identifier.rb +4 -3
  81. data/lib/action_view/renderer/abstract_renderer.rb +144 -11
  82. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  83. data/lib/action_view/renderer/object_renderer.rb +34 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +33 -283
  85. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +64 -17
  86. data/lib/action_view/renderer/renderer.rb +61 -4
  87. data/lib/action_view/renderer/streaming_template_renderer.rb +14 -8
  88. data/lib/action_view/renderer/template_renderer.rb +36 -26
  89. data/lib/action_view/rendering.rb +57 -38
  90. data/lib/action_view/routing_url_for.rb +15 -12
  91. data/lib/action_view/tasks/cache_digests.rake +2 -0
  92. data/lib/action_view/template.rb +69 -76
  93. data/lib/action_view/template/error.rb +32 -18
  94. data/lib/action_view/template/handlers.rb +4 -2
  95. data/lib/action_view/template/handlers/builder.rb +5 -6
  96. data/lib/action_view/template/handlers/erb.rb +20 -19
  97. data/lib/action_view/template/handlers/erb/erubi.rb +17 -9
  98. data/lib/action_view/template/handlers/html.rb +3 -1
  99. data/lib/action_view/template/handlers/raw.rb +4 -2
  100. data/lib/action_view/template/html.rb +8 -7
  101. data/lib/action_view/template/inline.rb +22 -0
  102. data/lib/action_view/template/raw_file.rb +25 -0
  103. data/lib/action_view/template/renderable.rb +24 -0
  104. data/lib/action_view/template/resolver.rb +194 -152
  105. data/lib/action_view/template/sources.rb +13 -0
  106. data/lib/action_view/template/sources/file.rb +17 -0
  107. data/lib/action_view/template/text.rb +5 -4
  108. data/lib/action_view/template/types.rb +3 -1
  109. data/lib/action_view/test_case.rb +38 -30
  110. data/lib/action_view/testing/resolvers.rb +20 -27
  111. data/lib/action_view/unbound_template.rb +31 -0
  112. data/lib/action_view/version.rb +2 -0
  113. data/lib/action_view/view_paths.rb +61 -40
  114. data/lib/assets/compiled/rails-ujs.js +84 -23
  115. metadata +34 -23
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -21
  117. data/lib/action_view/template/handlers/erb/deprecated_erubis.rb +0 -9
  118. data/lib/action_view/template/handlers/erb/erubis.rb +0 -81
@@ -1,29 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  class Template
3
5
  module Handlers
4
- autoload :Erubis, "action_view/template/handlers/erb/deprecated_erubis"
5
-
6
6
  class ERB
7
7
  autoload :Erubi, "action_view/template/handlers/erb/erubi"
8
- autoload :Erubis, "action_view/template/handlers/erb/erubis"
9
8
 
10
9
  # Specify trim mode for the ERB compiler. Defaults to '-'.
11
10
  # See ERB documentation for suitable values.
12
- class_attribute :erb_trim_mode
13
- self.erb_trim_mode = "-"
11
+ class_attribute :erb_trim_mode, default: "-"
14
12
 
15
13
  # Default implementation used.
16
- class_attribute :erb_implementation
17
- self.erb_implementation = Erubi
14
+ class_attribute :erb_implementation, default: Erubi
18
15
 
19
16
  # Do not escape templates of these mime types.
20
- class_attribute :escape_whitelist
21
- self.escape_whitelist = ["text/plain"]
17
+ class_attribute :escape_ignore_list, default: ["text/plain"]
22
18
 
23
19
  ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
24
20
 
25
- def self.call(template)
26
- new.call(template)
21
+ def self.call(template, source)
22
+ new.call(template, source)
27
23
  end
28
24
 
29
25
  def supports_streaming?
@@ -34,30 +30,35 @@ module ActionView
34
30
  true
35
31
  end
36
32
 
37
- def call(template)
33
+ def call(template, source)
38
34
  # First, convert to BINARY, so in case the encoding is
39
35
  # wrong, we can still find an encoding tag
40
36
  # (<%# encoding %>) inside the String using a regular
41
37
  # expression
42
- template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
38
+ template_source = source.b
43
39
 
44
40
  erb = template_source.gsub(ENCODING_TAG, "")
45
41
  encoding = $2
46
42
 
47
- erb.force_encoding valid_encoding(template.source.dup, encoding)
43
+ erb.force_encoding valid_encoding(source.dup, encoding)
48
44
 
49
45
  # Always make sure we return a String in the default_internal
50
46
  erb.encode!
51
47
 
52
- self.class.erb_implementation.new(
53
- erb,
54
- escape: (self.class.escape_whitelist.include? template.type),
48
+ options = {
49
+ escape: (self.class.escape_ignore_list.include? template.type),
55
50
  trim: (self.class.erb_trim_mode == "-")
56
- ).src
51
+ }
52
+
53
+ if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
54
+ 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"
56
+ end
57
+
58
+ self.class.erb_implementation.new(erb, options).src
57
59
  end
58
60
 
59
61
  private
60
-
61
62
  def valid_encoding(string, encoding)
62
63
  # If a magic encoding comment was found, tag the
63
64
  # String with this encoding. This is for a case
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "erubi"
2
4
 
3
5
  module ActionView
@@ -11,17 +13,23 @@ module ActionView
11
13
 
12
14
  # Dup properties so that we don't modify argument
13
15
  properties = Hash[properties]
14
- properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
15
- properties[:postamble] = "@output_buffer.to_s"
16
- properties[:bufvar] = "@output_buffer"
16
+
17
+ properties[:bufvar] ||= "@output_buffer"
18
+ properties[:preamble] ||= ""
19
+ properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
20
+
17
21
  properties[:escapefunc] = ""
18
22
 
19
23
  super
20
24
  end
21
25
 
22
26
  def evaluate(action_view_erb_handler_context)
23
- pr = eval("proc { #{@src} }", binding, @filename || "(erubi)")
24
- action_view_erb_handler_context.instance_eval(&pr)
27
+ src = @src
28
+ view = Class.new(ActionView::Base) {
29
+ include action_view_erb_handler_context._routes.url_helpers
30
+ class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
31
+ }.empty
32
+ view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
25
33
  end
26
34
 
27
35
  private
@@ -31,7 +39,7 @@ module ActionView
31
39
  if text == "\n"
32
40
  @newline_pending += 1
33
41
  else
34
- src << "@output_buffer.safe_append='"
42
+ src << bufvar << ".safe_append='"
35
43
  src << "\n" * @newline_pending if @newline_pending > 0
36
44
  src << text.gsub(/['\\]/, '\\\\\&')
37
45
  src << "'.freeze;"
@@ -46,9 +54,9 @@ module ActionView
46
54
  flush_newline_if_pending(src)
47
55
 
48
56
  if (indicator == "==") || @escape
49
- src << "@output_buffer.safe_expr_append="
57
+ src << bufvar << ".safe_expr_append="
50
58
  else
51
- src << "@output_buffer.append="
59
+ src << bufvar << ".append="
52
60
  end
53
61
 
54
62
  if BLOCK_EXPR.match?(code)
@@ -70,7 +78,7 @@ module ActionView
70
78
 
71
79
  def flush_newline_if_pending(src)
72
80
  if @newline_pending > 0
73
- src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
81
+ src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
74
82
  @newline_pending = 0
75
83
  end
76
84
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Template::Handlers
3
5
  class Html < Raw
4
- def call(template)
6
+ def call(template, source)
5
7
  "ActionView::OutputBuffer.new #{super}"
6
8
  end
7
9
  end
@@ -1,8 +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
- "#{template.source.inspect}.html_safe;"
6
+ def call(template, source)
7
+ "#{source.inspect}.html_safe;"
6
8
  end
7
9
  end
8
10
  end
@@ -1,13 +1,14 @@
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
@@ -24,8 +25,8 @@ module ActionView #:nodoc:
24
25
  to_str
25
26
  end
26
27
 
27
- def formats
28
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
28
+ def format
29
+ @type
29
30
  end
30
31
  end
31
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,3 +1,5 @@
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"
@@ -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,6 +35,41 @@ 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
75
  class SmallCache < Concurrent::Map
@@ -41,13 +78,13 @@ module ActionView
41
78
  end
42
79
  end
43
80
 
44
- # preallocate all the default blocks for performance/memory consumption reasons
81
+ # Preallocate all the default blocks for performance/memory consumption reasons
45
82
  PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
46
83
  PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
47
84
  NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
48
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
@@ -56,31 +93,16 @@ module ActionView
56
93
  end
57
94
 
58
95
  def inspect
59
- "#<#{self.class.name}:0x#{(object_id << 1).to_s(16)} keys=#{@data.size} queries=#{@query_cache.size}>"
96
+ "#{to_s[0..-2]} keys=#{@data.size} queries=#{@query_cache.size}>"
60
97
  end
61
98
 
62
99
  # Cache the templates returned by the block
63
100
  def cache(key, name, prefix, partial, locals)
64
- if Resolver.caching?
65
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
66
- else
67
- fresh_templates = yield
68
- cached_templates = @data[key][name][prefix][partial][locals]
69
-
70
- if templates_have_changed?(cached_templates, fresh_templates)
71
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
72
- else
73
- cached_templates || NO_TEMPLATES
74
- end
75
- end
101
+ @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
76
102
  end
77
103
 
78
104
  def cache_query(query) # :nodoc:
79
- if Resolver.caching?
80
- @query_cache[query] ||= canonical_no_templates(yield)
81
- else
82
- yield
83
- end
105
+ @query_cache[query] ||= canonical_no_templates(yield)
84
106
  end
85
107
 
86
108
  def clear
@@ -88,7 +110,7 @@ module ActionView
88
110
  @query_cache.clear
89
111
  end
90
112
 
91
- # Get the cache size. Do not call this
113
+ # Get the cache size. Do not call this
92
114
  # method. This method is not guaranteed to be here ever.
93
115
  def size # :nodoc:
94
116
  size = 0
@@ -106,27 +128,12 @@ module ActionView
106
128
  end
107
129
 
108
130
  private
109
-
110
131
  def canonical_no_templates(templates)
111
132
  templates.empty? ? NO_TEMPLATES : templates
112
133
  end
113
-
114
- def templates_have_changed?(cached_templates, fresh_templates)
115
- # if either the old or new template list is empty, we don't need to (and can't)
116
- # compare modification times, and instead just check whether the lists are different
117
- if cached_templates.blank? || fresh_templates.blank?
118
- return fresh_templates.blank? != cached_templates.blank?
119
- end
120
-
121
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
122
-
123
- # if a template has changed, it will be now be newer than all the cached templates
124
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
125
- end
126
134
  end
127
135
 
128
- cattr_accessor :caching
129
- self.caching = true
136
+ cattr_accessor :caching, default: true
130
137
 
131
138
  class << self
132
139
  alias :caching? :caching
@@ -142,14 +149,10 @@ module ActionView
142
149
 
143
150
  # Normalizes the arguments and passes it on to find_templates.
144
151
  def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
145
- cached(key, [name, prefix, partial], details, locals) do
146
- find_templates(name, prefix, partial, details)
147
- end
148
- end
152
+ locals = locals.map(&:to_s).sort!.freeze
149
153
 
150
- def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
151
154
  cached(key, [name, prefix, partial], details, locals) do
152
- find_templates(name, prefix, partial, details, true)
155
+ _find_all(name, prefix, partial, details, key, locals)
153
156
  end
154
157
  end
155
158
 
@@ -158,19 +161,17 @@ module ActionView
158
161
  end
159
162
 
160
163
  private
164
+ def _find_all(name, prefix, partial, details, key, locals)
165
+ find_templates(name, prefix, partial, details, locals)
166
+ end
161
167
 
162
168
  delegate :caching?, to: :class
163
169
 
164
170
  # This is what child classes implement. No defaults are needed
165
171
  # because Resolver guarantees that the arguments are present and
166
172
  # normalized.
167
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
168
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method"
169
- end
170
-
171
- # Helpers that builds a path. Useful for building virtual paths.
172
- def build_path(name, prefix, partial)
173
- 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"
174
175
  end
175
176
 
176
177
  # Handles templates caching. If a key is given and caching is on
@@ -179,25 +180,13 @@ module ActionView
179
180
  # resolver is fresher before returning it.
180
181
  def cached(key, path_info, details, locals)
181
182
  name, prefix, partial = path_info
182
- locals = locals.map(&:to_s).sort!
183
183
 
184
184
  if key
185
185
  @cache.cache(key, name, prefix, partial, locals) do
186
- decorate(yield, path_info, details, locals)
186
+ yield
187
187
  end
188
188
  else
189
- decorate(yield, path_info, details, locals)
190
- end
191
- end
192
-
193
- # Ensures all the resolver information is set in the template.
194
- def decorate(templates, path_info, details, locals)
195
- cached = nil
196
- templates.each do |t|
197
- t.locals = locals
198
- t.formats = details[:formats] || [:html] if t.formats.empty?
199
- t.variants = details[:variants] || [] if t.variants.empty?
200
- t.virtual_path ||= (cached ||= build_path(*path_info))
189
+ yield
201
190
  end
202
191
  end
203
192
  end
@@ -207,41 +196,74 @@ module ActionView
207
196
  EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
208
197
  DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
209
198
 
210
- def initialize(pattern = nil)
211
- @pattern = pattern || DEFAULT_PATTERN
212
- super()
199
+ def initialize
200
+ @pattern = DEFAULT_PATTERN
201
+ @unbound_templates = Concurrent::Map.new
202
+ @path_parser = PathParser.new
203
+ super
213
204
  end
214
205
 
215
- private
206
+ def clear_cache
207
+ @unbound_templates.clear
208
+ @path_parser = PathParser.new
209
+ super
210
+ end
216
211
 
217
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
212
+ private
213
+ def _find_all(name, prefix, partial, details, key, locals)
218
214
  path = Path.build(name, prefix, partial)
219
- query(path, details, details[:formats], outside_app_allowed)
215
+ query(path, details, details[:formats], locals, cache: !!key)
220
216
  end
221
217
 
222
- def query(path, details, formats, outside_app_allowed)
223
- query = build_query(path, details)
224
-
225
- template_paths = find_template_paths(query)
226
- 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)
227
221
 
228
222
  template_paths.map do |template|
229
- handler, format, variant = extract_handler_and_format_and_variant(template)
230
- contents = File.binread(template)
231
-
232
- Template.new(contents, File.expand_path(template), handler,
233
- virtual_path: path.virtual,
234
- format: format,
235
- variant: variant,
236
- updated_at: mtime(template)
237
- )
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)
238
233
  end
239
234
  end
240
235
 
236
+ def source_for_template(template)
237
+ Template::Sources::File.new(template)
238
+ end
239
+
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,
251
+ )
252
+ end
253
+
241
254
  def reject_files_external_to_app(files)
242
255
  files.reject { |filename| !inside_path?(@path, filename) }
243
256
  end
244
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
266
+
245
267
  def find_template_paths(query)
246
268
  Dir[query].uniq.reject do |filename|
247
269
  File.directory?(filename) ||
@@ -264,7 +286,7 @@ module ActionView
264
286
  query.gsub!(/:prefix(\/)?/, prefix)
265
287
 
266
288
  partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
267
- query.gsub!(/:action/, partial)
289
+ query.gsub!(":action", partial)
268
290
 
269
291
  details.each do |ext, candidates|
270
292
  if ext == :variants && candidates == :any
@@ -278,73 +300,31 @@ module ActionView
278
300
  end
279
301
 
280
302
  def escape_entry(entry)
281
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze)
282
- end
283
-
284
- # Returns the file mtime from the filesystem.
285
- def mtime(p)
286
- File.mtime(p)
303
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
287
304
  end
288
305
 
289
306
  # Extract handler, formats and variant from path. If a format cannot be found neither
290
307
  # from the path, or the handler, we should return the array of formats given
291
308
  # to the resolver.
292
309
  def extract_handler_and_format_and_variant(path)
293
- pieces = File.basename(path).split(".".freeze)
294
- pieces.shift
295
-
296
- extension = pieces.pop
310
+ details = @path_parser.parse(path)
297
311
 
298
- handler = Template.handler_for_extension(extension)
299
- format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
300
- format &&= Template::Types[format]
312
+ handler = Template.handler_for_extension(details[:handler])
313
+ format = details[:format] || handler.try(:default_format)
314
+ variant = details[:variant]
301
315
 
316
+ # Template::Types[format] and handler.default_format can return nil
302
317
  [handler, format, variant]
303
318
  end
304
319
  end
305
320
 
306
- # A resolver that loads files from the filesystem. It allows setting your own
307
- # resolving pattern. Such pattern can be a glob string supported by some variables.
308
- #
309
- # ==== Examples
310
- #
311
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
312
- # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
313
- #
314
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
315
- #
316
- # This one allows you to keep files with different formats in separate subdirectories,
317
- # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
318
- # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
319
- #
320
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
321
- #
322
- # If you don't specify a pattern then the default will be used.
323
- #
324
- # In order to use any of the customized resolvers above in a Rails application, you just need
325
- # to configure ActionController::Base.view_paths in an initializer, for example:
326
- #
327
- # ActionController::Base.view_paths = FileSystemResolver.new(
328
- # Rails.root.join("app/views"),
329
- # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
330
- # )
331
- #
332
- # ==== Pattern format and variables
333
- #
334
- # Pattern has to be a valid glob string, and it allows you to use the
335
- # following variables:
336
- #
337
- # * <tt>:prefix</tt> - usually the controller path
338
- # * <tt>:action</tt> - name of the action
339
- # * <tt>:locale</tt> - possible locale versions
340
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
341
- # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
342
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
343
- #
321
+ # A resolver that loads files from the filesystem.
344
322
  class FileSystemResolver < PathResolver
345
- def initialize(path, pattern = nil)
323
+ attr_reader :path
324
+
325
+ def initialize(path)
346
326
  raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
347
- super(pattern)
327
+ super()
348
328
  @path = File.expand_path(path)
349
329
  end
350
330
 
@@ -361,30 +341,92 @@ module ActionView
361
341
 
362
342
  # An Optimized resolver for Rails' most common case.
363
343
  class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
364
- def build_query(path, details)
365
- query = escape_entry(File.join(@path, path))
366
-
367
- exts = EXTENSIONS.map do |ext, prefix|
368
- if ext == :variants && details[ext] == :any
369
- "{#{prefix}*,}"
370
- else
371
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
344
+ def initialize(path)
345
+ super(path)
346
+ end
347
+
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))}*"
353
+
354
+ Dir[query].reject do |filename|
355
+ File.directory?(filename)
372
356
  end
373
- end.join
357
+ end
374
358
 
375
- query + exts
376
- end
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
377
413
  end
378
414
 
379
415
  # The same as FileSystemResolver but does not allow templates to store
380
416
  # a virtual path since it is invalid for such resolvers.
381
417
  class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
418
+ private_class_method :new
419
+
382
420
  def self.instances
383
421
  [new(""), new("/")]
384
422
  end
385
423
 
386
- def decorate(*)
387
- 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
388
430
  end
389
431
  end
390
432
  end