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