actionview 7.1.5.2 → 8.1.2.1
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 +96 -436
- data/README.rdoc +1 -1
- data/lib/action_view/base.rb +29 -11
- data/lib/action_view/buffers.rb +1 -1
- data/lib/action_view/cache_expiry.rb +9 -3
- data/lib/action_view/dependency_tracker/erb_tracker.rb +37 -28
- data/lib/action_view/dependency_tracker/ruby_tracker.rb +43 -0
- data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
- data/lib/action_view/dependency_tracker.rb +8 -2
- data/lib/action_view/digestor.rb +6 -2
- data/lib/action_view/gem_version.rb +3 -3
- data/lib/action_view/helpers/asset_tag_helper.rb +39 -8
- data/lib/action_view/helpers/atom_feed_helper.rb +1 -3
- data/lib/action_view/helpers/cache_helper.rb +10 -2
- data/lib/action_view/helpers/capture_helper.rb +2 -2
- data/lib/action_view/helpers/controller_helper.rb +6 -2
- data/lib/action_view/helpers/csrf_helper.rb +1 -1
- data/lib/action_view/helpers/date_helper.rb +28 -4
- data/lib/action_view/helpers/form_helper.rb +277 -272
- data/lib/action_view/helpers/form_options_helper.rb +39 -35
- data/lib/action_view/helpers/form_tag_helper.rb +115 -72
- data/lib/action_view/helpers/javascript_helper.rb +5 -1
- data/lib/action_view/helpers/number_helper.rb +14 -0
- data/lib/action_view/helpers/output_safety_helper.rb +5 -6
- data/lib/action_view/helpers/rendering_helper.rb +160 -50
- data/lib/action_view/helpers/sanitize_helper.rb +6 -0
- data/lib/action_view/helpers/tag_helper.rb +225 -48
- data/lib/action_view/helpers/tags/base.rb +11 -9
- data/lib/action_view/helpers/tags/check_box.rb +9 -3
- data/lib/action_view/helpers/tags/collection_check_boxes.rb +4 -3
- data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
- data/lib/action_view/helpers/tags/datetime_field.rb +1 -1
- data/lib/action_view/helpers/tags/file_field.rb +7 -2
- data/lib/action_view/helpers/tags/hidden_field.rb +1 -1
- data/lib/action_view/helpers/tags/label.rb +3 -10
- data/lib/action_view/helpers/tags/radio_button.rb +1 -1
- data/lib/action_view/helpers/tags/select.rb +6 -1
- data/lib/action_view/helpers/tags/select_renderer.rb +6 -4
- data/lib/action_view/helpers/tags/text_area.rb +1 -1
- data/lib/action_view/helpers/tags/text_field.rb +1 -1
- data/lib/action_view/helpers/text_helper.rb +11 -4
- data/lib/action_view/helpers/translation_helper.rb +6 -1
- data/lib/action_view/helpers/url_helper.rb +42 -90
- data/lib/action_view/layouts.rb +9 -11
- data/lib/action_view/locale/en.yml +3 -0
- data/lib/action_view/log_subscriber.rb +9 -8
- data/lib/action_view/railtie.rb +12 -2
- data/lib/action_view/record_identifier.rb +22 -1
- data/lib/action_view/render_parser/prism_render_parser.rb +139 -0
- data/lib/action_view/{ripper_ast_parser.rb → render_parser/ripper_render_parser.rb} +162 -10
- data/lib/action_view/render_parser.rb +21 -169
- data/lib/action_view/renderer/abstract_renderer.rb +1 -1
- data/lib/action_view/renderer/partial_renderer.rb +18 -2
- data/lib/action_view/renderer/renderer.rb +32 -38
- data/lib/action_view/renderer/streaming_template_renderer.rb +8 -2
- data/lib/action_view/renderer/template_renderer.rb +3 -3
- data/lib/action_view/rendering.rb +6 -7
- data/lib/action_view/structured_event_subscriber.rb +97 -0
- data/lib/action_view/template/error.rb +18 -3
- data/lib/action_view/template/handlers/erb/erubi.rb +1 -1
- data/lib/action_view/template/handlers/erb.rb +77 -44
- data/lib/action_view/template/raw_file.rb +4 -0
- data/lib/action_view/template/renderable.rb +7 -1
- data/lib/action_view/template/resolver.rb +0 -3
- data/lib/action_view/template.rb +39 -12
- data/lib/action_view/test_case.rb +57 -62
- data/lib/action_view.rb +4 -0
- metadata +17 -14
- data/lib/action_view/dependency_tracker/ripper_tracker.rb +0 -59
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/structured_event_subscriber"
|
|
4
|
+
|
|
5
|
+
module ActionView
|
|
6
|
+
class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
|
|
7
|
+
VIEWS_PATTERN = /^app\/views\//
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@root = nil
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render_template(event)
|
|
15
|
+
emit_debug_event("action_view.render_template",
|
|
16
|
+
identifier: from_rails_root(event.payload[:identifier]),
|
|
17
|
+
layout: from_rails_root(event.payload[:layout]),
|
|
18
|
+
duration_ms: event.duration.round(2),
|
|
19
|
+
gc_ms: event.gc_time.round(2),
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
debug_only :render_template
|
|
23
|
+
|
|
24
|
+
def render_partial(event)
|
|
25
|
+
emit_debug_event("action_view.render_partial",
|
|
26
|
+
identifier: from_rails_root(event.payload[:identifier]),
|
|
27
|
+
layout: from_rails_root(event.payload[:layout]),
|
|
28
|
+
duration_ms: event.duration.round(2),
|
|
29
|
+
gc_ms: event.gc_time.round(2),
|
|
30
|
+
cache_hit: event.payload[:cache_hit],
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
debug_only :render_partial
|
|
34
|
+
|
|
35
|
+
def render_layout(event)
|
|
36
|
+
emit_event("action_view.render_layout",
|
|
37
|
+
identifier: from_rails_root(event.payload[:identifier]),
|
|
38
|
+
duration_ms: event.duration.round(2),
|
|
39
|
+
gc_ms: event.gc_time.round(2),
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
debug_only :render_layout
|
|
43
|
+
|
|
44
|
+
def render_collection(event)
|
|
45
|
+
emit_debug_event("action_view.render_collection",
|
|
46
|
+
identifier: from_rails_root(event.payload[:identifier] || "templates"),
|
|
47
|
+
layout: from_rails_root(event.payload[:layout]),
|
|
48
|
+
duration_ms: event.duration.round(2),
|
|
49
|
+
gc_ms: event.gc_time.round(2),
|
|
50
|
+
cache_hits: event.payload[:cache_hits],
|
|
51
|
+
count: event.payload[:count],
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
debug_only :render_collection
|
|
55
|
+
|
|
56
|
+
module Utils # :nodoc:
|
|
57
|
+
private
|
|
58
|
+
def from_rails_root(string)
|
|
59
|
+
return unless string
|
|
60
|
+
|
|
61
|
+
string = string.sub("#{rails_root}/", "")
|
|
62
|
+
string.sub!(VIEWS_PATTERN, "")
|
|
63
|
+
string
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def rails_root # :doc:
|
|
67
|
+
@root ||= Rails.try(:root)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
include Utils
|
|
72
|
+
|
|
73
|
+
class Start # :nodoc:
|
|
74
|
+
include Utils
|
|
75
|
+
|
|
76
|
+
def start(name, id, payload)
|
|
77
|
+
ActiveSupport.event_reporter.debug("action_view.render_start",
|
|
78
|
+
is_layout: name == "render_layout.action_view",
|
|
79
|
+
identifier: from_rails_root(payload[:identifier]),
|
|
80
|
+
layout: from_rails_root(payload[:layout]),
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def finish(name, id, payload)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.attach_to(*)
|
|
89
|
+
ActiveSupport::Notifications.subscribe("render_template.action_view", Start.new)
|
|
90
|
+
ActiveSupport::Notifications.subscribe("render_layout.action_view", Start.new)
|
|
91
|
+
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
ActionView::StructuredEventSubscriber.attach_to :action_view
|
|
@@ -27,6 +27,17 @@ module ActionView
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
class StrictLocalsError < ArgumentError # :nodoc:
|
|
31
|
+
def initialize(argument_error, template)
|
|
32
|
+
message = argument_error.message.
|
|
33
|
+
gsub("unknown keyword:", "unknown local:").
|
|
34
|
+
gsub("missing keyword:", "missing local:").
|
|
35
|
+
gsub("no keywords accepted", "no locals accepted").
|
|
36
|
+
concat(" for #{template.short_identifier}")
|
|
37
|
+
super(message)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
30
41
|
class MissingTemplate < ActionViewError # :nodoc:
|
|
31
42
|
attr_reader :path, :paths, :prefixes, :partial
|
|
32
43
|
|
|
@@ -249,9 +260,13 @@ module ActionView
|
|
|
249
260
|
end
|
|
250
261
|
|
|
251
262
|
def message
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
263
|
+
if template.is_a?(Template::Inline)
|
|
264
|
+
<<~MESSAGE
|
|
265
|
+
Encountered a syntax error while rendering template: check #{@offending_code_string}
|
|
266
|
+
MESSAGE
|
|
267
|
+
else
|
|
268
|
+
"Encountered a syntax error while rendering template located at: #{template.short_identifier}"
|
|
269
|
+
end
|
|
255
270
|
end
|
|
256
271
|
|
|
257
272
|
def annotated_source_code
|
|
@@ -18,7 +18,7 @@ module ActionView
|
|
|
18
18
|
properties[:preamble] ||= ""
|
|
19
19
|
properties[:postamble] ||= "#{properties[:bufvar]}"
|
|
20
20
|
|
|
21
|
-
# Tell
|
|
21
|
+
# Tell Erubi whether the template will be compiled with `frozen_string_literal: true`
|
|
22
22
|
properties[:freeze_template_literals] = !Template.frozen_string_literal
|
|
23
23
|
|
|
24
24
|
properties[:escapefunc] = ""
|
|
@@ -40,18 +40,22 @@ module ActionView
|
|
|
40
40
|
|
|
41
41
|
# Translate an error location returned by ErrorHighlight to the correct
|
|
42
42
|
# source location inside the template.
|
|
43
|
-
def translate_location(spot,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
def translate_location(spot, _backtrace_location, source)
|
|
44
|
+
compiled = spot[:script_lines]
|
|
45
|
+
highlight = compiled[spot[:first_lineno] - 1]&.byteslice((spot[:first_column] - 1)...spot[:last_column])
|
|
46
|
+
return nil if highlight.blank?
|
|
47
|
+
|
|
48
|
+
source_lines = source.lines
|
|
49
|
+
lineno_delta = find_lineno_offset(compiled, source_lines, highlight, spot[:first_lineno])
|
|
50
|
+
|
|
51
|
+
tokens = ::ERB::Util.tokenize(source_lines[spot[:first_lineno] - lineno_delta - 1])
|
|
52
|
+
column_delta = find_offset(spot[:snippet], tokens, spot[:first_column])
|
|
53
|
+
|
|
48
54
|
spot[:first_lineno] -= lineno_delta
|
|
49
55
|
spot[:last_lineno] -= lineno_delta
|
|
50
|
-
|
|
51
|
-
column_delta = spot[:first_column] - new_first_column
|
|
52
56
|
spot[:first_column] -= column_delta
|
|
53
57
|
spot[:last_column] -= column_delta
|
|
54
|
-
spot[:script_lines] =
|
|
58
|
+
spot[:script_lines] = source_lines
|
|
55
59
|
|
|
56
60
|
spot
|
|
57
61
|
rescue NotImplementedError, LocationParsingError
|
|
@@ -82,7 +86,7 @@ module ActionView
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
|
|
85
|
-
options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}
|
|
89
|
+
options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}\n-->';"
|
|
86
90
|
options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
|
|
87
91
|
end
|
|
88
92
|
|
|
@@ -105,51 +109,80 @@ module ActionView
|
|
|
105
109
|
raise WrongEncodingError.new(string, string.encoding)
|
|
106
110
|
end
|
|
107
111
|
|
|
112
|
+
# Return the offset between the error lineno and the source lineno.
|
|
113
|
+
# Searches in reverse from the backtrace lineno so we have a better
|
|
114
|
+
# chance of finding the correct line
|
|
115
|
+
#
|
|
116
|
+
# The compiled template is likely to be longer than the source.
|
|
117
|
+
# Use the difference between the compiled and source sizes to
|
|
118
|
+
# determine the earliest line that could contain the highlight.
|
|
119
|
+
def find_lineno_offset(compiled, source_lines, highlight, error_lineno)
|
|
120
|
+
first_index = error_lineno - 1 - compiled.size + source_lines.size
|
|
121
|
+
first_index = 0 if first_index < 0
|
|
122
|
+
|
|
123
|
+
last_index = error_lineno - 1
|
|
124
|
+
last_index = source_lines.size - 1 if last_index >= source_lines.size
|
|
125
|
+
|
|
126
|
+
last_index.downto(first_index) do |line_index|
|
|
127
|
+
next unless source_lines[line_index].include?(highlight)
|
|
128
|
+
return error_lineno - 1 - line_index
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
raise LocationParsingError, "Couldn't find code snippet"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Find which token in the source template spans the byte range that
|
|
135
|
+
# contains the error_column, then return the offset compared to the
|
|
136
|
+
# original source template.
|
|
137
|
+
#
|
|
138
|
+
# Iterate consecutive pairs of CODE or TEXT tokens, requiring
|
|
139
|
+
# a match of the first token before matching either token.
|
|
140
|
+
#
|
|
141
|
+
# For example, if we want to find tokens A, B, C, we do the following:
|
|
142
|
+
# 1. Find a match for A: test error_column or advance scanner.
|
|
143
|
+
# 2. Find a match for B or A:
|
|
144
|
+
# a. If B: start over with next token set (B, C).
|
|
145
|
+
# b. If A: test error_column or advance scanner.
|
|
146
|
+
# c. Otherwise: Advance 1 byte
|
|
147
|
+
#
|
|
148
|
+
# Prioritize matching the next token over the current token once
|
|
149
|
+
# a match for the current token has been found. This is to prevent
|
|
150
|
+
# the current token from looping past the next token if they both
|
|
151
|
+
# match (i.e. if the current token is a single space character).
|
|
108
152
|
def find_offset(compiled, source_tokens, error_column)
|
|
109
153
|
compiled = StringScanner.new(compiled)
|
|
154
|
+
offset_source_tokens(source_tokens).each_cons(2) do |(name, str, offset), (_, next_str, _)|
|
|
155
|
+
matched_str = false
|
|
110
156
|
|
|
111
|
-
|
|
157
|
+
until compiled.eos?
|
|
158
|
+
if matched_str && next_str && compiled.match?(next_str)
|
|
159
|
+
break
|
|
160
|
+
elsif compiled.match?(str)
|
|
161
|
+
matched_str = true
|
|
112
162
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
163
|
+
if name == :CODE && compiled.pos <= error_column && compiled.pos + str.bytesize >= error_column
|
|
164
|
+
return compiled.pos - offset
|
|
165
|
+
end
|
|
126
166
|
|
|
127
|
-
|
|
128
|
-
offset = error_column - compiled.pos
|
|
129
|
-
return passed_tokens.map(&:last).join.bytesize + offset
|
|
167
|
+
compiled.pos += str.bytesize
|
|
130
168
|
else
|
|
131
|
-
|
|
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
|
|
169
|
+
compiled.pos += 1
|
|
140
170
|
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
171
|
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
raise LocationParsingError, "Couldn't find code snippet"
|
|
175
|
+
end
|
|
150
176
|
|
|
151
|
-
|
|
177
|
+
def offset_source_tokens(source_tokens)
|
|
178
|
+
source_offset = 0
|
|
179
|
+
with_offset = source_tokens.filter_map do |name, str|
|
|
180
|
+
result = [:CODE, str, source_offset] if name == :CODE || name == :PLAIN
|
|
181
|
+
result = [:TEXT, str, source_offset] if name == :TEXT
|
|
182
|
+
source_offset += str.bytesize
|
|
183
|
+
result
|
|
152
184
|
end
|
|
185
|
+
with_offset << [:EOS, nil, source_offset]
|
|
153
186
|
end
|
|
154
187
|
end
|
|
155
188
|
end
|
|
@@ -14,10 +14,16 @@ module ActionView
|
|
|
14
14
|
|
|
15
15
|
def render(context, *args)
|
|
16
16
|
@renderable.render_in(context)
|
|
17
|
+
rescue NoMethodError
|
|
18
|
+
if !@renderable.respond_to?(:render_in)
|
|
19
|
+
raise ArgumentError, "'#{@renderable.inspect}' is not a renderable object. It must implement #render_in."
|
|
20
|
+
else
|
|
21
|
+
raise
|
|
22
|
+
end
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
def format
|
|
20
|
-
@renderable.format
|
|
26
|
+
@renderable.try(:format)
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -4,14 +4,11 @@ require "pathname"
|
|
|
4
4
|
require "active_support/core_ext/class"
|
|
5
5
|
require "active_support/core_ext/module/attribute_accessors"
|
|
6
6
|
require "action_view/template"
|
|
7
|
-
require "thread"
|
|
8
7
|
require "concurrent/map"
|
|
9
8
|
|
|
10
9
|
module ActionView
|
|
11
10
|
# = Action View Resolver
|
|
12
11
|
class Resolver
|
|
13
|
-
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
|
|
14
|
-
|
|
15
12
|
class PathParser # :nodoc:
|
|
16
13
|
ParsedPath = Struct.new(:path, :details)
|
|
17
14
|
|
data/lib/action_view/template.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "thread"
|
|
4
3
|
require "delegate"
|
|
5
4
|
|
|
6
5
|
module ActionView
|
|
@@ -8,7 +7,7 @@ module ActionView
|
|
|
8
7
|
class Template
|
|
9
8
|
extend ActiveSupport::Autoload
|
|
10
9
|
|
|
11
|
-
STRICT_LOCALS_REGEX = /\#\s+locals:\s+\((
|
|
10
|
+
STRICT_LOCALS_REGEX = /\#\s+locals:\s+\((.*?)\)(?=\s*-?%>|\s*$)/m
|
|
12
11
|
|
|
13
12
|
# === Encodings in ActionView::Template
|
|
14
13
|
#
|
|
@@ -148,6 +147,20 @@ module ActionView
|
|
|
148
147
|
# <p><%= alert %></p>
|
|
149
148
|
# <% end %>
|
|
150
149
|
#
|
|
150
|
+
# By default, templates will accept any <tt>locals</tt> as keyword arguments
|
|
151
|
+
# and make them available to <tt>local_assigns</tt>. To restrict what
|
|
152
|
+
# <tt>local_assigns</tt> a template will accept, add a <tt>locals:</tt> magic comment:
|
|
153
|
+
#
|
|
154
|
+
# <%# locals: (headline:, alerts: []) %>
|
|
155
|
+
#
|
|
156
|
+
# <h1><%= headline %></h1>
|
|
157
|
+
#
|
|
158
|
+
# <% alerts.each do |alert| %>
|
|
159
|
+
# <p><%= alert %></p>
|
|
160
|
+
# <% end %>
|
|
161
|
+
#
|
|
162
|
+
# Read more about strict locals in {Action View Overview}[https://guides.rubyonrails.org/action_view_overview.html#strict-locals]
|
|
163
|
+
# in the guides.
|
|
151
164
|
|
|
152
165
|
eager_autoload do
|
|
153
166
|
autoload :Error
|
|
@@ -216,11 +229,21 @@ module ActionView
|
|
|
216
229
|
end
|
|
217
230
|
|
|
218
231
|
def spot(location) # :nodoc:
|
|
219
|
-
ast = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
|
|
220
232
|
node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
|
|
221
|
-
|
|
233
|
+
found =
|
|
234
|
+
if RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism
|
|
235
|
+
require "prism"
|
|
222
236
|
|
|
223
|
-
|
|
237
|
+
if Prism::VERSION >= "1.0.0"
|
|
238
|
+
result = Prism.parse(compiled_source).value
|
|
239
|
+
result.breadth_first_search { |node| node.node_id == node_id }
|
|
240
|
+
end
|
|
241
|
+
else
|
|
242
|
+
node = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
|
|
243
|
+
find_node_by_id(node, node_id)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
ErrorHighlight.spot(found) if found
|
|
224
247
|
end
|
|
225
248
|
|
|
226
249
|
# Translate an error location returned by ErrorHighlight to the correct
|
|
@@ -333,7 +356,7 @@ module ActionView
|
|
|
333
356
|
|
|
334
357
|
# This method is responsible for marking a template as having strict locals
|
|
335
358
|
# which means the template can only accept the locals defined in a magic
|
|
336
|
-
# comment. For example, if your template
|
|
359
|
+
# comment. For example, if your template accepts the locals +title+ and
|
|
337
360
|
# +comment_count+, add the following to your template file:
|
|
338
361
|
#
|
|
339
362
|
# <%# locals: (title: "Default title", comment_count: 0) %>
|
|
@@ -343,7 +366,7 @@ module ActionView
|
|
|
343
366
|
def strict_locals!
|
|
344
367
|
if @strict_locals == NONE
|
|
345
368
|
self.source.sub!(STRICT_LOCALS_REGEX, "")
|
|
346
|
-
@strict_locals = $1
|
|
369
|
+
@strict_locals = $1&.rstrip
|
|
347
370
|
|
|
348
371
|
return if @strict_locals.nil? # Magic comment not found
|
|
349
372
|
|
|
@@ -425,9 +448,9 @@ module ActionView
|
|
|
425
448
|
method_arguments =
|
|
426
449
|
if set_strict_locals
|
|
427
450
|
if set_strict_locals.include?("&")
|
|
428
|
-
"output_buffer, #{set_strict_locals}"
|
|
451
|
+
"local_assigns, output_buffer, #{set_strict_locals}"
|
|
429
452
|
else
|
|
430
|
-
"output_buffer, #{set_strict_locals}, &_"
|
|
453
|
+
"local_assigns, output_buffer, #{set_strict_locals}, &_"
|
|
431
454
|
end
|
|
432
455
|
else
|
|
433
456
|
"local_assigns, output_buffer, &_"
|
|
@@ -486,11 +509,12 @@ module ActionView
|
|
|
486
509
|
|
|
487
510
|
return unless strict_locals?
|
|
488
511
|
|
|
489
|
-
parameters = mod.instance_method(method_name).parameters
|
|
512
|
+
parameters = mod.instance_method(method_name).parameters
|
|
513
|
+
parameters -= [[:req, :local_assigns], [:req, :output_buffer]]
|
|
514
|
+
|
|
490
515
|
# Check compiled method parameters to ensure that only kwargs
|
|
491
516
|
# were provided as strict locals, preventing `locals: (foo, *foo)` etc
|
|
492
517
|
# and allowing `locals: (foo:)`.
|
|
493
|
-
|
|
494
518
|
non_kwarg_parameters = parameters.select do |parameter|
|
|
495
519
|
![:keyreq, :key, :keyrest, :nokey].include?(parameter[0])
|
|
496
520
|
end
|
|
@@ -531,12 +555,15 @@ module ActionView
|
|
|
531
555
|
end
|
|
532
556
|
end
|
|
533
557
|
|
|
558
|
+
RUBY_RESERVED_KEYWORDS = ::ActiveSupport::Delegation::RUBY_RESERVED_KEYWORDS
|
|
559
|
+
private_constant :RUBY_RESERVED_KEYWORDS
|
|
560
|
+
|
|
534
561
|
def locals_code
|
|
535
562
|
return "" if strict_locals?
|
|
536
563
|
|
|
537
564
|
# Only locals with valid variable names get set directly. Others will
|
|
538
565
|
# still be available in local_assigns.
|
|
539
|
-
locals = @locals -
|
|
566
|
+
locals = @locals - RUBY_RESERVED_KEYWORDS
|
|
540
567
|
|
|
541
568
|
locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
|
|
542
569
|
|
|
@@ -60,7 +60,56 @@ module ActionView
|
|
|
60
60
|
include ActiveSupport::Testing::ConstantLookup
|
|
61
61
|
|
|
62
62
|
delegate :lookup_context, to: :controller
|
|
63
|
-
attr_accessor :controller, :request, :output_buffer
|
|
63
|
+
attr_accessor :controller, :request, :output_buffer
|
|
64
|
+
|
|
65
|
+
# Returns the content rendered by the last +render+ call.
|
|
66
|
+
#
|
|
67
|
+
# The returned object behaves like a string but also exposes a number of methods
|
|
68
|
+
# that allows you to parse the content string in formats registered using
|
|
69
|
+
# <tt>.register_parser</tt>.
|
|
70
|
+
#
|
|
71
|
+
# By default includes the following parsers:
|
|
72
|
+
#
|
|
73
|
+
# +.html+
|
|
74
|
+
#
|
|
75
|
+
# Parse the <tt>rendered</tt> content String into HTML. By default, this means
|
|
76
|
+
# a <tt>Nokogiri::XML::Node</tt>.
|
|
77
|
+
#
|
|
78
|
+
# test "renders HTML" do
|
|
79
|
+
# article = Article.create!(title: "Hello, world")
|
|
80
|
+
#
|
|
81
|
+
# render partial: "articles/article", locals: { article: article }
|
|
82
|
+
#
|
|
83
|
+
# assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# To parse the rendered content into a <tt>Capybara::Simple::Node</tt>,
|
|
87
|
+
# re-register an <tt>:html</tt> parser with a call to
|
|
88
|
+
# <tt>Capybara.string</tt>:
|
|
89
|
+
#
|
|
90
|
+
# register_parser :html, -> rendered { Capybara.string(rendered) }
|
|
91
|
+
#
|
|
92
|
+
# test "renders HTML" do
|
|
93
|
+
# article = Article.create!(title: "Hello, world")
|
|
94
|
+
#
|
|
95
|
+
# render partial: article
|
|
96
|
+
#
|
|
97
|
+
# rendered.html.assert_css "h1", text: "Hello, world"
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# +.json+
|
|
101
|
+
#
|
|
102
|
+
# Parse the <tt>rendered</tt> content String into JSON. By default, this means
|
|
103
|
+
# a <tt>ActiveSupport::HashWithIndifferentAccess</tt>.
|
|
104
|
+
#
|
|
105
|
+
# test "renders JSON" do
|
|
106
|
+
# article = Article.create!(title: "Hello, world")
|
|
107
|
+
#
|
|
108
|
+
# render formats: :json, partial: "articles/article", locals: { article: article }
|
|
109
|
+
#
|
|
110
|
+
# assert_pattern { rendered.json => { title: "Hello, world" } }
|
|
111
|
+
# end
|
|
112
|
+
attr_accessor :rendered
|
|
64
113
|
|
|
65
114
|
module ClassMethods
|
|
66
115
|
def inherited(descendant) # :nodoc:
|
|
@@ -171,10 +220,9 @@ module ActionView
|
|
|
171
220
|
# Almost a duplicate from ActionController::Helpers
|
|
172
221
|
methods.flatten.each do |method|
|
|
173
222
|
_helpers_for_modification.module_eval <<~end_eval, __FILE__, __LINE__ + 1
|
|
174
|
-
def #{method}(
|
|
175
|
-
_test_case.send(:'#{method}',
|
|
176
|
-
end
|
|
177
|
-
ruby2_keywords(:'#{method}')
|
|
223
|
+
def #{method}(...) # def current_user(...)
|
|
224
|
+
_test_case.send(:'#{method}', ...) # _test_case.send(:'current_user', ...)
|
|
225
|
+
end # end
|
|
178
226
|
end_eval
|
|
179
227
|
end
|
|
180
228
|
end
|
|
@@ -202,7 +250,7 @@ module ActionView
|
|
|
202
250
|
|
|
203
251
|
setup :setup_with_controller
|
|
204
252
|
|
|
205
|
-
register_parser :html, -> rendered { Rails::Dom::Testing.
|
|
253
|
+
register_parser :html, -> rendered { Rails::Dom::Testing.html_document_fragment.parse(rendered) }
|
|
206
254
|
register_parser :json, -> rendered { JSON.parse(rendered, object_class: ActiveSupport::HashWithIndifferentAccess) }
|
|
207
255
|
|
|
208
256
|
ActiveSupport.run_load_hooks(:action_view_test_case, self)
|
|
@@ -244,57 +292,6 @@ module ActionView
|
|
|
244
292
|
@_rendered_views ||= RenderedViewsCollection.new
|
|
245
293
|
end
|
|
246
294
|
|
|
247
|
-
##
|
|
248
|
-
# :method: rendered
|
|
249
|
-
#
|
|
250
|
-
# Returns the content rendered by the last +render+ call.
|
|
251
|
-
#
|
|
252
|
-
# The returned object behaves like a string but also exposes a number of methods
|
|
253
|
-
# that allows you to parse the content string in formats registered using
|
|
254
|
-
# <tt>.register_parser</tt>.
|
|
255
|
-
#
|
|
256
|
-
# By default includes the following parsers:
|
|
257
|
-
#
|
|
258
|
-
# +.html+
|
|
259
|
-
#
|
|
260
|
-
# Parse the <tt>rendered</tt> content String into HTML. By default, this means
|
|
261
|
-
# a <tt>Nokogiri::XML::Node</tt>.
|
|
262
|
-
#
|
|
263
|
-
# test "renders HTML" do
|
|
264
|
-
# article = Article.create!(title: "Hello, world")
|
|
265
|
-
#
|
|
266
|
-
# render partial: "articles/article", locals: { article: article }
|
|
267
|
-
#
|
|
268
|
-
# assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
|
|
269
|
-
# end
|
|
270
|
-
#
|
|
271
|
-
# To parse the rendered content into a <tt>Capybara::Simple::Node</tt>,
|
|
272
|
-
# re-register an <tt>:html</tt> parser with a call to
|
|
273
|
-
# <tt>Capybara.string</tt>:
|
|
274
|
-
#
|
|
275
|
-
# register_parser :html, -> rendered { Capybara.string(rendered) }
|
|
276
|
-
#
|
|
277
|
-
# test "renders HTML" do
|
|
278
|
-
# article = Article.create!(title: "Hello, world")
|
|
279
|
-
#
|
|
280
|
-
# render partial: article
|
|
281
|
-
#
|
|
282
|
-
# rendered.html.assert_css "h1", text: "Hello, world"
|
|
283
|
-
# end
|
|
284
|
-
#
|
|
285
|
-
# +.json+
|
|
286
|
-
#
|
|
287
|
-
# Parse the <tt>rendered</tt> content String into JSON. By default, this means
|
|
288
|
-
# a <tt>ActiveSupport::HashWithIndifferentAccess</tt>.
|
|
289
|
-
#
|
|
290
|
-
# test "renders JSON" do
|
|
291
|
-
# article = Article.create!(title: "Hello, world")
|
|
292
|
-
#
|
|
293
|
-
# render formats: :json, partial: "articles/article", locals: { article: article }
|
|
294
|
-
#
|
|
295
|
-
# assert_pattern { rendered.json => { title: "Hello, world" } }
|
|
296
|
-
# end
|
|
297
|
-
|
|
298
295
|
def _routes
|
|
299
296
|
@controller._routes if @controller.respond_to?(:_routes)
|
|
300
297
|
end
|
|
@@ -302,7 +299,6 @@ module ActionView
|
|
|
302
299
|
class RenderedViewContent < String # :nodoc:
|
|
303
300
|
end
|
|
304
301
|
|
|
305
|
-
# Need to experiment if this priority is the best one: rendered => output_buffer
|
|
306
302
|
class RenderedViewsCollection
|
|
307
303
|
def initialize
|
|
308
304
|
@rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
|
|
@@ -416,7 +412,7 @@ module ActionView
|
|
|
416
412
|
end]
|
|
417
413
|
end
|
|
418
414
|
|
|
419
|
-
def method_missing(selector,
|
|
415
|
+
def method_missing(selector, ...)
|
|
420
416
|
begin
|
|
421
417
|
routes = @controller.respond_to?(:_routes) && @controller._routes
|
|
422
418
|
rescue
|
|
@@ -426,16 +422,15 @@ module ActionView
|
|
|
426
422
|
if routes &&
|
|
427
423
|
(routes.named_routes.route_defined?(selector) ||
|
|
428
424
|
routes.mounted_helpers.method_defined?(selector))
|
|
429
|
-
@controller.__send__(selector,
|
|
425
|
+
@controller.__send__(selector, ...)
|
|
430
426
|
else
|
|
431
427
|
super
|
|
432
428
|
end
|
|
433
429
|
end
|
|
434
|
-
ruby2_keywords(:method_missing)
|
|
435
430
|
|
|
436
431
|
def respond_to_missing?(name, include_private = false)
|
|
437
432
|
begin
|
|
438
|
-
routes =
|
|
433
|
+
routes = @controller.respond_to?(:_routes) && @controller._routes
|
|
439
434
|
rescue
|
|
440
435
|
# Don't call routes, if there is an error on _routes call
|
|
441
436
|
end
|
data/lib/action_view.rb
CHANGED
|
@@ -81,6 +81,7 @@ module ActionView
|
|
|
81
81
|
autoload :MissingTemplate
|
|
82
82
|
autoload :ActionViewError
|
|
83
83
|
autoload :EncodingError
|
|
84
|
+
autoload :StrictLocalsError
|
|
84
85
|
autoload :TemplateError
|
|
85
86
|
autoload :SyntaxErrorInTemplate
|
|
86
87
|
autoload :WrongEncodingError
|
|
@@ -90,6 +91,9 @@ module ActionView
|
|
|
90
91
|
autoload :CacheExpiry
|
|
91
92
|
autoload :TestCase
|
|
92
93
|
|
|
94
|
+
singleton_class.attr_accessor :render_tracker
|
|
95
|
+
self.render_tracker = :regex
|
|
96
|
+
|
|
93
97
|
def self.eager_load!
|
|
94
98
|
super
|
|
95
99
|
ActionView::Helpers.eager_load!
|