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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/ext/herb/nodes.c +6 -4
  4. data/lib/herb/3.0/herb.so +0 -0
  5. data/lib/herb/3.1/herb.so +0 -0
  6. data/lib/herb/3.2/herb.so +0 -0
  7. data/lib/herb/3.3/herb.so +0 -0
  8. data/lib/herb/3.4/herb.so +0 -0
  9. data/lib/herb/ast/helpers.rb +26 -0
  10. data/lib/herb/ast/nodes.rb +7 -3
  11. data/lib/herb/cli.rb +158 -1
  12. data/lib/herb/engine/compiler.rb +399 -0
  13. data/lib/herb/engine/debug_visitor.rb +321 -0
  14. data/lib/herb/engine/error_formatter.rb +420 -0
  15. data/lib/herb/engine/parser_error_overlay.rb +767 -0
  16. data/lib/herb/engine/validation_error_overlay.rb +182 -0
  17. data/lib/herb/engine/validation_errors.rb +65 -0
  18. data/lib/herb/engine/validator.rb +75 -0
  19. data/lib/herb/engine/validators/accessibility_validator.rb +31 -0
  20. data/lib/herb/engine/validators/nesting_validator.rb +95 -0
  21. data/lib/herb/engine/validators/security_validator.rb +71 -0
  22. data/lib/herb/engine.rb +366 -0
  23. data/lib/herb/project.rb +3 -3
  24. data/lib/herb/version.rb +1 -1
  25. data/lib/herb/visitor.rb +2 -0
  26. data/lib/herb.rb +2 -0
  27. data/sig/herb/ast/helpers.rbs +16 -0
  28. data/sig/herb/ast/nodes.rbs +4 -2
  29. data/sig/herb/engine/compiler.rbs +109 -0
  30. data/sig/herb/engine/debug.rbs +38 -0
  31. data/sig/herb/engine/debug_visitor.rbs +70 -0
  32. data/sig/herb/engine/error_formatter.rbs +47 -0
  33. data/sig/herb/engine/parser_error_overlay.rbs +41 -0
  34. data/sig/herb/engine/validation_error_overlay.rbs +35 -0
  35. data/sig/herb/engine/validation_errors.rbs +45 -0
  36. data/sig/herb/engine/validator.rbs +37 -0
  37. data/sig/herb/engine/validators/accessibility_validator.rbs +19 -0
  38. data/sig/herb/engine/validators/nesting_validator.rbs +25 -0
  39. data/sig/herb/engine/validators/security_validator.rbs +23 -0
  40. data/sig/herb/engine.rbs +72 -0
  41. data/sig/herb/visitor.rbs +2 -0
  42. data/sig/herb_c_extension.rbs +7 -0
  43. data/sig/serialized_ast_nodes.rbs +1 -0
  44. data/src/ast_nodes.c +2 -1
  45. data/src/ast_pretty_print.c +2 -1
  46. data/src/element_source.c +11 -0
  47. data/src/include/ast_nodes.h +3 -1
  48. data/src/include/element_source.h +13 -0
  49. data/src/include/version.h +1 -1
  50. data/src/parser.c +3 -0
  51. data/src/parser_helpers.c +1 -0
  52. 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("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;").gsub("'",
198
+ "&#39;")
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