herb 0.9.2-arm-linux-gnu → 0.9.4-arm-linux-gnu

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/config.yml +125 -0
  4. data/ext/herb/error_helpers.c +172 -2
  5. data/ext/herb/extconf.rb +6 -0
  6. data/ext/herb/extension.c +16 -2
  7. data/ext/herb/extension_helpers.c +6 -5
  8. data/ext/herb/extension_helpers.h +4 -4
  9. data/ext/herb/nodes.c +89 -3
  10. data/lib/herb/3.0/herb.so +0 -0
  11. data/lib/herb/3.1/herb.so +0 -0
  12. data/lib/herb/3.2/herb.so +0 -0
  13. data/lib/herb/3.3/herb.so +0 -0
  14. data/lib/herb/3.4/herb.so +0 -0
  15. data/lib/herb/4.0/herb.so +0 -0
  16. data/lib/herb/ast/erb_content_node.rb +32 -0
  17. data/lib/herb/ast/nodes.rb +244 -3
  18. data/lib/herb/cli.rb +12 -2
  19. data/lib/herb/engine/compiler.rb +166 -75
  20. data/lib/herb/engine/validators/security_validator.rb +40 -0
  21. data/lib/herb/engine.rb +3 -0
  22. data/lib/herb/errors.rb +268 -0
  23. data/lib/herb/parser_options.rb +7 -2
  24. data/lib/herb/project.rb +58 -17
  25. data/lib/herb/version.rb +1 -1
  26. data/lib/herb/visitor.rb +82 -0
  27. data/lib/herb.rb +1 -0
  28. data/sig/herb/ast/erb_content_node.rbs +13 -0
  29. data/sig/herb/ast/nodes.rbs +98 -2
  30. data/sig/herb/engine/compiler.rbs +31 -2
  31. data/sig/herb/engine/validators/security_validator.rbs +4 -0
  32. data/sig/herb/engine.rbs +3 -0
  33. data/sig/herb/errors.rbs +122 -0
  34. data/sig/herb/parser_options.rbs +6 -2
  35. data/sig/herb/visitor.rbs +12 -0
  36. data/sig/serialized_ast_errors.rbs +29 -0
  37. data/sig/serialized_ast_nodes.rbs +19 -0
  38. data/src/analyze/action_view/attribute_extraction_helpers.c +420 -91
  39. data/src/analyze/action_view/image_tag.c +87 -0
  40. data/src/analyze/action_view/javascript_include_tag.c +22 -12
  41. data/src/analyze/action_view/registry.c +6 -3
  42. data/src/analyze/action_view/tag.c +19 -2
  43. data/src/analyze/action_view/tag_helper_node_builders.c +105 -36
  44. data/src/analyze/action_view/tag_helpers.c +792 -44
  45. data/src/analyze/analyze.c +167 -13
  46. data/src/analyze/{helpers.c → analyze_helpers.c} +1 -1
  47. data/src/analyze/analyzed_ruby.c +1 -1
  48. data/src/analyze/builders.c +11 -8
  49. data/src/analyze/conditional_elements.c +6 -7
  50. data/src/analyze/conditional_open_tags.c +6 -7
  51. data/src/analyze/control_type.c +4 -2
  52. data/src/analyze/invalid_structures.c +5 -5
  53. data/src/analyze/missing_end.c +2 -2
  54. data/src/analyze/parse_errors.c +47 -6
  55. data/src/analyze/prism_annotate.c +7 -7
  56. data/src/analyze/render_nodes.c +6 -26
  57. data/src/analyze/strict_locals.c +651 -0
  58. data/src/analyze/transform.c +7 -0
  59. data/src/{ast_node.c → ast/ast_node.c} +8 -8
  60. data/src/{ast_nodes.c → ast/ast_nodes.c} +82 -11
  61. data/src/{ast_pretty_print.c → ast/ast_pretty_print.c} +113 -9
  62. data/src/{pretty_print.c → ast/pretty_print.c} +9 -9
  63. data/src/errors.c +398 -8
  64. data/src/extract.c +5 -5
  65. data/src/herb.c +15 -5
  66. data/src/include/analyze/action_view/attribute_extraction_helpers.h +3 -1
  67. data/src/include/analyze/action_view/tag_helper_handler.h +3 -3
  68. data/src/include/analyze/action_view/tag_helper_node_builders.h +34 -5
  69. data/src/include/analyze/action_view/tag_helpers.h +4 -3
  70. data/src/include/analyze/analyze.h +12 -5
  71. data/src/include/analyze/analyzed_ruby.h +2 -2
  72. data/src/include/analyze/builders.h +4 -4
  73. data/src/include/analyze/conditional_elements.h +2 -2
  74. data/src/include/analyze/conditional_open_tags.h +2 -2
  75. data/src/include/analyze/control_type.h +1 -1
  76. data/src/include/analyze/helpers.h +2 -2
  77. data/src/include/analyze/invalid_structures.h +1 -1
  78. data/src/include/analyze/prism_annotate.h +2 -2
  79. data/src/include/analyze/render_nodes.h +1 -1
  80. data/src/include/analyze/strict_locals.h +11 -0
  81. data/src/include/{ast_node.h → ast/ast_node.h} +4 -4
  82. data/src/include/{ast_nodes.h → ast/ast_nodes.h} +38 -14
  83. data/src/include/{ast_pretty_print.h → ast/ast_pretty_print.h} +3 -3
  84. data/src/include/{pretty_print.h → ast/pretty_print.h} +4 -4
  85. data/src/include/errors.h +65 -7
  86. data/src/include/extract.h +2 -2
  87. data/src/include/herb.h +5 -5
  88. data/src/include/{lex_helpers.h → lexer/lex_helpers.h} +5 -5
  89. data/src/include/{lexer.h → lexer/lexer.h} +1 -1
  90. data/src/include/{lexer_peek_helpers.h → lexer/lexer_peek_helpers.h} +2 -2
  91. data/src/include/{lexer_struct.h → lexer/lexer_struct.h} +2 -2
  92. data/src/include/{token.h → lexer/token.h} +3 -3
  93. data/src/include/{token_matchers.h → lexer/token_matchers.h} +1 -1
  94. data/src/include/{token_struct.h → lexer/token_struct.h} +3 -3
  95. data/src/include/{util → lib}/hb_foreach.h +1 -1
  96. data/src/include/{util → lib}/hb_string.h +5 -1
  97. data/src/include/{location.h → location/location.h} +1 -1
  98. data/src/include/parser/dot_notation.h +12 -0
  99. data/src/include/{parser.h → parser/parser.h} +11 -4
  100. data/src/include/{parser_helpers.h → parser/parser_helpers.h} +6 -6
  101. data/src/include/{prism_context.h → prism/prism_context.h} +2 -2
  102. data/src/include/{prism_helpers.h → prism/prism_helpers.h} +6 -6
  103. data/src/include/{html_util.h → util/html_util.h} +1 -1
  104. data/src/include/util/ruby_util.h +9 -0
  105. data/src/include/{utf8.h → util/utf8.h} +1 -1
  106. data/src/include/{util.h → util/util.h} +1 -1
  107. data/src/include/version.h +1 -1
  108. data/src/include/visitor.h +3 -3
  109. data/src/{lexer_peek_helpers.c → lexer/lexer_peek_helpers.c} +3 -3
  110. data/src/{token.c → lexer/token.c} +8 -8
  111. data/src/{token_matchers.c → lexer/token_matchers.c} +2 -2
  112. data/src/lexer.c +6 -6
  113. data/src/{util → lib}/hb_allocator.c +2 -2
  114. data/src/{util → lib}/hb_arena.c +1 -1
  115. data/src/{util → lib}/hb_arena_debug.c +2 -2
  116. data/src/{util → lib}/hb_array.c +2 -2
  117. data/src/{util → lib}/hb_buffer.c +2 -2
  118. data/src/{util → lib}/hb_narray.c +1 -1
  119. data/src/{util → lib}/hb_string.c +2 -2
  120. data/src/{location.c → location/location.c} +2 -2
  121. data/src/{position.c → location/position.c} +2 -2
  122. data/src/{range.c → location/range.c} +1 -1
  123. data/src/main.c +11 -11
  124. data/src/parser/dot_notation.c +100 -0
  125. data/src/{parser_match_tags.c → parser/match_tags.c} +34 -5
  126. data/src/{parser_helpers.c → parser/parser_helpers.c} +10 -10
  127. data/src/parser.c +68 -32
  128. data/src/{prism_helpers.c → prism/prism_helpers.c} +7 -7
  129. data/src/{ruby_parser.c → prism/ruby_parser.c} +1 -1
  130. data/src/{html_util.c → util/html_util.c} +4 -4
  131. data/src/{io.c → util/io.c} +3 -3
  132. data/src/util/ruby_util.c +42 -0
  133. data/src/{utf8.c → util/utf8.c} +2 -2
  134. data/src/{util.c → util/util.c} +4 -4
  135. data/src/visitor.c +35 -3
  136. data/templates/ext/herb/error_helpers.c.erb +2 -2
  137. data/templates/ext/herb/nodes.c.erb +1 -1
  138. data/templates/java/error_helpers.c.erb +1 -1
  139. data/templates/java/error_helpers.h.erb +2 -2
  140. data/templates/java/nodes.c.erb +4 -4
  141. data/templates/java/nodes.h.erb +1 -1
  142. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +4 -4
  143. data/templates/javascript/packages/node/extension/nodes.cpp.erb +4 -4
  144. data/templates/lib/herb/visitor.rb.erb +14 -0
  145. data/templates/src/analyze/missing_end.c.erb +2 -2
  146. data/templates/src/{ast_nodes.c.erb → ast/ast_nodes.c.erb} +9 -9
  147. data/templates/src/{ast_pretty_print.c.erb → ast/ast_pretty_print.c.erb} +8 -8
  148. data/templates/src/errors.c.erb +8 -8
  149. data/templates/src/include/{ast_nodes.h.erb → ast/ast_nodes.h.erb} +11 -12
  150. data/templates/src/include/{ast_pretty_print.h.erb → ast/ast_pretty_print.h.erb} +2 -2
  151. data/templates/src/include/errors.h.erb +7 -7
  152. data/templates/src/{parser_match_tags.c.erb → parser/match_tags.c.erb} +4 -4
  153. data/templates/src/visitor.c.erb +3 -3
  154. data/templates/wasm/error_helpers.cpp.erb +4 -4
  155. data/templates/wasm/nodes.cpp.erb +5 -5
  156. metadata +76 -68
  157. data/src/include/element_source.h +0 -10
  158. /data/src/include/{util → lib}/hb_allocator.h +0 -0
  159. /data/src/include/{util → lib}/hb_arena.h +0 -0
  160. /data/src/include/{util → lib}/hb_arena_debug.h +0 -0
  161. /data/src/include/{util → lib}/hb_array.h +0 -0
  162. /data/src/include/{util → lib}/hb_buffer.h +0 -0
  163. /data/src/include/{util → lib}/hb_narray.h +0 -0
  164. /data/src/include/{util → lib}/string.h +0 -0
  165. /data/src/include/{position.h → location/position.h} +0 -0
  166. /data/src/include/{range.h → location/range.h} +0 -0
  167. /data/src/include/{herb_prism_node.h → prism/herb_prism_node.h} +0 -0
  168. /data/src/include/{prism_serialized.h → prism/prism_serialized.h} +0 -0
  169. /data/src/include/{ruby_parser.h → prism/ruby_parser.h} +0 -0
  170. /data/src/include/{io.h → util/io.h} +0 -0
  171. /data/templates/src/include/{util → lib}/hb_foreach.h.erb +0 -0
@@ -3,6 +3,14 @@
3
3
  module Herb
4
4
  class Engine
5
5
  class Compiler < ::Herb::Visitor
6
+ EXPRESSION_TOKEN_TYPES = [:expr, :expr_escaped, :expr_block, :expr_block_escaped].freeze
7
+
8
+ TRAILING_WHITESPACE = /[ \t]+\z/
9
+ TRAILING_INDENTATION = /\n[ \t]+\z/
10
+ TRAILING_INDENTATION_CAPTURE = /\n([ \t]+)\z/
11
+ WHITESPACE_ONLY = /\A[ \t]+\z/
12
+ WHITESPACE_ONLY_CAPTURE = /\A([ \t]+)\z/
13
+
6
14
  attr_reader :tokens
7
15
 
8
16
  def initialize(engine, options = {})
@@ -14,6 +22,9 @@ module Herb
14
22
  @element_stack = [] #: Array[String]
15
23
  @context_stack = [:html_content]
16
24
  @trim_next_whitespace = false
25
+ @last_trim_consumed_newline = false
26
+ @pending_leading_whitespace = nil
27
+ @pending_leading_whitespace_insert_index = 0
17
28
  end
18
29
 
19
30
  def generate_output
@@ -46,43 +57,19 @@ module Herb
46
57
  end
47
58
 
48
59
  def visit_html_element_node(node)
49
- tag_name = node.tag_name&.value&.downcase
50
-
51
- @element_stack.push(tag_name) if tag_name
52
-
53
- if tag_name == "script"
54
- push_context(:script_content)
55
- elsif tag_name == "style"
56
- push_context(:style_content)
60
+ with_element_context(node) do
61
+ visit(node.open_tag)
62
+ visit_all(node.body)
63
+ visit(node.close_tag)
57
64
  end
58
-
59
- visit(node.open_tag)
60
- visit_all(node.body)
61
- visit(node.close_tag)
62
-
63
- pop_context if ["script", "style"].include?(tag_name)
64
-
65
- @element_stack.pop if tag_name
66
65
  end
67
66
 
68
67
  def visit_html_conditional_element_node(node)
69
- tag_name = node.tag_name&.value&.downcase
70
-
71
- @element_stack.push(tag_name) if tag_name
72
-
73
- if tag_name == "script"
74
- push_context(:script_content)
75
- elsif tag_name == "style"
76
- push_context(:style_content)
68
+ with_element_context(node) do
69
+ visit(node.open_conditional)
70
+ visit_all(node.body)
71
+ visit(node.close_conditional)
77
72
  end
78
-
79
- visit(node.open_conditional)
80
- visit_all(node.body)
81
- visit(node.close_conditional)
82
-
83
- pop_context if ["script", "style"].include?(tag_name)
84
-
85
- @element_stack.pop if tag_name
86
73
  end
87
74
 
88
75
  def visit_html_open_tag_node(node)
@@ -261,6 +248,8 @@ module Herb
261
248
  def visit_erb_block_node(node)
262
249
  opening = node.tag_opening.value
263
250
 
251
+ check_for_escaped_erb_tag!(opening)
252
+
264
253
  if opening.include?("=")
265
254
  should_escape = should_escape_output?(opening)
266
255
  code = node.content.value.strip
@@ -271,28 +260,32 @@ module Herb
271
260
  [:expr_block, code, current_context]
272
261
  end
273
262
 
263
+ @last_trim_consumed_newline = false
264
+ @trim_next_whitespace = true if right_trim?(node)
265
+
274
266
  visit_all(node.body)
275
267
  visit_erb_block_end_node(node.end_node, escaped: should_escape)
276
268
  else
277
269
  visit_erb_control_node(node) do
278
270
  visit_all(node.body)
271
+ visit(node.rescue_clause)
272
+ visit(node.else_clause)
273
+ visit(node.ensure_clause)
279
274
  visit(node.end_node)
280
275
  end
281
276
  end
282
277
  end
283
278
 
284
279
  def visit_erb_block_end_node(node, escaped: false)
285
- has_left_trim = node.tag_opening.value.start_with?("<%-")
286
-
287
- remove_trailing_whitespace_from_last_token! if has_left_trim
280
+ remove_trailing_whitespace_from_last_token! if left_trim?(node)
288
281
 
289
282
  code = node.content.value.strip
290
283
 
291
284
  if at_line_start?
292
- lspace = extract_and_remove_lspace!
293
- rspace = " \n"
285
+ leading_space = extract_and_remove_leading_space!
286
+ right_space = " \n"
294
287
 
295
- @tokens << [:expr_block_end, "#{lspace}#{code}#{rspace}", current_context, escaped]
288
+ @tokens << [:expr_block_end, "#{leading_space}#{code}#{right_space}", current_context, escaped]
296
289
  @trim_next_whitespace = true
297
290
  else
298
291
  @tokens << [:expr_block_end, code, current_context, escaped]
@@ -317,6 +310,15 @@ module Herb
317
310
 
318
311
  private
319
312
 
313
+ def check_for_escaped_erb_tag!(opening)
314
+ return unless opening.start_with?("<%%")
315
+
316
+ raise Herb::Engine::GeneratorTemplateError,
317
+ "This file appears to be a generator template (a template used to generate ERB files) " \
318
+ "rather than a standard ERB template. It contains escaped ERB tags like <%%= %> which " \
319
+ "produce literal ERB output in the generated file."
320
+ end
321
+
320
322
  def current_context
321
323
  @context_stack.last
322
324
  end
@@ -329,16 +331,38 @@ module Herb
329
331
  @context_stack.pop
330
332
  end
331
333
 
334
+ #: (untyped node) { () -> untyped } -> untyped
335
+ def with_element_context(node)
336
+ tag_name = node.tag_name&.value&.downcase
337
+
338
+ @element_stack.push(tag_name) if tag_name
339
+
340
+ if tag_name == "script"
341
+ push_context(:script_content)
342
+ elsif tag_name == "style"
343
+ push_context(:style_content)
344
+ end
345
+
346
+ yield
347
+
348
+ pop_context if ["script", "style"].include?(tag_name)
349
+
350
+ @element_stack.pop if tag_name
351
+ end
352
+
332
353
  def process_erb_tag(node, skip_comment_check: false)
333
354
  opening = node.tag_opening.value
334
355
 
356
+ check_for_escaped_erb_tag!(opening)
357
+
335
358
  if !skip_comment_check && erb_comment?(opening)
336
- has_left_trim = opening.start_with?("<%-")
337
- remove_trailing_whitespace_from_last_token! if has_left_trim
359
+ follows_newline = leading_space_follows_newline?
360
+ remove_trailing_whitespace_from_last_token! if left_trim?(node)
338
361
 
339
362
  if at_line_start?
340
- extract_and_remove_lspace!
363
+ leading_space = extract_and_remove_leading_space!
341
364
  @trim_next_whitespace = true
365
+ save_pending_leading_whitespace!(leading_space) if !leading_space.empty? && follows_newline
342
366
  end
343
367
  return
344
368
  end
@@ -357,10 +381,17 @@ module Herb
357
381
  return if text.empty?
358
382
 
359
383
  if @trim_next_whitespace
384
+ @last_trim_consumed_newline = text.match?(/\A[ \t]*\r?\n/)
360
385
  text = text.sub(/\A[ \t]*\r?\n/, "")
361
386
  @trim_next_whitespace = false
387
+
388
+ restore_pending_leading_whitespace! unless @last_trim_consumed_newline
389
+ else
390
+ @last_trim_consumed_newline = false
362
391
  end
363
392
 
393
+ @pending_leading_whitespace = nil
394
+
364
395
  return if text.empty?
365
396
 
366
397
  @tokens << [:text, text, current_context]
@@ -376,10 +407,12 @@ module Herb
376
407
 
377
408
  def add_expression(code)
378
409
  @tokens << [:expr, code, current_context]
410
+ @last_trim_consumed_newline = false
379
411
  end
380
412
 
381
413
  def add_expression_escaped(code)
382
414
  @tokens << [:expr_escaped, code, current_context]
415
+ @last_trim_consumed_newline = false
383
416
  end
384
417
 
385
418
  def optimize_tokens(tokens)
@@ -463,10 +496,16 @@ module Herb
463
496
  end
464
497
 
465
498
  def process_erb_output(node, opening, code)
466
- has_right_trim = node.tag_closing&.value == "-%>"
499
+ if @trim_next_whitespace && @pending_leading_whitespace
500
+ restore_pending_leading_whitespace!
501
+ @pending_leading_whitespace = nil
502
+ @trim_next_whitespace = false
503
+ @last_trim_consumed_newline = false
504
+ end
505
+
467
506
  should_escape = should_escape_output?(opening)
468
507
  add_expression_with_escaping(code, should_escape)
469
- @trim_next_whitespace = true if has_right_trim
508
+ @trim_next_whitespace = true if right_trim?(node)
470
509
  end
471
510
 
472
511
  def indicator_for(type)
@@ -493,78 +532,130 @@ module Herb
493
532
  end
494
533
 
495
534
  def at_line_start?
496
- @tokens.empty? ||
497
- @tokens.last[0] != :text ||
498
- @tokens.last[1].empty? ||
499
- @tokens.last[1].end_with?("\n") ||
500
- (@tokens.last[1] =~ /\A[ \t]+\z/ && preceding_token_ends_with_newline?) ||
501
- @tokens.last[1] =~ /\n[ \t]+\z/
535
+ return true if @tokens.empty?
536
+
537
+ last_type = @tokens.last[0]
538
+ last_value = @tokens.last[1]
539
+
540
+ if last_type == :text
541
+ last_value.empty? || last_value.end_with?("\n") || (last_value =~ WHITESPACE_ONLY && preceding_token_ends_with_newline?) || last_value =~ TRAILING_INDENTATION
542
+ elsif EXPRESSION_TOKEN_TYPES.include?(last_type)
543
+ @last_trim_consumed_newline
544
+ else
545
+ last_value.end_with?("\n")
546
+ end
502
547
  end
503
548
 
504
549
  def preceding_token_ends_with_newline?
505
550
  return true unless @tokens.length >= 2
506
551
 
507
552
  preceding = @tokens[-2]
508
- return false if [:expr, :expr_escaped, :expr_block, :expr_block_escaped, :expr_block_end].include?(preceding[0])
553
+ return @last_trim_consumed_newline if EXPRESSION_TOKEN_TYPES.include?(preceding[0])
554
+ return preceding[1].end_with?("\n") if preceding[0] == :expr_block_end
509
555
  return true unless preceding[0] == :text
510
556
 
511
557
  preceding[1].end_with?("\n")
512
558
  end
513
559
 
514
- def extract_lspace
515
- return "" unless @tokens.last && @tokens.last[0] == :text
560
+ def left_trim?(node)
561
+ node.tag_opening.value == "<%-"
562
+ end
516
563
 
517
- text = @tokens.last[1]
564
+ def right_trim?(node)
565
+ node.tag_closing&.value == "-%>"
566
+ end
567
+
568
+ def last_text_token
569
+ return unless @tokens.last && @tokens.last[0] == :text
570
+
571
+ @tokens.last
572
+ end
518
573
 
519
- return Regexp.last_match(1) if text =~ /\n([ \t]+)\z/ || text =~ /\A([ \t]+)\z/
574
+ def extract_leading_space
575
+ token = last_text_token
576
+ return "" unless token
577
+
578
+ text = token[1]
579
+
580
+ return Regexp.last_match(1) if text =~ TRAILING_INDENTATION_CAPTURE || text =~ WHITESPACE_ONLY_CAPTURE
520
581
 
521
582
  ""
522
583
  end
523
584
 
524
- def extract_and_remove_lspace!
525
- lspace = extract_lspace
526
- return lspace if lspace.empty?
585
+ def leading_space_follows_newline?
586
+ token = last_text_token
587
+ return false unless token
588
+
589
+ text = token[1]
590
+
591
+ return true if text.match?(TRAILING_INDENTATION)
592
+ return true if @last_trim_consumed_newline && text.match?(WHITESPACE_ONLY)
593
+
594
+ false
595
+ end
596
+
597
+ def extract_and_remove_leading_space!
598
+ leading_space = extract_leading_space
599
+ return leading_space if leading_space.empty?
527
600
 
528
601
  text = @tokens.last[1]
529
- if text =~ /\n[ \t]+\z/
530
- text.sub!(/[ \t]+\z/, "")
531
- elsif text =~ /\A[ \t]+\z/
602
+
603
+ if text =~ TRAILING_INDENTATION
604
+ text.sub!(TRAILING_WHITESPACE, "")
605
+ elsif text =~ WHITESPACE_ONLY
532
606
  text.replace("")
533
607
  end
608
+
534
609
  @tokens.last[1] = text
535
610
 
536
- lspace
611
+ leading_space
537
612
  end
538
613
 
539
614
  def apply_trim(node, code)
540
- has_left_trim = node.tag_opening.value.start_with?("<%-")
541
- node.tag_closing&.value
542
-
543
- remove_trailing_whitespace_from_last_token! if has_left_trim
615
+ follows_newline = leading_space_follows_newline?
616
+ removed_whitespace = left_trim?(node) ? remove_trailing_whitespace_from_last_token! : ""
544
617
 
545
618
  if at_line_start?
546
- lspace = extract_and_remove_lspace!
547
- rspace = Herb::Engine.heredoc?(code) ? "\n" : " \n"
619
+ leading_space = extract_and_remove_leading_space!
620
+ effective_leading_space = leading_space.empty? ? removed_whitespace : leading_space
621
+ right_space = Herb::Engine.heredoc?(code) ? "\n" : " \n"
548
622
 
549
- @tokens << [:code, "#{lspace}#{code}#{rspace}", current_context]
623
+ @pending_leading_whitespace_insert_index = @tokens.length
624
+ @pending_leading_whitespace = effective_leading_space if !effective_leading_space.empty? && follows_newline
625
+ @tokens << [:code, "#{effective_leading_space}#{code}#{right_space}", current_context]
550
626
  @trim_next_whitespace = true
551
627
  else
552
628
  @tokens << [:code, code, current_context]
553
629
  end
554
630
  end
555
631
 
632
+ def save_pending_leading_whitespace!(whitespace)
633
+ @pending_leading_whitespace = whitespace
634
+ @pending_leading_whitespace_insert_index = @tokens.length
635
+ end
636
+
637
+ def restore_pending_leading_whitespace!
638
+ return unless @pending_leading_whitespace
639
+
640
+ @tokens.insert(@pending_leading_whitespace_insert_index, [:text, @pending_leading_whitespace, current_context])
641
+ end
642
+
556
643
  def remove_trailing_whitespace_from_last_token!
557
- return unless @tokens.last && @tokens.last[0] == :text
644
+ token = last_text_token
645
+ return "" unless token
558
646
 
559
- text = @tokens.last[1]
647
+ text = token[1]
648
+ removed = text[TRAILING_WHITESPACE] || ""
560
649
 
561
- if text =~ /\n[ \t]+\z/
562
- text.sub!(/[ \t]+\z/, "")
563
- @tokens.last[1] = text
564
- elsif text =~ /\A[ \t]+\z/
650
+ if text =~ TRAILING_INDENTATION
651
+ text.sub!(TRAILING_WHITESPACE, "")
652
+ token[1] = text
653
+ elsif text =~ WHITESPACE_ONLY
565
654
  text.replace("")
566
- @tokens.last[1] = text
655
+ token[1] = text
567
656
  end
657
+
658
+ removed
568
659
  end
569
660
  end
570
661
  end
@@ -28,6 +28,20 @@ module Herb
28
28
 
29
29
  next unless child.is_a?(Herb::AST::ERBContentNode) && erb_outputs?(child)
30
30
 
31
+ prism_node = child.prism
32
+
33
+ next if prism_node && tag_attributes_prism_node?(prism_node)
34
+
35
+ if prism_node && conditional_tag_attributes_prism_node?(prism_node)
36
+ add_security_error(
37
+ child.location,
38
+ "Avoid using conditional `tag.attributes` in attribute position.",
39
+ "Use `<% if ... %><%= tag.attributes(...) %><% end %>` instead."
40
+ )
41
+
42
+ next
43
+ end
44
+
31
45
  add_security_error(
32
46
  child.location,
33
47
  "ERB output tags (<%= %>) are not allowed in attribute position.",
@@ -48,6 +62,32 @@ module Herb
48
62
  end
49
63
  end
50
64
 
65
+ def tag_attributes_prism_node?(prism_node)
66
+ return false unless prism_node.is_a?(Prism::CallNode)
67
+ return false unless prism_node.name == :attributes
68
+
69
+ receiver = prism_node.receiver
70
+ return false unless receiver.is_a?(Prism::CallNode)
71
+
72
+ receiver.name == :tag
73
+ end
74
+
75
+ def conditional_tag_attributes_prism_node?(prism_node)
76
+ if prism_node.is_a?(Prism::IfNode) || prism_node.is_a?(Prism::UnlessNode)
77
+ body = prism_node.statements&.body
78
+ return false unless body
79
+ return false unless body.length == 1
80
+
81
+ return tag_attributes_prism_node?(body.first)
82
+ end
83
+
84
+ if prism_node.is_a?(Prism::AndNode) || prism_node.is_a?(Prism::OrNode)
85
+ return tag_attributes_prism_node?(prism_node.right)
86
+ end
87
+
88
+ false
89
+ end
90
+
51
91
  def add_security_error(location, message, suggestion)
52
92
  add_diagnostic(message, location, :error, code: "SecurityViolation", source: "SecurityValidator",
53
93
  suggestion: suggestion)
data/lib/herb/engine.rb CHANGED
@@ -31,6 +31,9 @@ module Herb
31
31
  class CompilationError < StandardError
32
32
  end
33
33
 
34
+ class GeneratorTemplateError < CompilationError
35
+ end
36
+
34
37
  class InvalidRubyError < CompilationError
35
38
  attr_reader :compiled_source
36
39