herb 0.6.1-x86-linux-musl → 0.7.0-x86-linux-musl
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/README.md +1 -0
- data/ext/herb/nodes.c +6 -4
- data/lib/herb/3.0/herb.so +0 -0
- data/lib/herb/3.1/herb.so +0 -0
- data/lib/herb/3.2/herb.so +0 -0
- data/lib/herb/3.3/herb.so +0 -0
- data/lib/herb/3.4/herb.so +0 -0
- data/lib/herb/ast/helpers.rb +26 -0
- data/lib/herb/ast/nodes.rb +7 -3
- data/lib/herb/cli.rb +158 -1
- data/lib/herb/engine/compiler.rb +399 -0
- data/lib/herb/engine/debug_visitor.rb +321 -0
- data/lib/herb/engine/error_formatter.rb +420 -0
- data/lib/herb/engine/parser_error_overlay.rb +767 -0
- data/lib/herb/engine/validation_error_overlay.rb +182 -0
- data/lib/herb/engine/validation_errors.rb +65 -0
- data/lib/herb/engine/validator.rb +75 -0
- data/lib/herb/engine/validators/accessibility_validator.rb +31 -0
- data/lib/herb/engine/validators/nesting_validator.rb +95 -0
- data/lib/herb/engine/validators/security_validator.rb +71 -0
- data/lib/herb/engine.rb +366 -0
- data/lib/herb/project.rb +3 -3
- data/lib/herb/version.rb +1 -1
- data/lib/herb/visitor.rb +2 -0
- data/lib/herb.rb +2 -0
- data/sig/herb/ast/helpers.rbs +16 -0
- data/sig/herb/ast/nodes.rbs +4 -2
- data/sig/herb/engine/compiler.rbs +109 -0
- data/sig/herb/engine/debug.rbs +38 -0
- data/sig/herb/engine/debug_visitor.rbs +70 -0
- data/sig/herb/engine/error_formatter.rbs +47 -0
- data/sig/herb/engine/parser_error_overlay.rbs +41 -0
- data/sig/herb/engine/validation_error_overlay.rbs +35 -0
- data/sig/herb/engine/validation_errors.rbs +45 -0
- data/sig/herb/engine/validator.rbs +37 -0
- data/sig/herb/engine/validators/accessibility_validator.rbs +19 -0
- data/sig/herb/engine/validators/nesting_validator.rbs +25 -0
- data/sig/herb/engine/validators/security_validator.rbs +23 -0
- data/sig/herb/engine.rbs +72 -0
- data/sig/herb/visitor.rbs +2 -0
- data/sig/herb_c_extension.rbs +7 -0
- data/sig/serialized_ast_nodes.rbs +1 -0
- data/src/ast_nodes.c +2 -1
- data/src/ast_pretty_print.c +2 -1
- data/src/element_source.c +11 -0
- data/src/include/ast_nodes.h +3 -1
- data/src/include/element_source.h +13 -0
- data/src/include/version.h +1 -1
- data/src/parser.c +3 -0
- data/src/parser_helpers.c +1 -0
- metadata +30 -2
@@ -0,0 +1,399 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module Herb
|
4
|
+
class Engine
|
5
|
+
class Compiler < ::Herb::Visitor
|
6
|
+
attr_reader :tokens
|
7
|
+
|
8
|
+
def initialize(engine, options = {})
|
9
|
+
super()
|
10
|
+
|
11
|
+
@engine = engine
|
12
|
+
@escape = options.fetch(:escape) { options.fetch(:escape_html, false) }
|
13
|
+
@tokens = [] #: Array[untyped]
|
14
|
+
@element_stack = [] #: Array[String]
|
15
|
+
@context_stack = [:html_content]
|
16
|
+
@trim_next_whitespace = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_output
|
20
|
+
optimized_tokens = optimize_tokens(@tokens)
|
21
|
+
|
22
|
+
optimized_tokens.each do |type, value, context|
|
23
|
+
case type
|
24
|
+
when :text
|
25
|
+
@engine.send(:add_text, value)
|
26
|
+
when :code
|
27
|
+
@engine.send(:add_code, value)
|
28
|
+
when :expr
|
29
|
+
if [:attribute_value, :script_content, :style_content].include?(context)
|
30
|
+
add_context_aware_expression(value, context)
|
31
|
+
else
|
32
|
+
indicator = @escape ? "==" : "="
|
33
|
+
@engine.send(:add_expression, indicator, value)
|
34
|
+
end
|
35
|
+
when :expr_escaped
|
36
|
+
if [:attribute_value, :script_content, :style_content].include?(context)
|
37
|
+
add_context_aware_expression(value, context)
|
38
|
+
else
|
39
|
+
indicator = @escape ? "=" : "=="
|
40
|
+
@engine.send(:add_expression, indicator, value)
|
41
|
+
end
|
42
|
+
when :expr_block
|
43
|
+
indicator = @escape ? "==" : "="
|
44
|
+
@engine.send(:add_expression_block, indicator, value)
|
45
|
+
when :expr_block_escaped
|
46
|
+
indicator = @escape ? "=" : "=="
|
47
|
+
@engine.send(:add_expression_block, indicator, value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def visit_document_node(node)
|
53
|
+
visit_all(node.children)
|
54
|
+
end
|
55
|
+
|
56
|
+
def visit_html_element_node(node)
|
57
|
+
tag_name = node.tag_name&.value&.downcase
|
58
|
+
|
59
|
+
@element_stack.push(tag_name) if tag_name
|
60
|
+
|
61
|
+
if tag_name == "script"
|
62
|
+
push_context(:script_content)
|
63
|
+
elsif tag_name == "style"
|
64
|
+
push_context(:style_content)
|
65
|
+
end
|
66
|
+
|
67
|
+
visit(node.open_tag)
|
68
|
+
visit_all(node.body)
|
69
|
+
visit(node.close_tag)
|
70
|
+
|
71
|
+
pop_context if %w[script style].include?(tag_name)
|
72
|
+
|
73
|
+
@element_stack.pop if tag_name
|
74
|
+
end
|
75
|
+
|
76
|
+
def visit_html_open_tag_node(node)
|
77
|
+
add_text(node.tag_opening&.value || "<")
|
78
|
+
add_text(node.tag_name.value) if node.tag_name
|
79
|
+
|
80
|
+
visit_all(node.children)
|
81
|
+
|
82
|
+
add_text(node.tag_closing&.value || ">")
|
83
|
+
end
|
84
|
+
|
85
|
+
def visit_html_attribute_node(node)
|
86
|
+
add_text(" ")
|
87
|
+
|
88
|
+
visit(node.name)
|
89
|
+
|
90
|
+
return unless node.value
|
91
|
+
|
92
|
+
add_text(node.equals.value)
|
93
|
+
visit(node.value)
|
94
|
+
end
|
95
|
+
|
96
|
+
def visit_html_attribute_name_node(node)
|
97
|
+
visit_all(node.children)
|
98
|
+
end
|
99
|
+
|
100
|
+
def visit_html_attribute_value_node(node)
|
101
|
+
push_context(:attribute_value)
|
102
|
+
|
103
|
+
add_text(node.open_quote&.value) if node.quoted
|
104
|
+
|
105
|
+
visit_all(node.children)
|
106
|
+
|
107
|
+
add_text(node.close_quote&.value) if node.quoted
|
108
|
+
|
109
|
+
pop_context
|
110
|
+
end
|
111
|
+
|
112
|
+
def visit_html_close_tag_node(node)
|
113
|
+
tag_name = node.tag_name&.value&.downcase
|
114
|
+
|
115
|
+
if @engine.content_for_head && tag_name == "head"
|
116
|
+
escaped_html = @engine.content_for_head.gsub("'", "\\\\'")
|
117
|
+
@tokens << [:expr, "'#{escaped_html}'.html_safe", current_context]
|
118
|
+
end
|
119
|
+
|
120
|
+
add_text(node.tag_opening&.value)
|
121
|
+
add_text(node.tag_name&.value)
|
122
|
+
add_text(node.tag_closing&.value)
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_html_text_node(node)
|
126
|
+
add_text(node.content)
|
127
|
+
end
|
128
|
+
|
129
|
+
def visit_literal_node(node)
|
130
|
+
add_text(node.content)
|
131
|
+
end
|
132
|
+
|
133
|
+
def visit_whitespace_node(node)
|
134
|
+
add_text(node.value.value) if node.value
|
135
|
+
end
|
136
|
+
|
137
|
+
def visit_html_comment_node(node)
|
138
|
+
add_text(node.comment_start.value)
|
139
|
+
visit_all(node.children)
|
140
|
+
add_text(node.comment_end.value)
|
141
|
+
end
|
142
|
+
|
143
|
+
def visit_html_doctype_node(node)
|
144
|
+
add_text(node.tag_opening.value)
|
145
|
+
visit_all(node.children)
|
146
|
+
add_text(node.tag_closing.value)
|
147
|
+
end
|
148
|
+
|
149
|
+
def visit_xml_declaration_node(node)
|
150
|
+
add_text(node.tag_opening.value)
|
151
|
+
visit_all(node.children)
|
152
|
+
add_text(node.tag_closing.value)
|
153
|
+
end
|
154
|
+
|
155
|
+
def visit_cdata_node(node)
|
156
|
+
add_text(node.cdata_opening.value)
|
157
|
+
visit_all(node.children)
|
158
|
+
add_text(node.cdata_closing.value)
|
159
|
+
end
|
160
|
+
|
161
|
+
def visit_erb_content_node(node)
|
162
|
+
process_erb_tag(node)
|
163
|
+
end
|
164
|
+
|
165
|
+
def visit_erb_control_node(node, &_block)
|
166
|
+
add_code(node.content.value.strip)
|
167
|
+
|
168
|
+
yield if block_given?
|
169
|
+
end
|
170
|
+
|
171
|
+
def visit_erb_if_node(node)
|
172
|
+
visit_erb_control_node(node) do
|
173
|
+
visit_all(node.statements)
|
174
|
+
visit(node.subsequent)
|
175
|
+
visit(node.end_node)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def visit_erb_else_node(node)
|
180
|
+
visit_erb_control_node(node) do
|
181
|
+
visit_all(node.statements)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def visit_erb_unless_node(node)
|
186
|
+
visit_erb_control_node(node) do
|
187
|
+
visit_all(node.statements)
|
188
|
+
visit(node.else_clause)
|
189
|
+
visit(node.end_node)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def visit_erb_case_node(node)
|
194
|
+
visit_erb_control_with_parts(node, :conditions, :else_clause, :end_node)
|
195
|
+
end
|
196
|
+
|
197
|
+
def visit_erb_when_node(node)
|
198
|
+
visit_erb_control_with_parts(node, :statements)
|
199
|
+
end
|
200
|
+
|
201
|
+
def visit_erb_for_node(node)
|
202
|
+
visit_erb_control_with_parts(node, :statements, :end_node)
|
203
|
+
end
|
204
|
+
|
205
|
+
def visit_erb_while_node(node)
|
206
|
+
visit_erb_control_with_parts(node, :statements, :end_node)
|
207
|
+
end
|
208
|
+
|
209
|
+
def visit_erb_until_node(node)
|
210
|
+
visit_erb_control_with_parts(node, :statements, :end_node)
|
211
|
+
end
|
212
|
+
|
213
|
+
def visit_erb_begin_node(node)
|
214
|
+
visit_erb_control_with_parts(node, :statements, :rescue_clause, :else_clause, :ensure_clause, :end_node)
|
215
|
+
end
|
216
|
+
|
217
|
+
def visit_erb_rescue_node(node)
|
218
|
+
visit_erb_control_with_parts(node, :statements, :subsequent)
|
219
|
+
end
|
220
|
+
|
221
|
+
def visit_erb_ensure_node(node)
|
222
|
+
visit_erb_control_with_parts(node, :statements)
|
223
|
+
end
|
224
|
+
|
225
|
+
def visit_erb_end_node(node)
|
226
|
+
visit_erb_control_node(node)
|
227
|
+
end
|
228
|
+
|
229
|
+
def visit_erb_case_match_node(node)
|
230
|
+
visit_erb_control_with_parts(node, :children, :conditions, :else_clause, :end_node)
|
231
|
+
end
|
232
|
+
|
233
|
+
def visit_erb_in_node(node)
|
234
|
+
visit_erb_control_with_parts(node, :statements)
|
235
|
+
end
|
236
|
+
|
237
|
+
def visit_erb_yield_node(node)
|
238
|
+
process_erb_tag(node, skip_comment_check: true)
|
239
|
+
end
|
240
|
+
|
241
|
+
def visit_erb_block_node(node)
|
242
|
+
opening = node.tag_opening.value
|
243
|
+
|
244
|
+
if opening.include?("=")
|
245
|
+
should_escape = should_escape_output?(opening)
|
246
|
+
code = node.content.value.strip
|
247
|
+
|
248
|
+
@tokens << if should_escape
|
249
|
+
[:expr_block_escaped, code, current_context]
|
250
|
+
else
|
251
|
+
[:expr_block, code, current_context]
|
252
|
+
end
|
253
|
+
|
254
|
+
visit_all(node.body)
|
255
|
+
visit(node.end_node)
|
256
|
+
else
|
257
|
+
visit_erb_control_node(node) do
|
258
|
+
visit_all(node.body)
|
259
|
+
visit(node.end_node)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def visit_erb_control_with_parts(node, *parts)
|
265
|
+
visit_erb_control_node(node) do
|
266
|
+
parts.each do |part|
|
267
|
+
value = node.send(part)
|
268
|
+
case value
|
269
|
+
when Array
|
270
|
+
visit_all(value)
|
271
|
+
when nil
|
272
|
+
# Skip nil values
|
273
|
+
else
|
274
|
+
visit(value)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
|
282
|
+
def current_context
|
283
|
+
@context_stack.last
|
284
|
+
end
|
285
|
+
|
286
|
+
def push_context(context)
|
287
|
+
@context_stack.push(context)
|
288
|
+
end
|
289
|
+
|
290
|
+
def pop_context
|
291
|
+
@context_stack.pop
|
292
|
+
end
|
293
|
+
|
294
|
+
def add_context_aware_expression(code, context)
|
295
|
+
case context
|
296
|
+
when :attribute_value
|
297
|
+
@engine.send(:with_buffer) { @engine.instance_variable_get(:@src) << " << ::Herb::Engine.attr((" << code << "))" }
|
298
|
+
when :script_content
|
299
|
+
@engine.send(:with_buffer) { @engine.instance_variable_get(:@src) << " << ::Herb::Engine.js((" << code << "))" }
|
300
|
+
when :style_content
|
301
|
+
@engine.send(:with_buffer) { @engine.instance_variable_get(:@src) << " << ::Herb::Engine.css((" << code << "))" }
|
302
|
+
else
|
303
|
+
@engine.send(:add_expression_result_escaped, code)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def process_erb_tag(node, skip_comment_check: false)
|
308
|
+
opening = node.tag_opening.value
|
309
|
+
|
310
|
+
return if !skip_comment_check && erb_comment?(opening)
|
311
|
+
|
312
|
+
code = node.content.value.strip
|
313
|
+
|
314
|
+
if erb_output?(opening)
|
315
|
+
process_erb_output(opening, code)
|
316
|
+
else
|
317
|
+
add_code(code)
|
318
|
+
end
|
319
|
+
|
320
|
+
handle_whitespace_trimming(node)
|
321
|
+
end
|
322
|
+
|
323
|
+
def add_text(text)
|
324
|
+
return if text.empty?
|
325
|
+
|
326
|
+
if @trim_next_whitespace
|
327
|
+
text = text.lstrip
|
328
|
+
@trim_next_whitespace = false
|
329
|
+
end
|
330
|
+
|
331
|
+
return if text.empty?
|
332
|
+
|
333
|
+
@tokens << [:text, text, current_context]
|
334
|
+
end
|
335
|
+
|
336
|
+
def add_code(code)
|
337
|
+
@tokens << [:code, code, current_context]
|
338
|
+
end
|
339
|
+
|
340
|
+
def add_expression(code)
|
341
|
+
@tokens << [:expr, code, current_context]
|
342
|
+
end
|
343
|
+
|
344
|
+
def add_expression_escaped(code)
|
345
|
+
@tokens << [:expr_escaped, code, current_context]
|
346
|
+
end
|
347
|
+
|
348
|
+
def optimize_tokens(tokens)
|
349
|
+
return tokens if tokens.empty?
|
350
|
+
|
351
|
+
optimized = [] #: Array[untyped]
|
352
|
+
current_text = ""
|
353
|
+
current_context = nil
|
354
|
+
|
355
|
+
tokens.each do |type, value, context|
|
356
|
+
if type == :text
|
357
|
+
current_text += value
|
358
|
+
current_context ||= context
|
359
|
+
else
|
360
|
+
unless current_text.empty?
|
361
|
+
optimized << [:text, current_text, current_context]
|
362
|
+
|
363
|
+
current_text = ""
|
364
|
+
current_context = nil
|
365
|
+
end
|
366
|
+
|
367
|
+
optimized << [type, value, context]
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
optimized << [:text, current_text, current_context] unless current_text.empty?
|
372
|
+
|
373
|
+
optimized
|
374
|
+
end
|
375
|
+
|
376
|
+
def process_erb_output(opening, code)
|
377
|
+
should_escape = should_escape_output?(opening)
|
378
|
+
add_expression_with_escaping(code, should_escape)
|
379
|
+
end
|
380
|
+
|
381
|
+
def should_escape_output?(opening)
|
382
|
+
is_double_equals = opening == "<%=="
|
383
|
+
is_double_equals ? !@escape : @escape
|
384
|
+
end
|
385
|
+
|
386
|
+
def add_expression_with_escaping(code, should_escape)
|
387
|
+
if should_escape
|
388
|
+
add_expression_escaped(code)
|
389
|
+
else
|
390
|
+
add_expression(code)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def handle_whitespace_trimming(node)
|
395
|
+
@trim_next_whitespace = true if node.tag_closing&.value == "-%>"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,321 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Herb
|
4
|
+
class Engine
|
5
|
+
class DebugVisitor < Herb::Visitor
|
6
|
+
def initialize(engine)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@engine = engine
|
10
|
+
@top_level_elements = [] #: Array[Herb::AST::HTMLElementNode]
|
11
|
+
@element_stack = [] #: Array[String]
|
12
|
+
@debug_attributes_applied = false
|
13
|
+
@in_attribute = false
|
14
|
+
@in_html_comment = false
|
15
|
+
@in_html_doctype = false
|
16
|
+
@erb_nodes_to_wrap = [] #: Array[Herb::AST::ERBContentNode]
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug_enabled?
|
20
|
+
@engine.debug
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_document_node(node)
|
24
|
+
return unless debug_enabled?
|
25
|
+
|
26
|
+
find_top_level_elements(node)
|
27
|
+
|
28
|
+
super
|
29
|
+
|
30
|
+
wrap_all_erb_nodes(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def visit_html_element_node(node)
|
34
|
+
return super unless debug_enabled?
|
35
|
+
|
36
|
+
tag_name = node.tag_name&.value&.downcase
|
37
|
+
@element_stack.push(tag_name) if tag_name
|
38
|
+
|
39
|
+
add_debug_attributes_to_element(node.open_tag) if should_add_debug_attributes_to_element?(node.open_tag)
|
40
|
+
|
41
|
+
super
|
42
|
+
|
43
|
+
@element_stack.pop if tag_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_html_attribute_node(node)
|
47
|
+
@in_attribute = true
|
48
|
+
super
|
49
|
+
@in_attribute = false
|
50
|
+
end
|
51
|
+
|
52
|
+
def visit_html_comment_node(node)
|
53
|
+
@in_html_comment = true
|
54
|
+
super
|
55
|
+
@in_html_comment = false
|
56
|
+
end
|
57
|
+
|
58
|
+
def visit_html_doctype_node(node)
|
59
|
+
@in_html_doctype = true
|
60
|
+
super
|
61
|
+
@in_html_doctype = false
|
62
|
+
end
|
63
|
+
|
64
|
+
def visit_erb_content_node(node)
|
65
|
+
if debug_enabled? && !@in_attribute && !@in_html_comment && !@in_html_doctype && !in_excluded_context? && erb_output?(node.tag_opening.value)
|
66
|
+
code = node.content.value.strip
|
67
|
+
|
68
|
+
@erb_nodes_to_wrap << node unless complex_rails_helper?(code)
|
69
|
+
end
|
70
|
+
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
def visit_erb_yield_node(_node)
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def wrap_all_erb_nodes(node)
|
81
|
+
replace_erb_nodes_recursive(node)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates a dummy location for AST nodes that don't need real location info
|
85
|
+
#: () -> Herb::Location
|
86
|
+
def dummy_location
|
87
|
+
@dummy_location ||= Herb::Location.from(0, 0, 0, 0)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates a dummy range for tokens that don't need real range info
|
91
|
+
#: () -> Herb::Range
|
92
|
+
def dummy_range
|
93
|
+
@dummy_range ||= Herb::Range.from(0, 0)
|
94
|
+
end
|
95
|
+
|
96
|
+
def replace_erb_nodes_recursive(node)
|
97
|
+
array_properties = [:children, :body, :statements]
|
98
|
+
|
99
|
+
array_properties.each do |prop|
|
100
|
+
next unless node.respond_to?(prop) && node.send(prop).is_a?(Array)
|
101
|
+
|
102
|
+
array = node.send(prop)
|
103
|
+
|
104
|
+
array.each_with_index do |child, index|
|
105
|
+
if @erb_nodes_to_wrap.include?(child)
|
106
|
+
debug_span = create_debug_span_for_erb(child)
|
107
|
+
array[index] = debug_span
|
108
|
+
else
|
109
|
+
replace_erb_nodes_recursive(child)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
node_properties = [:subsequent, :else_clause, :end_node, :rescue_clause, :ensure_clause]
|
115
|
+
|
116
|
+
node_properties.each do |prop|
|
117
|
+
if node.respond_to?(prop) && node.send(prop)
|
118
|
+
child_node = node.send(prop)
|
119
|
+
replace_erb_nodes_recursive(child_node)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def find_top_level_elements(document_node)
|
125
|
+
@top_level_elements = [] #: Array[Herb::AST::HTMLElementNode]
|
126
|
+
|
127
|
+
document_node.children.each do |child|
|
128
|
+
@top_level_elements << child if child.is_a?(Herb::AST::HTMLElementNode)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def should_add_debug_attributes_to_element?(open_tag_node)
|
133
|
+
return false if @debug_attributes_applied
|
134
|
+
|
135
|
+
parent_element = find_parent_element_for_open_tag(open_tag_node)
|
136
|
+
return false unless parent_element
|
137
|
+
|
138
|
+
return @top_level_elements.first == parent_element if @top_level_elements.length >= 1
|
139
|
+
|
140
|
+
false
|
141
|
+
end
|
142
|
+
|
143
|
+
def find_parent_element_for_open_tag(open_tag_node)
|
144
|
+
@top_level_elements.find { |element| element.open_tag == open_tag_node }
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_debug_attributes_to_element(open_tag_node)
|
148
|
+
return if @debug_attributes_applied
|
149
|
+
|
150
|
+
view_type = determine_view_type
|
151
|
+
|
152
|
+
debug_attributes = [
|
153
|
+
create_debug_attribute("data-herb-debug-outline-type", view_type),
|
154
|
+
create_debug_attribute("data-herb-debug-file-name", @engine.filename&.basename&.to_s || "unknown"),
|
155
|
+
create_debug_attribute("data-herb-debug-file-relative-path", @engine.relative_file_path || ""),
|
156
|
+
create_debug_attribute("data-herb-debug-file-full-path", @engine.filename&.to_s || "unknown")
|
157
|
+
]
|
158
|
+
|
159
|
+
if @top_level_elements.length > 1
|
160
|
+
debug_attributes << create_debug_attribute("data-herb-debug-attach-to-parent", "true")
|
161
|
+
end
|
162
|
+
|
163
|
+
debug_attributes.each do |attr|
|
164
|
+
open_tag_node.children << attr
|
165
|
+
end
|
166
|
+
|
167
|
+
@debug_attributes_applied = true
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_debug_attribute(name, value)
|
171
|
+
name_literal = Herb::AST::LiteralNode.new("LiteralNode", dummy_location, [], name.dup)
|
172
|
+
name_node = Herb::AST::HTMLAttributeNameNode.new("HTMLAttributeNameNode", dummy_location, [], [name_literal])
|
173
|
+
|
174
|
+
value_literal = Herb::AST::LiteralNode.new("LiteralNode", dummy_location, [], value.dup)
|
175
|
+
value_node = Herb::AST::HTMLAttributeValueNode.new("HTMLAttributeValueNode", dummy_location, [], create_token(:quote, '"'),
|
176
|
+
[value_literal], create_token(:quote, '"'), true)
|
177
|
+
|
178
|
+
equals_token = create_token(:equals, "=")
|
179
|
+
|
180
|
+
Herb::AST::HTMLAttributeNode.new("HTMLAttributeNode", dummy_location, [], name_node, equals_token, value_node)
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_token(type, value)
|
184
|
+
Herb::Token.new(value.dup, dummy_range, dummy_location, type.to_s)
|
185
|
+
end
|
186
|
+
|
187
|
+
def create_debug_span_for_erb(erb_node)
|
188
|
+
opening = erb_node.tag_opening.value
|
189
|
+
code = erb_node.content.value.strip
|
190
|
+
erb_code = "#{opening} #{code} %>"
|
191
|
+
|
192
|
+
return erb_node if complex_rails_helper?(code)
|
193
|
+
|
194
|
+
line = erb_node.location&.start&.line
|
195
|
+
column = erb_node.location&.start&.column
|
196
|
+
|
197
|
+
escaped_erb = erb_code.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """).gsub("'",
|
198
|
+
"'")
|
199
|
+
|
200
|
+
outline_type = if @top_level_elements.empty?
|
201
|
+
"erb-output #{determine_view_type}"
|
202
|
+
else
|
203
|
+
"erb-output"
|
204
|
+
end
|
205
|
+
|
206
|
+
debug_attributes = [
|
207
|
+
create_debug_attribute("data-herb-debug-outline-type", outline_type),
|
208
|
+
create_debug_attribute("data-herb-debug-erb", escaped_erb),
|
209
|
+
create_debug_attribute("data-herb-debug-file-name", @engine.filename&.basename.to_s),
|
210
|
+
create_debug_attribute("data-herb-debug-file-relative-path", @engine.relative_file_path || ""),
|
211
|
+
create_debug_attribute("data-herb-debug-file-full-path", @engine.filename.to_s),
|
212
|
+
create_debug_attribute("data-herb-debug-inserted", "true")
|
213
|
+
]
|
214
|
+
|
215
|
+
debug_attributes << create_debug_attribute("data-herb-debug-line", line.to_s) if line
|
216
|
+
|
217
|
+
debug_attributes << create_debug_attribute("data-herb-debug-column", (column + 1).to_s) if column
|
218
|
+
|
219
|
+
debug_attributes << create_debug_attribute("style", "display: contents;")
|
220
|
+
|
221
|
+
tag_name_token = create_token(:tag_name, "span")
|
222
|
+
|
223
|
+
open_tag = Herb::AST::HTMLOpenTagNode.new(
|
224
|
+
"HTMLOpenTagNode",
|
225
|
+
dummy_location,
|
226
|
+
[],
|
227
|
+
create_token(:tag_opening, "<"),
|
228
|
+
tag_name_token,
|
229
|
+
create_token(:tag_closing, ">"),
|
230
|
+
debug_attributes,
|
231
|
+
false
|
232
|
+
)
|
233
|
+
|
234
|
+
close_tag = Herb::AST::HTMLCloseTagNode.new(
|
235
|
+
"HTMLCloseTagNode",
|
236
|
+
dummy_location,
|
237
|
+
[],
|
238
|
+
create_token(:tag_opening, "</"),
|
239
|
+
create_token(:tag_name, "span"),
|
240
|
+
[],
|
241
|
+
create_token(:tag_closing, ">")
|
242
|
+
)
|
243
|
+
|
244
|
+
Herb::AST::HTMLElementNode.new("HTMLElementNode", dummy_location, [], open_tag, tag_name_token, [erb_node], close_tag,
|
245
|
+
false, "Debug")
|
246
|
+
end
|
247
|
+
|
248
|
+
def determine_view_type
|
249
|
+
if component?
|
250
|
+
"component"
|
251
|
+
elsif partial?
|
252
|
+
"partial"
|
253
|
+
else
|
254
|
+
"view"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def partial?
|
259
|
+
return false unless @engine.filename
|
260
|
+
|
261
|
+
basename = @engine.filename.basename.to_s
|
262
|
+
basename.start_with?("_")
|
263
|
+
end
|
264
|
+
|
265
|
+
def component?
|
266
|
+
return false unless @engine.filename
|
267
|
+
|
268
|
+
path = @engine.filename.to_s
|
269
|
+
path.include?("/components/")
|
270
|
+
end
|
271
|
+
|
272
|
+
def in_head_context?
|
273
|
+
@element_stack.include?("head")
|
274
|
+
end
|
275
|
+
|
276
|
+
def in_script_or_style_context?
|
277
|
+
["script", "style"].include?(@element_stack.last)
|
278
|
+
end
|
279
|
+
|
280
|
+
def in_excluded_context?
|
281
|
+
excluded_tags = ["script", "style", "head", "textarea", "pre"]
|
282
|
+
excluded_tags.any? { |tag| @element_stack.include?(tag) }
|
283
|
+
end
|
284
|
+
|
285
|
+
def erb_output?(opening)
|
286
|
+
opening.include?("=") && !opening.include?("#")
|
287
|
+
end
|
288
|
+
|
289
|
+
# TODO: Rewrite using Prism Nodes once available
|
290
|
+
def complex_rails_helper?(code)
|
291
|
+
cleaned_code = code.strip.gsub(/\s+/, " ")
|
292
|
+
|
293
|
+
return true if cleaned_code.match?(/\bturbo_frame_tag\s*[(\s]/)
|
294
|
+
|
295
|
+
return true if cleaned_code.match?(/\blink_to\s.*\s+do\s*$/) ||
|
296
|
+
cleaned_code.match?(/\blink_to\s.*\{\s*$/) ||
|
297
|
+
cleaned_code.match?(/\blink_to\s.*\s+do\s*\|/) ||
|
298
|
+
cleaned_code.match?(/\blink_to\s.*\{\s*\|/)
|
299
|
+
|
300
|
+
return true if cleaned_code.match?(/\brender[\s(]/)
|
301
|
+
|
302
|
+
return true if cleaned_code.match?(/\bform_with\s.*\s+do\s*[|$]/) ||
|
303
|
+
cleaned_code.match?(/\bform_with\s.*\{\s*[|$]/)
|
304
|
+
|
305
|
+
return true if cleaned_code.match?(/\bcontent_for\s.*\s+do\s*$/) ||
|
306
|
+
cleaned_code.match?(/\bcontent_for\s.*\{\s*$/)
|
307
|
+
|
308
|
+
return true if cleaned_code.match?(/\bcontent_tag\s.*\s+do\s*$/) ||
|
309
|
+
cleaned_code.match?(/\bcontent_tag\s.*\{\s*$/)
|
310
|
+
|
311
|
+
return true if cleaned_code.match?(/\bcontent_tag\(.*\s+do\s*$/) ||
|
312
|
+
cleaned_code.match?(/\bcontent_tag\(.*\{\s*$/)
|
313
|
+
|
314
|
+
return true if cleaned_code.match?(/\btag\.\w+\s.*do\s*$/) ||
|
315
|
+
cleaned_code.match?(/\btag\.\w+\s.*\{\s*$/)
|
316
|
+
|
317
|
+
false
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|