herb 0.9.1-aarch64-linux-gnu → 0.9.3-aarch64-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 +136 -97
  20. data/lib/herb/engine/validators/security_validator.rb +40 -0
  21. data/lib/herb/engine.rb +21 -0
  22. data/lib/herb/errors.rb +268 -0
  23. data/lib/herb/parser_options.rb +7 -2
  24. data/lib/herb/version.rb +1 -1
  25. data/lib/herb/visitor.rb +82 -0
  26. data/lib/herb.rb +1 -0
  27. data/sig/herb/ast/erb_content_node.rbs +13 -0
  28. data/sig/herb/ast/nodes.rbs +98 -2
  29. data/sig/herb/engine/compiler.rbs +18 -3
  30. data/sig/herb/engine/validators/security_validator.rbs +4 -0
  31. data/sig/herb/engine.rbs +4 -0
  32. data/sig/herb/errors.rbs +122 -0
  33. data/sig/herb/parser_options.rbs +6 -2
  34. data/sig/herb/visitor.rbs +12 -0
  35. data/sig/serialized_ast_errors.rbs +29 -0
  36. data/sig/serialized_ast_nodes.rbs +19 -0
  37. data/src/analyze/action_view/attribute_extraction_helpers.c +425 -87
  38. data/src/analyze/action_view/image_tag.c +87 -0
  39. data/src/analyze/action_view/javascript_include_tag.c +102 -0
  40. data/src/analyze/action_view/javascript_tag.c +55 -0
  41. data/src/analyze/action_view/registry.c +10 -3
  42. data/src/analyze/action_view/tag.c +19 -2
  43. data/src/analyze/action_view/tag_helper_node_builders.c +119 -37
  44. data/src/analyze/action_view/tag_helpers.c +1033 -32
  45. data/src/analyze/analyze.c +165 -10
  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 +5 -5
  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 +637 -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 +6 -4
  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} +2 -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 +4 -8
  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} +54 -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 +78 -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,8 @@
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
+
6
8
  attr_reader :tokens
7
9
 
8
10
  def initialize(engine, options = {})
@@ -10,13 +12,13 @@ module Herb
10
12
 
11
13
  @engine = engine
12
14
  @escape = options.fetch(:escape) { options.fetch(:escape_html, false) }
13
- @attrfunc = options.fetch(:attrfunc, @escape ? "__herb.attr" : "::Herb::Engine.attr")
14
- @jsfunc = options.fetch(:jsfunc, @escape ? "__herb.js" : "::Herb::Engine.js")
15
- @cssfunc = options.fetch(:cssfunc, @escape ? "__herb.css" : "::Herb::Engine.css")
16
15
  @tokens = [] #: Array[untyped]
17
16
  @element_stack = [] #: Array[String]
18
17
  @context_stack = [:html_content]
19
18
  @trim_next_whitespace = false
19
+ @last_trim_consumed_newline = false
20
+ @pending_leading_whitespace = nil
21
+ @pending_leading_whitespace_insert_index = 0
20
22
  end
21
23
 
22
24
  def generate_output
@@ -28,26 +30,16 @@ module Herb
28
30
  @engine.send(:add_text, value)
29
31
  when :code
30
32
  @engine.send(:add_code, value)
31
- when :expr
32
- if [:attribute_value, :script_content, :style_content].include?(context)
33
- add_context_aware_expression(value, context)
34
- else
35
- indicator = @escape ? "==" : "="
36
- @engine.send(:add_expression, indicator, value)
37
- end
38
- when :expr_escaped
39
- if [:attribute_value, :script_content, :style_content].include?(context)
40
- add_context_aware_expression(value, context)
33
+ when :expr, :expr_escaped
34
+ indicator = indicator_for(type)
35
+
36
+ if context_aware_context?(context)
37
+ @engine.send(:add_context_aware_expression, indicator, value, context)
41
38
  else
42
- indicator = @escape ? "=" : "=="
43
39
  @engine.send(:add_expression, indicator, value)
44
40
  end
45
- when :expr_block
46
- indicator = @escape ? "==" : "="
47
- @engine.send(:add_expression_block, indicator, value)
48
- when :expr_block_escaped
49
- indicator = @escape ? "=" : "=="
50
- @engine.send(:add_expression_block, indicator, value)
41
+ when :expr_block, :expr_block_escaped
42
+ @engine.send(:add_expression_block, indicator_for(type), value)
51
43
  when :expr_block_end
52
44
  @engine.send(:add_expression_block_end, value, escaped: escaped)
53
45
  end
@@ -59,43 +51,19 @@ module Herb
59
51
  end
60
52
 
61
53
  def visit_html_element_node(node)
62
- tag_name = node.tag_name&.value&.downcase
63
-
64
- @element_stack.push(tag_name) if tag_name
65
-
66
- if tag_name == "script"
67
- push_context(:script_content)
68
- elsif tag_name == "style"
69
- push_context(:style_content)
54
+ with_element_context(node) do
55
+ visit(node.open_tag)
56
+ visit_all(node.body)
57
+ visit(node.close_tag)
70
58
  end
71
-
72
- visit(node.open_tag)
73
- visit_all(node.body)
74
- visit(node.close_tag)
75
-
76
- pop_context if ["script", "style"].include?(tag_name)
77
-
78
- @element_stack.pop if tag_name
79
59
  end
80
60
 
81
61
  def visit_html_conditional_element_node(node)
82
- tag_name = node.tag_name&.value&.downcase
83
-
84
- @element_stack.push(tag_name) if tag_name
85
-
86
- if tag_name == "script"
87
- push_context(:script_content)
88
- elsif tag_name == "style"
89
- push_context(:style_content)
62
+ with_element_context(node) do
63
+ visit(node.open_conditional)
64
+ visit_all(node.body)
65
+ visit(node.close_conditional)
90
66
  end
91
-
92
- visit(node.open_conditional)
93
- visit_all(node.body)
94
- visit(node.close_conditional)
95
-
96
- pop_context if ["script", "style"].include?(tag_name)
97
-
98
- @element_stack.pop if tag_name
99
67
  end
100
68
 
101
69
  def visit_html_open_tag_node(node)
@@ -283,12 +251,16 @@ module Herb
283
251
  else
284
252
  [:expr_block, code, current_context]
285
253
  end
254
+ @last_trim_consumed_newline = false
286
255
 
287
256
  visit_all(node.body)
288
257
  visit_erb_block_end_node(node.end_node, escaped: should_escape)
289
258
  else
290
259
  visit_erb_control_node(node) do
291
260
  visit_all(node.body)
261
+ visit(node.rescue_clause)
262
+ visit(node.else_clause)
263
+ visit(node.ensure_clause)
292
264
  visit(node.end_node)
293
265
  end
294
266
  end
@@ -302,10 +274,10 @@ module Herb
302
274
  code = node.content.value.strip
303
275
 
304
276
  if at_line_start?
305
- lspace = extract_and_remove_lspace!
306
- rspace = " \n"
277
+ leading_space = extract_and_remove_leading_space!
278
+ right_space = " \n"
307
279
 
308
- @tokens << [:expr_block_end, "#{lspace}#{code}#{rspace}", current_context, escaped]
280
+ @tokens << [:expr_block_end, "#{leading_space}#{code}#{right_space}", current_context, escaped]
309
281
  @trim_next_whitespace = true
310
282
  else
311
283
  @tokens << [:expr_block_end, code, current_context, escaped]
@@ -342,25 +314,23 @@ module Herb
342
314
  @context_stack.pop
343
315
  end
344
316
 
345
- def add_context_aware_expression(code, context)
346
- closing = code.include?("#") ? "\n))" : "))"
347
-
348
- case context
349
- when :attribute_value
350
- @engine.send(:with_buffer) {
351
- @engine.src << " << #{@attrfunc}((" << code << closing
352
- }
353
- when :script_content
354
- @engine.send(:with_buffer) {
355
- @engine.src << " << #{@jsfunc}((" << code << closing
356
- }
357
- when :style_content
358
- @engine.send(:with_buffer) {
359
- @engine.src << " << #{@cssfunc}((" << code << closing
360
- }
361
- else
362
- @engine.send(:add_expression_result_escaped, code)
317
+ #: (untyped node) { () -> untyped } -> untyped
318
+ def with_element_context(node)
319
+ tag_name = node.tag_name&.value&.downcase
320
+
321
+ @element_stack.push(tag_name) if tag_name
322
+
323
+ if tag_name == "script"
324
+ push_context(:script_content)
325
+ elsif tag_name == "style"
326
+ push_context(:style_content)
363
327
  end
328
+
329
+ yield
330
+
331
+ pop_context if ["script", "style"].include?(tag_name)
332
+
333
+ @element_stack.pop if tag_name
364
334
  end
365
335
 
366
336
  def process_erb_tag(node, skip_comment_check: false)
@@ -368,11 +338,13 @@ module Herb
368
338
 
369
339
  if !skip_comment_check && erb_comment?(opening)
370
340
  has_left_trim = opening.start_with?("<%-")
341
+ follows_newline = leading_space_follows_newline?
371
342
  remove_trailing_whitespace_from_last_token! if has_left_trim
372
343
 
373
344
  if at_line_start?
374
- extract_and_remove_lspace!
345
+ leading_space = extract_and_remove_leading_space!
375
346
  @trim_next_whitespace = true
347
+ save_pending_leading_whitespace!(leading_space) if !leading_space.empty? && follows_newline
376
348
  end
377
349
  return
378
350
  end
@@ -391,10 +363,17 @@ module Herb
391
363
  return if text.empty?
392
364
 
393
365
  if @trim_next_whitespace
366
+ @last_trim_consumed_newline = text.match?(/\A[ \t]*\r?\n/)
394
367
  text = text.sub(/\A[ \t]*\r?\n/, "")
395
368
  @trim_next_whitespace = false
369
+
370
+ restore_pending_leading_whitespace! unless @last_trim_consumed_newline
371
+ else
372
+ @last_trim_consumed_newline = false
396
373
  end
397
374
 
375
+ @pending_leading_whitespace = nil
376
+
398
377
  return if text.empty?
399
378
 
400
379
  @tokens << [:text, text, current_context]
@@ -410,10 +389,12 @@ module Herb
410
389
 
411
390
  def add_expression(code)
412
391
  @tokens << [:expr, code, current_context]
392
+ @last_trim_consumed_newline = false
413
393
  end
414
394
 
415
395
  def add_expression_escaped(code)
416
396
  @tokens << [:expr_escaped, code, current_context]
397
+ @last_trim_consumed_newline = false
417
398
  end
418
399
 
419
400
  def optimize_tokens(tokens)
@@ -497,12 +478,29 @@ module Herb
497
478
  end
498
479
 
499
480
  def process_erb_output(node, opening, code)
481
+ if @trim_next_whitespace && @pending_leading_whitespace
482
+ restore_pending_leading_whitespace!
483
+ @pending_leading_whitespace = nil
484
+ @trim_next_whitespace = false
485
+ @last_trim_consumed_newline = false
486
+ end
487
+
500
488
  has_right_trim = node.tag_closing&.value == "-%>"
501
489
  should_escape = should_escape_output?(opening)
502
490
  add_expression_with_escaping(code, should_escape)
503
491
  @trim_next_whitespace = true if has_right_trim
504
492
  end
505
493
 
494
+ def indicator_for(type)
495
+ escaped = [:expr_escaped, :expr_block_escaped].include?(type)
496
+
497
+ escaped ^ @escape ? "==" : "="
498
+ end
499
+
500
+ def context_aware_context?(context)
501
+ [:attribute_value, :script_content, :style_content].include?(context)
502
+ end
503
+
506
504
  def should_escape_output?(opening)
507
505
  is_double_equals = opening == "<%=="
508
506
  is_double_equals ? !@escape : @escape
@@ -517,78 +515,119 @@ module Herb
517
515
  end
518
516
 
519
517
  def at_line_start?
520
- @tokens.empty? ||
521
- @tokens.last[0] != :text ||
522
- @tokens.last[1].empty? ||
523
- @tokens.last[1].end_with?("\n") ||
524
- (@tokens.last[1] =~ /\A[ \t]+\z/ && preceding_token_ends_with_newline?) ||
525
- @tokens.last[1] =~ /\n[ \t]+\z/
518
+ return true if @tokens.empty?
519
+
520
+ last_type = @tokens.last[0]
521
+ last_value = @tokens.last[1]
522
+
523
+ if last_type == :text
524
+ last_value.empty? || last_value.end_with?("\n") || (last_value =~ /\A[ \t]+\z/ && preceding_token_ends_with_newline?) || last_value =~ /\n[ \t]+\z/
525
+ elsif EXPRESSION_TOKEN_TYPES.include?(last_type)
526
+ @last_trim_consumed_newline
527
+ else
528
+ last_value.end_with?("\n")
529
+ end
526
530
  end
527
531
 
528
532
  def preceding_token_ends_with_newline?
529
533
  return true unless @tokens.length >= 2
530
534
 
531
535
  preceding = @tokens[-2]
532
- return false if [:expr, :expr_escaped, :expr_block, :expr_block_escaped, :expr_block_end].include?(preceding[0])
536
+ return @last_trim_consumed_newline if EXPRESSION_TOKEN_TYPES.include?(preceding[0])
537
+ return preceding[1].end_with?("\n") if preceding[0] == :expr_block_end
533
538
  return true unless preceding[0] == :text
534
539
 
535
540
  preceding[1].end_with?("\n")
536
541
  end
537
542
 
538
- def extract_lspace
539
- return "" unless @tokens.last && @tokens.last[0] == :text
543
+ def last_text_token
544
+ return unless @tokens.last && @tokens.last[0] == :text
540
545
 
541
- text = @tokens.last[1]
546
+ @tokens.last
547
+ end
548
+
549
+ def extract_leading_space
550
+ token = last_text_token
551
+ return "" unless token
552
+
553
+ text = token[1]
542
554
 
543
555
  return Regexp.last_match(1) if text =~ /\n([ \t]+)\z/ || text =~ /\A([ \t]+)\z/
544
556
 
545
557
  ""
546
558
  end
547
559
 
548
- def extract_and_remove_lspace!
549
- lspace = extract_lspace
550
- return lspace if lspace.empty?
560
+ def leading_space_follows_newline?
561
+ token = last_text_token
562
+ return false unless token
563
+
564
+ token[1].match?(/\n[ \t]+\z/)
565
+ end
566
+
567
+ def extract_and_remove_leading_space!
568
+ leading_space = extract_leading_space
569
+ return leading_space if leading_space.empty?
551
570
 
552
571
  text = @tokens.last[1]
572
+
553
573
  if text =~ /\n[ \t]+\z/
554
574
  text.sub!(/[ \t]+\z/, "")
555
575
  elsif text =~ /\A[ \t]+\z/
556
576
  text.replace("")
557
577
  end
578
+
558
579
  @tokens.last[1] = text
559
580
 
560
- lspace
581
+ leading_space
561
582
  end
562
583
 
563
584
  def apply_trim(node, code)
564
585
  has_left_trim = node.tag_opening.value.start_with?("<%-")
565
- node.tag_closing&.value
566
586
 
567
- remove_trailing_whitespace_from_last_token! if has_left_trim
587
+ follows_newline = leading_space_follows_newline?
588
+ removed_whitespace = has_left_trim ? remove_trailing_whitespace_from_last_token! : ""
568
589
 
569
590
  if at_line_start?
570
- lspace = extract_and_remove_lspace!
571
- rspace = Herb::Engine.heredoc?(code) ? "\n" : " \n"
591
+ leading_space = extract_and_remove_leading_space!
592
+ effective_leading_space = leading_space.empty? ? removed_whitespace : leading_space
593
+ right_space = Herb::Engine.heredoc?(code) ? "\n" : " \n"
572
594
 
573
- @tokens << [:code, "#{lspace}#{code}#{rspace}", current_context]
595
+ @pending_leading_whitespace_insert_index = @tokens.length
596
+ @pending_leading_whitespace = effective_leading_space if !effective_leading_space.empty? && follows_newline
597
+ @tokens << [:code, "#{effective_leading_space}#{code}#{right_space}", current_context]
574
598
  @trim_next_whitespace = true
575
599
  else
576
600
  @tokens << [:code, code, current_context]
577
601
  end
578
602
  end
579
603
 
604
+ def save_pending_leading_whitespace!(whitespace)
605
+ @pending_leading_whitespace = whitespace
606
+ @pending_leading_whitespace_insert_index = @tokens.length
607
+ end
608
+
609
+ def restore_pending_leading_whitespace!
610
+ return unless @pending_leading_whitespace
611
+
612
+ @tokens.insert(@pending_leading_whitespace_insert_index, [:text, @pending_leading_whitespace, current_context])
613
+ end
614
+
580
615
  def remove_trailing_whitespace_from_last_token!
581
- return unless @tokens.last && @tokens.last[0] == :text
616
+ token = last_text_token
617
+ return "" unless token
582
618
 
583
- text = @tokens.last[1]
619
+ text = token[1]
620
+ removed = text[/[ \t]+\z/] || ""
584
621
 
585
622
  if text =~ /\n[ \t]+\z/
586
623
  text.sub!(/[ \t]+\z/, "")
587
- @tokens.last[1] = text
624
+ token[1] = text
588
625
  elsif text =~ /\A[ \t]+\z/
589
626
  text.replace("")
590
- @tokens.last[1] = text
627
+ token[1] = text
591
628
  end
629
+
630
+ removed
592
631
  end
593
632
  end
594
633
  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
@@ -55,6 +55,9 @@ module Herb
55
55
  @bufvar = properties[:bufvar] || properties[:outvar] || "_buf"
56
56
  @escape = properties.fetch(:escape) { properties.fetch(:escape_html, false) }
57
57
  @escapefunc = properties.fetch(:escapefunc, @escape ? "__herb.h" : "::Herb::Engine.h")
58
+ @attrfunc = properties.fetch(:attrfunc, @escape ? "__herb.attr" : "::Herb::Engine.attr")
59
+ @jsfunc = properties.fetch(:jsfunc, @escape ? "__herb.js" : "::Herb::Engine.js")
60
+ @cssfunc = properties.fetch(:cssfunc, @escape ? "__herb.css" : "::Herb::Engine.css")
58
61
  @src = properties[:src] || String.new
59
62
  @chain_appends = properties[:chain_appends]
60
63
  @buffer_on_stack = false
@@ -244,6 +247,24 @@ module Herb
244
247
  end
245
248
  end
246
249
 
250
+ def add_context_aware_expression(indicator, code, context)
251
+ escapefunc = context_escape_function(context)
252
+
253
+ if escapefunc.nil?
254
+ add_expression(indicator, code)
255
+ else
256
+ with_buffer { @src << " << #{escapefunc}((" << code << trailing_newline(code) << "))" }
257
+ end
258
+ end
259
+
260
+ def context_escape_function(context)
261
+ case context
262
+ when :attribute_value then @attrfunc
263
+ when :script_content then @jsfunc
264
+ when :style_content then @cssfunc
265
+ end
266
+ end
267
+
247
268
  def add_expression_result(code)
248
269
  with_buffer {
249
270
  @src << " << (" << code << trailing_newline(code) << ").to_s"