herb 0.9.2-x86_64-linux-gnu → 0.9.3-x86_64-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 (168) 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 +122 -59
  20. data/lib/herb/engine/validators/security_validator.rb +40 -0
  21. data/lib/herb/errors.rb +268 -0
  22. data/lib/herb/parser_options.rb +7 -2
  23. data/lib/herb/version.rb +1 -1
  24. data/lib/herb/visitor.rb +82 -0
  25. data/lib/herb.rb +1 -0
  26. data/sig/herb/ast/erb_content_node.rbs +13 -0
  27. data/sig/herb/ast/nodes.rbs +98 -2
  28. data/sig/herb/engine/compiler.rbs +15 -2
  29. data/sig/herb/engine/validators/security_validator.rbs +4 -0
  30. data/sig/herb/errors.rbs +122 -0
  31. data/sig/herb/parser_options.rbs +6 -2
  32. data/sig/herb/visitor.rbs +12 -0
  33. data/sig/serialized_ast_errors.rbs +29 -0
  34. data/sig/serialized_ast_nodes.rbs +19 -0
  35. data/src/analyze/action_view/attribute_extraction_helpers.c +420 -91
  36. data/src/analyze/action_view/image_tag.c +87 -0
  37. data/src/analyze/action_view/javascript_include_tag.c +22 -12
  38. data/src/analyze/action_view/registry.c +6 -3
  39. data/src/analyze/action_view/tag.c +19 -2
  40. data/src/analyze/action_view/tag_helper_node_builders.c +105 -36
  41. data/src/analyze/action_view/tag_helpers.c +792 -44
  42. data/src/analyze/analyze.c +165 -10
  43. data/src/analyze/{helpers.c → analyze_helpers.c} +1 -1
  44. data/src/analyze/analyzed_ruby.c +1 -1
  45. data/src/analyze/builders.c +11 -8
  46. data/src/analyze/conditional_elements.c +6 -7
  47. data/src/analyze/conditional_open_tags.c +6 -7
  48. data/src/analyze/control_type.c +4 -2
  49. data/src/analyze/invalid_structures.c +5 -5
  50. data/src/analyze/missing_end.c +2 -2
  51. data/src/analyze/parse_errors.c +5 -5
  52. data/src/analyze/prism_annotate.c +7 -7
  53. data/src/analyze/render_nodes.c +6 -26
  54. data/src/analyze/strict_locals.c +637 -0
  55. data/src/analyze/transform.c +7 -0
  56. data/src/{ast_node.c → ast/ast_node.c} +8 -8
  57. data/src/{ast_nodes.c → ast/ast_nodes.c} +82 -11
  58. data/src/{ast_pretty_print.c → ast/ast_pretty_print.c} +113 -9
  59. data/src/{pretty_print.c → ast/pretty_print.c} +9 -9
  60. data/src/errors.c +398 -8
  61. data/src/extract.c +5 -5
  62. data/src/herb.c +15 -5
  63. data/src/include/analyze/action_view/attribute_extraction_helpers.h +3 -1
  64. data/src/include/analyze/action_view/tag_helper_handler.h +3 -3
  65. data/src/include/analyze/action_view/tag_helper_node_builders.h +34 -5
  66. data/src/include/analyze/action_view/tag_helpers.h +4 -3
  67. data/src/include/analyze/analyze.h +6 -4
  68. data/src/include/analyze/analyzed_ruby.h +2 -2
  69. data/src/include/analyze/builders.h +4 -4
  70. data/src/include/analyze/conditional_elements.h +2 -2
  71. data/src/include/analyze/conditional_open_tags.h +2 -2
  72. data/src/include/analyze/control_type.h +1 -1
  73. data/src/include/analyze/helpers.h +2 -2
  74. data/src/include/analyze/invalid_structures.h +1 -1
  75. data/src/include/analyze/prism_annotate.h +2 -2
  76. data/src/include/analyze/render_nodes.h +1 -1
  77. data/src/include/analyze/strict_locals.h +11 -0
  78. data/src/include/{ast_node.h → ast/ast_node.h} +4 -4
  79. data/src/include/{ast_nodes.h → ast/ast_nodes.h} +38 -14
  80. data/src/include/{ast_pretty_print.h → ast/ast_pretty_print.h} +3 -3
  81. data/src/include/{pretty_print.h → ast/pretty_print.h} +4 -4
  82. data/src/include/errors.h +65 -7
  83. data/src/include/extract.h +2 -2
  84. data/src/include/herb.h +5 -5
  85. data/src/include/{lex_helpers.h → lexer/lex_helpers.h} +5 -5
  86. data/src/include/{lexer.h → lexer/lexer.h} +1 -1
  87. data/src/include/{lexer_peek_helpers.h → lexer/lexer_peek_helpers.h} +2 -2
  88. data/src/include/{lexer_struct.h → lexer/lexer_struct.h} +2 -2
  89. data/src/include/{token.h → lexer/token.h} +3 -3
  90. data/src/include/{token_matchers.h → lexer/token_matchers.h} +1 -1
  91. data/src/include/{token_struct.h → lexer/token_struct.h} +3 -3
  92. data/src/include/{util → lib}/hb_foreach.h +1 -1
  93. data/src/include/{util → lib}/hb_string.h +5 -1
  94. data/src/include/{location.h → location/location.h} +1 -1
  95. data/src/include/parser/dot_notation.h +12 -0
  96. data/src/include/{parser.h → parser/parser.h} +11 -4
  97. data/src/include/{parser_helpers.h → parser/parser_helpers.h} +6 -6
  98. data/src/include/{prism_context.h → prism/prism_context.h} +2 -2
  99. data/src/include/{prism_helpers.h → prism/prism_helpers.h} +6 -6
  100. data/src/include/{html_util.h → util/html_util.h} +1 -1
  101. data/src/include/util/ruby_util.h +9 -0
  102. data/src/include/{utf8.h → util/utf8.h} +1 -1
  103. data/src/include/{util.h → util/util.h} +1 -1
  104. data/src/include/version.h +1 -1
  105. data/src/include/visitor.h +3 -3
  106. data/src/{lexer_peek_helpers.c → lexer/lexer_peek_helpers.c} +3 -3
  107. data/src/{token.c → lexer/token.c} +8 -8
  108. data/src/{token_matchers.c → lexer/token_matchers.c} +2 -2
  109. data/src/lexer.c +6 -6
  110. data/src/{util → lib}/hb_allocator.c +2 -2
  111. data/src/{util → lib}/hb_arena.c +1 -1
  112. data/src/{util → lib}/hb_arena_debug.c +2 -2
  113. data/src/{util → lib}/hb_array.c +2 -2
  114. data/src/{util → lib}/hb_buffer.c +2 -2
  115. data/src/{util → lib}/hb_narray.c +1 -1
  116. data/src/{util → lib}/hb_string.c +2 -2
  117. data/src/{location.c → location/location.c} +2 -2
  118. data/src/{position.c → location/position.c} +2 -2
  119. data/src/{range.c → location/range.c} +1 -1
  120. data/src/main.c +11 -11
  121. data/src/parser/dot_notation.c +100 -0
  122. data/src/{parser_match_tags.c → parser/match_tags.c} +34 -5
  123. data/src/{parser_helpers.c → parser/parser_helpers.c} +10 -10
  124. data/src/parser.c +68 -32
  125. data/src/{prism_helpers.c → prism/prism_helpers.c} +7 -7
  126. data/src/{ruby_parser.c → prism/ruby_parser.c} +1 -1
  127. data/src/{html_util.c → util/html_util.c} +4 -4
  128. data/src/{io.c → util/io.c} +3 -3
  129. data/src/util/ruby_util.c +42 -0
  130. data/src/{utf8.c → util/utf8.c} +2 -2
  131. data/src/{util.c → util/util.c} +4 -4
  132. data/src/visitor.c +35 -3
  133. data/templates/ext/herb/error_helpers.c.erb +2 -2
  134. data/templates/ext/herb/nodes.c.erb +1 -1
  135. data/templates/java/error_helpers.c.erb +1 -1
  136. data/templates/java/error_helpers.h.erb +2 -2
  137. data/templates/java/nodes.c.erb +4 -4
  138. data/templates/java/nodes.h.erb +1 -1
  139. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +4 -4
  140. data/templates/javascript/packages/node/extension/nodes.cpp.erb +4 -4
  141. data/templates/lib/herb/visitor.rb.erb +14 -0
  142. data/templates/src/analyze/missing_end.c.erb +2 -2
  143. data/templates/src/{ast_nodes.c.erb → ast/ast_nodes.c.erb} +9 -9
  144. data/templates/src/{ast_pretty_print.c.erb → ast/ast_pretty_print.c.erb} +8 -8
  145. data/templates/src/errors.c.erb +8 -8
  146. data/templates/src/include/{ast_nodes.h.erb → ast/ast_nodes.h.erb} +11 -12
  147. data/templates/src/include/{ast_pretty_print.h.erb → ast/ast_pretty_print.h.erb} +2 -2
  148. data/templates/src/include/errors.h.erb +7 -7
  149. data/templates/src/{parser_match_tags.c.erb → parser/match_tags.c.erb} +4 -4
  150. data/templates/src/visitor.c.erb +3 -3
  151. data/templates/wasm/error_helpers.cpp.erb +4 -4
  152. data/templates/wasm/nodes.cpp.erb +5 -5
  153. metadata +76 -68
  154. data/src/include/element_source.h +0 -10
  155. /data/src/include/{util → lib}/hb_allocator.h +0 -0
  156. /data/src/include/{util → lib}/hb_arena.h +0 -0
  157. /data/src/include/{util → lib}/hb_arena_debug.h +0 -0
  158. /data/src/include/{util → lib}/hb_array.h +0 -0
  159. /data/src/include/{util → lib}/hb_buffer.h +0 -0
  160. /data/src/include/{util → lib}/hb_narray.h +0 -0
  161. /data/src/include/{util → lib}/string.h +0 -0
  162. /data/src/include/{position.h → location/position.h} +0 -0
  163. /data/src/include/{range.h → location/range.h} +0 -0
  164. /data/src/include/{herb_prism_node.h → prism/herb_prism_node.h} +0 -0
  165. /data/src/include/{prism_serialized.h → prism/prism_serialized.h} +0 -0
  166. /data/src/include/{ruby_parser.h → prism/ruby_parser.h} +0 -0
  167. /data/src/include/{io.h → util/io.h} +0 -0
  168. /data/templates/src/include/{util → lib}/hb_foreach.h.erb +0 -0
@@ -2128,6 +2128,9 @@ module Herb
2128
2128
  #| tag_closing: Herb::Token?,
2129
2129
  #| prism_node: String?,
2130
2130
  #| body: Array[Herb::AST::Node],
2131
+ #| rescue_clause: Herb::AST::ERBRescueNode?,
2132
+ #| else_clause: Herb::AST::ERBElseNode?,
2133
+ #| ensure_clause: Herb::AST::ERBEnsureNode?,
2131
2134
  #| end_node: Herb::AST::ERBEndNode?,
2132
2135
  #| }
2133
2136
  class ERBBlockNode < Node
@@ -2138,16 +2141,22 @@ module Herb
2138
2141
  attr_reader :tag_closing #: Herb::Token?
2139
2142
  attr_reader :prism_node #: String?
2140
2143
  attr_reader :body #: Array[Herb::AST::Node]
2144
+ attr_reader :rescue_clause #: Herb::AST::ERBRescueNode?
2145
+ attr_reader :else_clause #: Herb::AST::ERBElseNode?
2146
+ attr_reader :ensure_clause #: Herb::AST::ERBEnsureNode?
2141
2147
  attr_reader :end_node #: Herb::AST::ERBEndNode?
2142
2148
 
2143
- #: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, String, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void
2144
- def initialize(type, location, errors, tag_opening, content, tag_closing, prism_node, body, end_node)
2149
+ #: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, String, Array[Herb::AST::Node], Herb::AST::ERBRescueNode, Herb::AST::ERBElseNode, Herb::AST::ERBEnsureNode, Herb::AST::ERBEndNode) -> void
2150
+ def initialize(type, location, errors, tag_opening, content, tag_closing, prism_node, body, rescue_clause, else_clause, ensure_clause, end_node)
2145
2151
  super(type, location, errors)
2146
2152
  @tag_opening = tag_opening
2147
2153
  @content = content
2148
2154
  @tag_closing = tag_closing
2149
2155
  @prism_node = prism_node
2150
2156
  @body = body
2157
+ @rescue_clause = rescue_clause
2158
+ @else_clause = else_clause
2159
+ @ensure_clause = ensure_clause
2151
2160
  @end_node = end_node
2152
2161
  end
2153
2162
 
@@ -2175,6 +2184,9 @@ module Herb
2175
2184
  tag_closing: tag_closing,
2176
2185
  prism_node: prism_node,
2177
2186
  body: body,
2187
+ rescue_clause: rescue_clause,
2188
+ else_clause: else_clause,
2189
+ ensure_clause: ensure_clause,
2178
2190
  end_node: end_node,
2179
2191
  }) #: Herb::serialized_erb_block_node
2180
2192
  end
@@ -2186,7 +2198,7 @@ module Herb
2186
2198
 
2187
2199
  #: () -> Array[Herb::AST::Node?]
2188
2200
  def child_nodes
2189
- [*(body || []), end_node]
2201
+ [*(body || []), rescue_clause, else_clause, ensure_clause, end_node]
2190
2202
  end
2191
2203
 
2192
2204
  #: () -> Array[Herb::AST::Node]
@@ -2230,6 +2242,30 @@ module Herb
2230
2242
  end
2231
2243
  output += white("├── body: ")
2232
2244
  output += inspect_array(body, prefix: "│ ", indent: indent, depth: depth + 1, depth_limit: depth_limit)
2245
+ output += white("├── rescue_clause: ")
2246
+ if rescue_clause
2247
+ output += "\n"
2248
+ output += "│ └── "
2249
+ output += rescue_clause.tree_inspect(indent: indent, depth: depth + 1, depth_limit: depth_limit).gsub(/^/, " " * (indent + 1)).lstrip.gsub(/^/, "│ ").delete_prefix("│ ")
2250
+ else
2251
+ output += magenta("∅\n")
2252
+ end
2253
+ output += white("├── else_clause: ")
2254
+ if else_clause
2255
+ output += "\n"
2256
+ output += "│ └── "
2257
+ output += else_clause.tree_inspect(indent: indent, depth: depth + 1, depth_limit: depth_limit).gsub(/^/, " " * (indent + 1)).lstrip.gsub(/^/, "│ ").delete_prefix("│ ")
2258
+ else
2259
+ output += magenta("∅\n")
2260
+ end
2261
+ output += white("├── ensure_clause: ")
2262
+ if ensure_clause
2263
+ output += "\n"
2264
+ output += "│ └── "
2265
+ output += ensure_clause.tree_inspect(indent: indent, depth: depth + 1, depth_limit: depth_limit).gsub(/^/, " " * (indent + 1)).lstrip.gsub(/^/, "│ ").delete_prefix("│ ")
2266
+ else
2267
+ output += magenta("∅\n")
2268
+ end
2233
2269
  output += white("└── end_node: ")
2234
2270
  if end_node
2235
2271
  output += "\n"
@@ -3773,6 +3809,211 @@ module Herb
3773
3809
  end
3774
3810
  end
3775
3811
 
3812
+ #: type serialized_ruby_strict_local_node = {
3813
+ #| name: Herb::Token?,
3814
+ #| default_value: Herb::AST::RubyLiteralNode?,
3815
+ #| required: bool,
3816
+ #| double_splat: bool,
3817
+ #| }
3818
+ class RubyStrictLocalNode < Node
3819
+ include Colors
3820
+
3821
+ attr_reader :name #: Herb::Token?
3822
+ attr_reader :default_value #: Herb::AST::RubyLiteralNode?
3823
+ attr_reader :required #: bool
3824
+ attr_reader :double_splat #: bool
3825
+
3826
+ #: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::AST::RubyLiteralNode, bool, bool) -> void
3827
+ def initialize(type, location, errors, name, default_value, required, double_splat)
3828
+ super(type, location, errors)
3829
+ @name = name
3830
+ @default_value = default_value
3831
+ @required = required
3832
+ @double_splat = double_splat
3833
+ end
3834
+
3835
+ #: () -> serialized_ruby_strict_local_node
3836
+ def to_hash
3837
+ super.merge({
3838
+ name: name,
3839
+ default_value: default_value,
3840
+ required: required,
3841
+ double_splat: double_splat,
3842
+ }) #: Herb::serialized_ruby_strict_local_node
3843
+ end
3844
+
3845
+ #: (Visitor) -> void
3846
+ def accept(visitor)
3847
+ visitor.visit_ruby_strict_local_node(self)
3848
+ end
3849
+
3850
+ #: () -> Array[Herb::AST::Node?]
3851
+ def child_nodes
3852
+ [default_value]
3853
+ end
3854
+
3855
+ #: () -> Array[Herb::AST::Node]
3856
+ def compact_child_nodes
3857
+ child_nodes.compact
3858
+ end
3859
+
3860
+ #: () -> String
3861
+ def inspect
3862
+ tree_inspect.rstrip.gsub(/\s+$/, "")
3863
+ end
3864
+
3865
+ #: (?indent: Integer, ?depth: Integer, ?depth_limit: Integer) -> String
3866
+ def tree_inspect(indent: 0, depth: 0, depth_limit: 10)
3867
+ output = +""
3868
+
3869
+ output += white("@ #{bold(yellow(node_name.to_s))} #{dimmed("(location: #{location.tree_inspect})")}")
3870
+ output += "\n"
3871
+
3872
+ if depth >= depth_limit
3873
+ output += dimmed("└── [depth limit reached ...]\n\n")
3874
+
3875
+ return output.gsub(/^/, " " * indent)
3876
+ end
3877
+
3878
+ output += inspect_errors(prefix: "│ ")
3879
+
3880
+ output += white("├── name: ")
3881
+ output += name ? name.tree_inspect : magenta("∅")
3882
+ output += "\n"
3883
+ output += white("├── default_value: ")
3884
+ if default_value
3885
+ output += "\n"
3886
+ output += "│ └── "
3887
+ output += default_value.tree_inspect(indent: indent, depth: depth + 1, depth_limit: depth_limit).gsub(/^/, " " * (indent + 1)).lstrip.gsub(/^/, "│ ").delete_prefix("│ ")
3888
+ else
3889
+ output += magenta("∅\n")
3890
+ end
3891
+ output += white("├── required: ")
3892
+ output += [true, false].include?(required) ? bold(magenta(required.to_s)) : magenta("∅")
3893
+ output += "\n"
3894
+ output += white("└── double_splat: ")
3895
+ output += [true, false].include?(double_splat) ? bold(magenta(double_splat.to_s)) : magenta("∅")
3896
+ output += "\n"
3897
+ output += "\n"
3898
+
3899
+ output.gsub(/^/, " " * indent)
3900
+ end
3901
+ end
3902
+
3903
+ #: type serialized_erb_strict_locals_node = {
3904
+ #| tag_opening: Herb::Token?,
3905
+ #| content: Herb::Token?,
3906
+ #| tag_closing: Herb::Token?,
3907
+ #| analyzed_ruby: nil,
3908
+ #| prism_node: String?,
3909
+ #| locals: Array[Herb::AST::RubyStrictLocalNode]?,
3910
+ #| }
3911
+ class ERBStrictLocalsNode < Node
3912
+ include Colors
3913
+
3914
+ attr_reader :tag_opening #: Herb::Token?
3915
+ attr_reader :content #: Herb::Token?
3916
+ attr_reader :tag_closing #: Herb::Token?
3917
+ attr_reader :analyzed_ruby #: nil
3918
+ attr_reader :prism_node #: String?
3919
+ attr_reader :locals #: Array[Herb::AST::RubyStrictLocalNode]?
3920
+
3921
+ #: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, nil, String, Array[Herb::AST::RubyStrictLocalNode]) -> void
3922
+ def initialize(type, location, errors, tag_opening, content, tag_closing, analyzed_ruby, prism_node, locals)
3923
+ super(type, location, errors)
3924
+ @tag_opening = tag_opening
3925
+ @content = content
3926
+ @tag_closing = tag_closing
3927
+ @analyzed_ruby = analyzed_ruby
3928
+ @prism_node = prism_node
3929
+ @locals = locals
3930
+ end
3931
+
3932
+ #: () -> Prism::node?
3933
+ def deserialized_prism_node
3934
+ prism_node = @prism_node
3935
+ return nil unless prism_node
3936
+ return nil unless source
3937
+
3938
+ begin
3939
+ require "prism"
3940
+ rescue LoadError
3941
+ warn "The 'prism' gem is required to deserialize Prism nodes. Add it to your Gemfile or install it with: gem install prism"
3942
+ return nil
3943
+ end
3944
+
3945
+ Prism.load(source, prism_node).value
3946
+ end
3947
+
3948
+ #: () -> serialized_erb_strict_locals_node
3949
+ def to_hash
3950
+ super.merge({
3951
+ tag_opening: tag_opening,
3952
+ content: content,
3953
+ tag_closing: tag_closing,
3954
+ analyzed_ruby: analyzed_ruby,
3955
+ prism_node: prism_node,
3956
+ locals: locals,
3957
+ }) #: Herb::serialized_erb_strict_locals_node
3958
+ end
3959
+
3960
+ #: (Visitor) -> void
3961
+ def accept(visitor)
3962
+ visitor.visit_erb_strict_locals_node(self)
3963
+ end
3964
+
3965
+ #: () -> Array[Herb::AST::Node?]
3966
+ def child_nodes
3967
+ [*(locals || [])]
3968
+ end
3969
+
3970
+ #: () -> Array[Herb::AST::Node]
3971
+ def compact_child_nodes
3972
+ child_nodes.compact
3973
+ end
3974
+
3975
+ #: () -> String
3976
+ def inspect
3977
+ tree_inspect.rstrip.gsub(/\s+$/, "")
3978
+ end
3979
+
3980
+ #: (?indent: Integer, ?depth: Integer, ?depth_limit: Integer) -> String
3981
+ def tree_inspect(indent: 0, depth: 0, depth_limit: 10)
3982
+ output = +""
3983
+
3984
+ output += white("@ #{bold(yellow(node_name.to_s))} #{dimmed("(location: #{location.tree_inspect})")}")
3985
+ output += "\n"
3986
+
3987
+ if depth >= depth_limit
3988
+ output += dimmed("└── [depth limit reached ...]\n\n")
3989
+
3990
+ return output.gsub(/^/, " " * indent)
3991
+ end
3992
+
3993
+ output += inspect_errors(prefix: "│ ")
3994
+
3995
+ output += white("├── tag_opening: ")
3996
+ output += tag_opening ? tag_opening.tree_inspect : magenta("∅")
3997
+ output += "\n"
3998
+ output += white("├── content: ")
3999
+ output += content ? content.tree_inspect : magenta("∅")
4000
+ output += "\n"
4001
+ output += white("├── tag_closing: ")
4002
+ output += tag_closing ? tag_closing.tree_inspect : magenta("∅")
4003
+ output += "\n"
4004
+ if prism_node && source
4005
+ output += white("├── prism_node: ")
4006
+ output += Herb::PrismInspect.inspect_prism_serialized(prism_node, source, "│ ")
4007
+ output += "\n"
4008
+ end
4009
+ output += white("└── locals: ")
4010
+ output += inspect_array(locals, prefix: " ", indent: indent, depth: depth + 1, depth_limit: depth_limit)
4011
+ output += "\n"
4012
+
4013
+ output.gsub(/^/, " " * indent)
4014
+ end
4015
+ end
4016
+
3776
4017
  #: type serialized_erb_yield_node = {
3777
4018
  #| tag_opening: Herb::Token?,
3778
4019
  #| content: Herb::Token?,
data/lib/herb/cli.rb CHANGED
@@ -8,7 +8,7 @@ require "optparse"
8
8
  class Herb::CLI
9
9
  include Herb::Colors
10
10
 
11
- attr_accessor :json, :silent, :log_file, :no_timing, :local, :escape, :no_escape, :freeze, :debug, :tool, :strict, :analyze, :track_whitespace, :verbose, :isolate, :arena_stats, :leak_check
11
+ attr_accessor :json, :silent, :log_file, :no_timing, :local, :escape, :no_escape, :freeze, :debug, :tool, :strict, :analyze, :track_whitespace, :verbose, :isolate, :arena_stats, :leak_check, :action_view_helpers, :trim
12
12
 
13
13
  def initialize(args)
14
14
  @args = args
@@ -160,7 +160,7 @@ class Herb::CLI
160
160
  show_config
161
161
  exit(0)
162
162
  when "parse"
163
- Herb.parse(file_content, strict: strict.nil? || strict, analyze: analyze.nil? || analyze, track_whitespace: track_whitespace || false, arena_stats: arena_stats)
163
+ Herb.parse(file_content, strict: strict.nil? || strict, analyze: analyze.nil? || analyze, track_whitespace: track_whitespace || false, arena_stats: arena_stats, action_view_helpers: action_view_helpers || false)
164
164
  when "compile"
165
165
  compile_template
166
166
  when "render"
@@ -298,6 +298,14 @@ class Herb::CLI
298
298
  self.track_whitespace = true
299
299
  end
300
300
 
301
+ parser.on("--action-view-helpers", "Enable Action View helper detection (for parse command) (default: false)") do
302
+ self.action_view_helpers = true
303
+ end
304
+
305
+ parser.on("--trim", "Enable trimming of leading/trailing whitespace (for compile/render commands)") do
306
+ self.trim = true
307
+ end
308
+
301
309
  parser.on("--tool TOOL", "Show config for specific tool: linter, formatter (for config command)") do |t|
302
310
  self.tool = t.to_sym
303
311
  end
@@ -442,6 +450,7 @@ class Herb::CLI
442
450
  options[:debug_filename] = @file if @file
443
451
  end
444
452
 
453
+ options[:trim] = true if trim
445
454
  options[:validate_ruby] = true
446
455
  engine = Herb::Engine.new(file_content, options)
447
456
 
@@ -529,6 +538,7 @@ class Herb::CLI
529
538
  options[:debug_filename] = @file if @file
530
539
  end
531
540
 
541
+ options[:trim] = true if trim
532
542
  engine = Herb::Engine.new(file_content, options)
533
543
  compiled_code = engine.src
534
544
 
@@ -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 = {})
@@ -14,6 +16,9 @@ module Herb
14
16
  @element_stack = [] #: Array[String]
15
17
  @context_stack = [:html_content]
16
18
  @trim_next_whitespace = false
19
+ @last_trim_consumed_newline = false
20
+ @pending_leading_whitespace = nil
21
+ @pending_leading_whitespace_insert_index = 0
17
22
  end
18
23
 
19
24
  def generate_output
@@ -46,43 +51,19 @@ module Herb
46
51
  end
47
52
 
48
53
  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)
54
+ with_element_context(node) do
55
+ visit(node.open_tag)
56
+ visit_all(node.body)
57
+ visit(node.close_tag)
57
58
  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
59
  end
67
60
 
68
61
  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)
62
+ with_element_context(node) do
63
+ visit(node.open_conditional)
64
+ visit_all(node.body)
65
+ visit(node.close_conditional)
77
66
  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
67
  end
87
68
 
88
69
  def visit_html_open_tag_node(node)
@@ -270,12 +251,16 @@ module Herb
270
251
  else
271
252
  [:expr_block, code, current_context]
272
253
  end
254
+ @last_trim_consumed_newline = false
273
255
 
274
256
  visit_all(node.body)
275
257
  visit_erb_block_end_node(node.end_node, escaped: should_escape)
276
258
  else
277
259
  visit_erb_control_node(node) do
278
260
  visit_all(node.body)
261
+ visit(node.rescue_clause)
262
+ visit(node.else_clause)
263
+ visit(node.ensure_clause)
279
264
  visit(node.end_node)
280
265
  end
281
266
  end
@@ -289,10 +274,10 @@ module Herb
289
274
  code = node.content.value.strip
290
275
 
291
276
  if at_line_start?
292
- lspace = extract_and_remove_lspace!
293
- rspace = " \n"
277
+ leading_space = extract_and_remove_leading_space!
278
+ right_space = " \n"
294
279
 
295
- @tokens << [:expr_block_end, "#{lspace}#{code}#{rspace}", current_context, escaped]
280
+ @tokens << [:expr_block_end, "#{leading_space}#{code}#{right_space}", current_context, escaped]
296
281
  @trim_next_whitespace = true
297
282
  else
298
283
  @tokens << [:expr_block_end, code, current_context, escaped]
@@ -329,16 +314,37 @@ module Herb
329
314
  @context_stack.pop
330
315
  end
331
316
 
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)
327
+ end
328
+
329
+ yield
330
+
331
+ pop_context if ["script", "style"].include?(tag_name)
332
+
333
+ @element_stack.pop if tag_name
334
+ end
335
+
332
336
  def process_erb_tag(node, skip_comment_check: false)
333
337
  opening = node.tag_opening.value
334
338
 
335
339
  if !skip_comment_check && erb_comment?(opening)
336
340
  has_left_trim = opening.start_with?("<%-")
341
+ follows_newline = leading_space_follows_newline?
337
342
  remove_trailing_whitespace_from_last_token! if has_left_trim
338
343
 
339
344
  if at_line_start?
340
- extract_and_remove_lspace!
345
+ leading_space = extract_and_remove_leading_space!
341
346
  @trim_next_whitespace = true
347
+ save_pending_leading_whitespace!(leading_space) if !leading_space.empty? && follows_newline
342
348
  end
343
349
  return
344
350
  end
@@ -357,10 +363,17 @@ module Herb
357
363
  return if text.empty?
358
364
 
359
365
  if @trim_next_whitespace
366
+ @last_trim_consumed_newline = text.match?(/\A[ \t]*\r?\n/)
360
367
  text = text.sub(/\A[ \t]*\r?\n/, "")
361
368
  @trim_next_whitespace = false
369
+
370
+ restore_pending_leading_whitespace! unless @last_trim_consumed_newline
371
+ else
372
+ @last_trim_consumed_newline = false
362
373
  end
363
374
 
375
+ @pending_leading_whitespace = nil
376
+
364
377
  return if text.empty?
365
378
 
366
379
  @tokens << [:text, text, current_context]
@@ -376,10 +389,12 @@ module Herb
376
389
 
377
390
  def add_expression(code)
378
391
  @tokens << [:expr, code, current_context]
392
+ @last_trim_consumed_newline = false
379
393
  end
380
394
 
381
395
  def add_expression_escaped(code)
382
396
  @tokens << [:expr_escaped, code, current_context]
397
+ @last_trim_consumed_newline = false
383
398
  end
384
399
 
385
400
  def optimize_tokens(tokens)
@@ -463,6 +478,13 @@ module Herb
463
478
  end
464
479
 
465
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
+
466
488
  has_right_trim = node.tag_closing&.value == "-%>"
467
489
  should_escape = should_escape_output?(opening)
468
490
  add_expression_with_escaping(code, should_escape)
@@ -493,78 +515,119 @@ module Herb
493
515
  end
494
516
 
495
517
  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/
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
502
530
  end
503
531
 
504
532
  def preceding_token_ends_with_newline?
505
533
  return true unless @tokens.length >= 2
506
534
 
507
535
  preceding = @tokens[-2]
508
- 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
509
538
  return true unless preceding[0] == :text
510
539
 
511
540
  preceding[1].end_with?("\n")
512
541
  end
513
542
 
514
- def extract_lspace
515
- return "" unless @tokens.last && @tokens.last[0] == :text
543
+ def last_text_token
544
+ return unless @tokens.last && @tokens.last[0] == :text
516
545
 
517
- 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]
518
554
 
519
555
  return Regexp.last_match(1) if text =~ /\n([ \t]+)\z/ || text =~ /\A([ \t]+)\z/
520
556
 
521
557
  ""
522
558
  end
523
559
 
524
- def extract_and_remove_lspace!
525
- lspace = extract_lspace
526
- 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?
527
570
 
528
571
  text = @tokens.last[1]
572
+
529
573
  if text =~ /\n[ \t]+\z/
530
574
  text.sub!(/[ \t]+\z/, "")
531
575
  elsif text =~ /\A[ \t]+\z/
532
576
  text.replace("")
533
577
  end
578
+
534
579
  @tokens.last[1] = text
535
580
 
536
- lspace
581
+ leading_space
537
582
  end
538
583
 
539
584
  def apply_trim(node, code)
540
585
  has_left_trim = node.tag_opening.value.start_with?("<%-")
541
- node.tag_closing&.value
542
586
 
543
- 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! : ""
544
589
 
545
590
  if at_line_start?
546
- lspace = extract_and_remove_lspace!
547
- 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"
548
594
 
549
- @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]
550
598
  @trim_next_whitespace = true
551
599
  else
552
600
  @tokens << [:code, code, current_context]
553
601
  end
554
602
  end
555
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
+
556
615
  def remove_trailing_whitespace_from_last_token!
557
- return unless @tokens.last && @tokens.last[0] == :text
616
+ token = last_text_token
617
+ return "" unless token
558
618
 
559
- text = @tokens.last[1]
619
+ text = token[1]
620
+ removed = text[/[ \t]+\z/] || ""
560
621
 
561
622
  if text =~ /\n[ \t]+\z/
562
623
  text.sub!(/[ \t]+\z/, "")
563
- @tokens.last[1] = text
624
+ token[1] = text
564
625
  elsif text =~ /\A[ \t]+\z/
565
626
  text.replace("")
566
- @tokens.last[1] = text
627
+ token[1] = text
567
628
  end
629
+
630
+ removed
568
631
  end
569
632
  end
570
633
  end