herb 0.8.10-arm-linux-gnu → 0.9.1-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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +11 -3
  3. data/README.md +64 -34
  4. data/Rakefile +48 -40
  5. data/config.yml +473 -34
  6. data/ext/herb/error_helpers.c +535 -140
  7. data/ext/herb/error_helpers.h +1 -0
  8. data/ext/herb/extconf.rb +67 -28
  9. data/ext/herb/extension.c +321 -51
  10. data/ext/herb/extension.h +1 -0
  11. data/ext/herb/extension_helpers.c +24 -14
  12. data/ext/herb/extension_helpers.h +2 -2
  13. data/ext/herb/nodes.c +647 -270
  14. data/ext/herb/nodes.h +1 -0
  15. data/herb.gemspec +3 -2
  16. data/lib/herb/3.0/herb.so +0 -0
  17. data/lib/herb/3.1/herb.so +0 -0
  18. data/lib/herb/3.2/herb.so +0 -0
  19. data/lib/herb/3.3/herb.so +0 -0
  20. data/lib/herb/3.4/herb.so +0 -0
  21. data/lib/herb/4.0/herb.so +0 -0
  22. data/lib/herb/ast/helpers.rb +3 -3
  23. data/lib/herb/ast/node.rb +15 -2
  24. data/lib/herb/ast/nodes.rb +1530 -179
  25. data/lib/herb/bootstrap.rb +87 -0
  26. data/lib/herb/cli.rb +341 -31
  27. data/lib/herb/configuration.rb +248 -0
  28. data/lib/herb/defaults.yml +32 -0
  29. data/lib/herb/engine/compiler.rb +78 -11
  30. data/lib/herb/engine/debug_visitor.rb +13 -3
  31. data/lib/herb/engine/error_formatter.rb +13 -9
  32. data/lib/herb/engine/parser_error_overlay.rb +10 -6
  33. data/lib/herb/engine/validator.rb +8 -3
  34. data/lib/herb/engine/validators/nesting_validator.rb +2 -2
  35. data/lib/herb/engine.rb +119 -43
  36. data/lib/herb/errors.rb +808 -88
  37. data/lib/herb/lex_result.rb +1 -0
  38. data/lib/herb/location.rb +7 -3
  39. data/lib/herb/parse_result.rb +12 -2
  40. data/lib/herb/parser_options.rb +62 -0
  41. data/lib/herb/position.rb +1 -0
  42. data/lib/herb/prism_inspect.rb +120 -0
  43. data/lib/herb/project.rb +923 -331
  44. data/lib/herb/range.rb +1 -0
  45. data/lib/herb/token.rb +7 -1
  46. data/lib/herb/version.rb +1 -1
  47. data/lib/herb/visitor.rb +47 -2
  48. data/lib/herb/warnings.rb +6 -1
  49. data/lib/herb.rb +35 -3
  50. data/sig/herb/ast/helpers.rbs +2 -2
  51. data/sig/herb/ast/node.rbs +12 -2
  52. data/sig/herb/ast/nodes.rbs +773 -128
  53. data/sig/herb/bootstrap.rbs +31 -0
  54. data/sig/herb/configuration.rbs +89 -0
  55. data/sig/herb/engine/compiler.rbs +9 -1
  56. data/sig/herb/engine/debug_visitor.rbs +2 -0
  57. data/sig/herb/engine/validator.rbs +5 -1
  58. data/sig/herb/engine.rbs +21 -3
  59. data/sig/herb/errors.rbs +372 -63
  60. data/sig/herb/location.rbs +4 -0
  61. data/sig/herb/parse_result.rbs +4 -2
  62. data/sig/herb/parser_options.rbs +46 -0
  63. data/sig/herb/position.rbs +1 -0
  64. data/sig/herb/prism_inspect.rbs +28 -0
  65. data/sig/herb/range.rbs +1 -0
  66. data/sig/herb/token.rbs +6 -0
  67. data/sig/herb/visitor.rbs +31 -4
  68. data/sig/herb/warnings.rbs +6 -1
  69. data/sig/herb.rbs +14 -0
  70. data/sig/herb_c_extension.rbs +5 -2
  71. data/sig/rubyvm.rbs +5 -0
  72. data/sig/serialized_ast_errors.rbs +82 -6
  73. data/sig/serialized_ast_nodes.rbs +91 -6
  74. data/src/analyze/action_view/attribute_extraction_helpers.c +303 -0
  75. data/src/analyze/action_view/content_tag.c +78 -0
  76. data/src/analyze/action_view/link_to.c +167 -0
  77. data/src/analyze/action_view/registry.c +83 -0
  78. data/src/analyze/action_view/tag.c +70 -0
  79. data/src/analyze/action_view/tag_helper_node_builders.c +305 -0
  80. data/src/analyze/action_view/tag_helpers.c +815 -0
  81. data/src/analyze/action_view/turbo_frame_tag.c +88 -0
  82. data/src/analyze/analyze.c +885 -0
  83. data/src/{analyzed_ruby.c → analyze/analyzed_ruby.c} +13 -11
  84. data/src/analyze/builders.c +343 -0
  85. data/src/analyze/conditional_elements.c +594 -0
  86. data/src/analyze/conditional_open_tags.c +640 -0
  87. data/src/analyze/control_type.c +250 -0
  88. data/src/{analyze_helpers.c → analyze/helpers.c} +48 -23
  89. data/src/analyze/invalid_structures.c +193 -0
  90. data/src/{analyze_missing_end.c → analyze/missing_end.c} +33 -22
  91. data/src/analyze/parse_errors.c +84 -0
  92. data/src/analyze/prism_annotate.c +399 -0
  93. data/src/analyze/render_nodes.c +761 -0
  94. data/src/{analyze_transform.c → analyze/transform.c} +24 -3
  95. data/src/ast_node.c +17 -7
  96. data/src/ast_nodes.c +759 -387
  97. data/src/ast_pretty_print.c +264 -6
  98. data/src/errors.c +1454 -519
  99. data/src/extract.c +145 -49
  100. data/src/herb.c +52 -34
  101. data/src/html_util.c +241 -12
  102. data/src/include/analyze/action_view/attribute_extraction_helpers.h +36 -0
  103. data/src/include/analyze/action_view/tag_helper_handler.h +43 -0
  104. data/src/include/analyze/action_view/tag_helper_node_builders.h +70 -0
  105. data/src/include/analyze/action_view/tag_helpers.h +38 -0
  106. data/src/include/{analyze.h → analyze/analyze.h} +14 -4
  107. data/src/include/{analyzed_ruby.h → analyze/analyzed_ruby.h} +3 -3
  108. data/src/include/analyze/builders.h +27 -0
  109. data/src/include/analyze/conditional_elements.h +9 -0
  110. data/src/include/analyze/conditional_open_tags.h +9 -0
  111. data/src/include/analyze/control_type.h +14 -0
  112. data/src/include/{analyze_helpers.h → analyze/helpers.h} +4 -2
  113. data/src/include/analyze/invalid_structures.h +11 -0
  114. data/src/include/analyze/prism_annotate.h +16 -0
  115. data/src/include/analyze/render_nodes.h +11 -0
  116. data/src/include/ast_node.h +11 -5
  117. data/src/include/ast_nodes.h +154 -38
  118. data/src/include/ast_pretty_print.h +5 -0
  119. data/src/include/element_source.h +3 -8
  120. data/src/include/errors.h +206 -55
  121. data/src/include/extract.h +21 -5
  122. data/src/include/herb.h +18 -6
  123. data/src/include/herb_prism_node.h +13 -0
  124. data/src/include/html_util.h +7 -2
  125. data/src/include/io.h +3 -1
  126. data/src/include/lex_helpers.h +29 -0
  127. data/src/include/lexer.h +1 -1
  128. data/src/include/lexer_peek_helpers.h +87 -13
  129. data/src/include/lexer_struct.h +2 -0
  130. data/src/include/location.h +2 -1
  131. data/src/include/parser.h +28 -2
  132. data/src/include/parser_helpers.h +19 -3
  133. data/src/include/pretty_print.h +10 -5
  134. data/src/include/prism_context.h +45 -0
  135. data/src/include/prism_helpers.h +10 -7
  136. data/src/include/prism_serialized.h +12 -0
  137. data/src/include/token.h +16 -4
  138. data/src/include/token_struct.h +10 -3
  139. data/src/include/utf8.h +2 -1
  140. data/src/include/util/hb_allocator.h +78 -0
  141. data/src/include/util/hb_arena.h +6 -1
  142. data/src/include/util/hb_arena_debug.h +12 -1
  143. data/src/include/util/hb_array.h +7 -3
  144. data/src/include/util/hb_buffer.h +6 -4
  145. data/src/include/util/hb_foreach.h +79 -0
  146. data/src/include/util/hb_narray.h +8 -4
  147. data/src/include/util/hb_string.h +56 -9
  148. data/src/include/util.h +6 -3
  149. data/src/include/version.h +1 -1
  150. data/src/io.c +3 -2
  151. data/src/lexer.c +42 -30
  152. data/src/lexer_peek_helpers.c +12 -74
  153. data/src/location.c +2 -2
  154. data/src/main.c +53 -28
  155. data/src/parser.c +784 -247
  156. data/src/parser_helpers.c +110 -23
  157. data/src/parser_match_tags.c +129 -48
  158. data/src/pretty_print.c +29 -24
  159. data/src/prism_helpers.c +30 -27
  160. data/src/ruby_parser.c +2 -0
  161. data/src/token.c +151 -66
  162. data/src/token_matchers.c +0 -1
  163. data/src/utf8.c +7 -6
  164. data/src/util/hb_allocator.c +341 -0
  165. data/src/util/hb_arena.c +81 -56
  166. data/src/util/hb_arena_debug.c +32 -17
  167. data/src/util/hb_array.c +30 -15
  168. data/src/util/hb_buffer.c +17 -21
  169. data/src/util/hb_narray.c +22 -7
  170. data/src/util/hb_string.c +49 -35
  171. data/src/util.c +21 -11
  172. data/src/visitor.c +67 -0
  173. data/templates/ext/herb/error_helpers.c.erb +24 -11
  174. data/templates/ext/herb/error_helpers.h.erb +1 -0
  175. data/templates/ext/herb/nodes.c.erb +50 -16
  176. data/templates/ext/herb/nodes.h.erb +1 -0
  177. data/templates/java/error_helpers.c.erb +1 -1
  178. data/templates/java/nodes.c.erb +30 -8
  179. data/templates/java/org/herb/ast/Errors.java.erb +24 -1
  180. data/templates/java/org/herb/ast/Nodes.java.erb +80 -21
  181. data/templates/javascript/packages/core/src/errors.ts.erb +16 -3
  182. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +3 -1
  183. data/templates/javascript/packages/core/src/nodes.ts.erb +109 -32
  184. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +13 -4
  185. data/templates/javascript/packages/node/extension/nodes.cpp.erb +43 -4
  186. data/templates/lib/herb/ast/nodes.rb.erb +95 -32
  187. data/templates/lib/herb/errors.rb.erb +15 -3
  188. data/templates/lib/herb/visitor.rb.erb +2 -2
  189. data/templates/rust/src/ast/nodes.rs.erb +97 -44
  190. data/templates/rust/src/errors.rs.erb +2 -1
  191. data/templates/rust/src/nodes.rs.erb +168 -16
  192. data/templates/rust/src/union_types.rs.erb +60 -0
  193. data/templates/rust/src/visitor.rs.erb +81 -0
  194. data/templates/src/{analyze_missing_end.c.erb → analyze/missing_end.c.erb} +9 -6
  195. data/templates/src/{analyze_transform.c.erb → analyze/transform.c.erb} +2 -2
  196. data/templates/src/ast_nodes.c.erb +34 -26
  197. data/templates/src/ast_pretty_print.c.erb +24 -5
  198. data/templates/src/errors.c.erb +60 -54
  199. data/templates/src/include/ast_nodes.h.erb +6 -2
  200. data/templates/src/include/ast_pretty_print.h.erb +5 -0
  201. data/templates/src/include/errors.h.erb +15 -11
  202. data/templates/src/include/util/hb_foreach.h.erb +20 -0
  203. data/templates/src/parser_match_tags.c.erb +10 -4
  204. data/templates/src/visitor.c.erb +2 -2
  205. data/templates/template.rb +204 -29
  206. data/templates/wasm/error_helpers.cpp.erb +9 -5
  207. data/templates/wasm/nodes.cpp.erb +41 -4
  208. metadata +60 -16
  209. data/src/analyze.c +0 -1608
  210. data/src/element_source.c +0 -12
  211. data/src/include/util/hb_system.h +0 -9
  212. data/src/util/hb_system.c +0 -30
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ # typed: ignore
3
+
4
+ require "fileutils"
5
+
6
+ module Herb
7
+ module Bootstrap
8
+ ROOT_PATH = File.expand_path("../..", __dir__)
9
+ PRISM_VENDOR_DIR = File.join(ROOT_PATH, "vendor", "prism")
10
+
11
+ PRISM_ENTRIES = [
12
+ "config.yml",
13
+ "Rakefile",
14
+ "src/",
15
+ "include/",
16
+ "templates/"
17
+ ].freeze
18
+
19
+ def self.generate_templates
20
+ require "pathname"
21
+ require "set"
22
+ require_relative "../../templates/template"
23
+
24
+ Dir.chdir(ROOT_PATH) do
25
+ Dir.glob("#{ROOT_PATH}/templates/**/*.erb").each do |template|
26
+ Herb::Template.render(template)
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.git_source?
32
+ File.directory?(File.join(ROOT_PATH, ".git"))
33
+ end
34
+
35
+ def self.templates_generated?
36
+ File.exist?(File.join(ROOT_PATH, "ext", "herb", "nodes.c"))
37
+ end
38
+
39
+ def self.vendor_prism(prism_gem_path:)
40
+ FileUtils.mkdir_p(PRISM_VENDOR_DIR)
41
+
42
+ PRISM_ENTRIES.each do |entry|
43
+ source = File.join(prism_gem_path, entry)
44
+ next unless File.exist?(source)
45
+
46
+ puts "Vendoring '#{entry}' Prism file to #{PRISM_VENDOR_DIR}/#{entry}"
47
+ FileUtils.cp_r(source, PRISM_VENDOR_DIR)
48
+ end
49
+
50
+ generate_prism_templates unless prism_ast_header_exists?
51
+ end
52
+
53
+ def self.prism_vendored?
54
+ File.directory?(File.join(PRISM_VENDOR_DIR, "include"))
55
+ end
56
+
57
+ def self.prism_ast_header_exists?
58
+ File.exist?(File.join(PRISM_VENDOR_DIR, "include", "prism", "ast.h"))
59
+ end
60
+
61
+ def self.find_prism_gem_path
62
+ find_prism_as_bundler_sibling || find_prism_from_gem_spec
63
+ end
64
+
65
+ def self.generate_prism_templates
66
+ puts "Generating Prism template files..."
67
+ system("ruby", "#{PRISM_VENDOR_DIR}/templates/template.rb", exception: true)
68
+ end
69
+
70
+ def self.find_prism_as_bundler_sibling
71
+ bundler_gems_dir = File.expand_path("..", ROOT_PATH)
72
+ candidates = Dir.glob(File.join(bundler_gems_dir, "prism-*"))
73
+
74
+ candidates.find { |path| File.directory?(File.join(path, "src")) }
75
+ end
76
+
77
+ def self.find_prism_from_gem_spec
78
+ path = Gem::Specification.find_by_name("prism").full_gem_path
79
+
80
+ return path if File.directory?(File.join(path, "src"))
81
+
82
+ nil
83
+ rescue Gem::MissingSpecError
84
+ nil
85
+ end
86
+ end
87
+ end
data/lib/herb/cli.rb CHANGED
@@ -8,16 +8,16 @@ require "optparse"
8
8
  class Herb::CLI
9
9
  include Herb::Colors
10
10
 
11
- attr_accessor :json, :silent, :no_interactive, :no_log_file, :no_timing, :local, :escape, :no_escape, :freeze, :debug
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
12
12
 
13
13
  def initialize(args)
14
14
  @args = args
15
15
  @command = args[0]
16
- @file = args[1]
17
16
  end
18
17
 
19
18
  def call
20
19
  options
20
+ @file = @args[1]
21
21
 
22
22
  if silent
23
23
  if result.failed?
@@ -61,43 +61,60 @@ class Herb::CLI
61
61
  end
62
62
 
63
63
  def file_content
64
- if @file && File.exist?(@file)
64
+ if @file && @file != "-" && File.exist?(@file)
65
65
  File.read(@file)
66
- elsif @file
66
+ elsif @file && @file != "-"
67
67
  puts "File doesn't exist: #{@file}"
68
68
  exit(1)
69
+ elsif @file == "-" || !$stdin.tty?
70
+ $stdin.read
69
71
  else
70
72
  puts "No file provided."
71
73
  puts
72
74
  puts "Usage:"
73
75
  puts " bundle exec herb #{@command} [file] [options]"
76
+ puts
77
+ puts "You can also pipe content via stdin:"
78
+ puts " echo \"<div>Hello</div>\" | bundle exec herb #{@command}"
79
+ puts " cat file.html.erb | bundle exec herb #{@command}"
80
+ puts " bundle exec herb #{@command} -"
74
81
  exit(1)
75
82
  end
76
83
  end
77
84
 
78
85
  def help(exit_code = 0)
79
86
  message = <<~HELP
80
- ▗▖ ▗▖▗▄▄▄▖▗▄▄▖ ▗▄▄▖
81
- ▐▌ ▐▌▐▌ ▐▌ ▐▌▐▌ ▐▌
82
- ▐▛▀▜▌▐▛▀▀▘▐▛▀▚▖▐▛▀▚▖
83
- ▐▌ ▐▌▐▙▄▄▖▐▌ ▐▌▐▙▄▞▘
84
-
85
- Herb 🌿 Powerful and seamless HTML-aware ERB parsing and tooling.
87
+ Herb 🌿 Powerful and seamless HTML-aware ERB toolchain.
86
88
 
87
89
  Usage:
88
90
  bundle exec herb [command] [options]
89
91
 
90
92
  Commands:
91
- bundle exec herb lex [file] Lex a file.
92
- bundle exec herb parse [file] Parse a file.
93
- bundle exec herb compile [file] Compile ERB template to Ruby code.
94
- bundle exec herb render [file] Compile and render ERB template to final output.
95
- bundle exec herb analyze [path] Analyze a project by passing a directory to the root of the project
96
- bundle exec herb ruby [file] Extract Ruby from a file.
97
- bundle exec herb html [file] Extract HTML from a file.
98
- bundle exec herb prism [file] Extract Ruby from a file and parse the Ruby source with Prism.
99
- bundle exec herb playground [file] Open the content of the source file in the playground
100
- bundle exec herb version Prints the versions of the Herb gem and the libherb library.
93
+ bundle exec herb lex [file] Lex a file.
94
+ bundle exec herb parse [file] Parse a file.
95
+ bundle exec herb compile [file] Compile ERB template to Ruby code.
96
+ bundle exec herb render [file] Compile and render ERB template to final output.
97
+ bundle exec herb analyze [path] Analyze a project by passing a directory to the root of the project
98
+ bundle exec herb report [file] Generate a Markdown bug report for a file
99
+ bundle exec herb config [path] Show configuration and file patterns for a project
100
+ bundle exec herb ruby [file] Extract Ruby from a file.
101
+ bundle exec herb html [file] Extract HTML from a file.
102
+ bundle exec herb playground [file] Open the content of the source file in the playground
103
+ bundle exec herb version Prints the versions of the Herb gem and the libherb library.
104
+
105
+ bundle exec herb lint [patterns] Lint templates (delegates to @herb-tools/linter)
106
+ bundle exec herb format [patterns] Format templates (delegates to @herb-tools/formatter)
107
+ bundle exec herb highlight [file] Syntax highlight templates (delegates to @herb-tools/highlighter)
108
+ bundle exec herb print [file] Print AST (delegates to @herb-tools/printer)
109
+ bundle exec herb lsp Start the language server (delegates to @herb-tools/language-server)
110
+
111
+ stdin:
112
+ Commands that accept [file] also accept input via stdin:
113
+ echo "<div>Hello</div>" | bundle exec herb lex
114
+ cat file.html.erb | bundle exec herb parse
115
+
116
+ Use `-` to explicitly read from stdin:
117
+ bundle exec herb compile -
101
118
 
102
119
  Options:
103
120
  #{option_parser.to_s.strip.gsub(/^ /, " ")}
@@ -112,21 +129,44 @@ class Herb::CLI
112
129
  def result
113
130
  @result ||= case @command
114
131
  when "analyze"
115
- project = Herb::Project.new(directory)
116
- project.no_interactive = no_interactive
117
- project.no_log_file = no_log_file
132
+ path = @file || "."
133
+
134
+ if path != "-" && File.file?(path)
135
+ project = Herb::Project.new(File.dirname(path))
136
+ project.file_paths = [File.expand_path(path)]
137
+ else
138
+ unless File.directory?(path)
139
+ puts "Not a file or directory: '#{path}'."
140
+ exit(1)
141
+ end
142
+
143
+ project = Herb::Project.new(path)
144
+ end
145
+
146
+ project.no_log_file = log_file ? false : true
118
147
  project.no_timing = no_timing
119
148
  project.silent = silent
120
- has_issues = project.parse!
149
+ project.verbose = verbose || ci?
150
+ project.isolate = isolate
151
+ project.validate_ruby = true
152
+ project.arena_stats = arena_stats
153
+ project.leak_check = leak_check
154
+ has_issues = project.analyze!
121
155
  exit(has_issues ? 1 : 0)
156
+ when "report"
157
+ generate_report
158
+ exit(0)
159
+ when "config"
160
+ show_config
161
+ exit(0)
122
162
  when "parse"
123
- Herb.parse(file_content)
163
+ Herb.parse(file_content, strict: strict.nil? || strict, analyze: analyze.nil? || analyze, track_whitespace: track_whitespace || false, arena_stats: arena_stats)
124
164
  when "compile"
125
165
  compile_template
126
166
  when "render"
127
167
  render_template
128
168
  when "lex"
129
- Herb.lex(file_content)
169
+ Herb.lex(file_content, arena_stats: arena_stats)
130
170
  when "ruby"
131
171
  puts Herb.extract_ruby(file_content)
132
172
  exit(0)
@@ -134,7 +174,12 @@ class Herb::CLI
134
174
  puts Herb.extract_html(file_content)
135
175
  exit(0)
136
176
  when "playground"
137
- require "lz_string"
177
+ require "bundler/inline"
178
+
179
+ gemfile do
180
+ source "https://rubygems.org"
181
+ gem "lz_string"
182
+ end
138
183
 
139
184
  hash = LZString::UriSafe.compress(file_content)
140
185
  local_url = "http://localhost:5173"
@@ -152,6 +197,16 @@ class Herb::CLI
152
197
  system(%(open "#{url}##{hash}"))
153
198
  exit(0)
154
199
  end
200
+ when "lint"
201
+ run_node_tool("herb-lint", "@herb-tools/linter")
202
+ when "format"
203
+ run_node_tool("herb-format", "@herb-tools/formatter")
204
+ when "print"
205
+ run_node_tool("herb-print", "@herb-tools/printer")
206
+ when "highlight"
207
+ run_node_tool("herb-highlight", "@herb-tools/highlighter")
208
+ when "lsp"
209
+ run_node_tool("herb-language-server", "@herb-tools/language-server")
155
210
  when "help"
156
211
  help
157
212
  when "version"
@@ -187,12 +242,16 @@ class Herb::CLI
187
242
  self.silent = true
188
243
  end
189
244
 
190
- parser.on("-n", "--non-interactive", "Disable interactive output (progress bars, terminal clearing)") do
191
- self.no_interactive = true
245
+ parser.on("--verbose", "Show detailed per-file progress (default in CI)") do
246
+ self.verbose = true
247
+ end
248
+
249
+ parser.on("--isolate", "Fork each file into its own process for crash isolation (slower)") do
250
+ self.isolate = true
192
251
  end
193
252
 
194
- parser.on("--no-log-file", "Disable log file generation") do
195
- self.no_log_file = true
253
+ parser.on("--log-file", "Enable log file generation") do
254
+ self.log_file = true
196
255
  end
197
256
 
198
257
  parser.on("--no-timing", "Disable timing output") do
@@ -218,15 +277,100 @@ class Herb::CLI
218
277
  parser.on("--debug", "Enable debug mode with ERB expression wrapping (for compile command)") do
219
278
  self.debug = true
220
279
  end
280
+
281
+ parser.on("--strict", "Enable strict mode - report errors for omitted closing tags (for parse/compile/render commands) (default: true)") do
282
+ self.strict = true
283
+ end
284
+
285
+ parser.on("--no-strict", "Disable strict mode (for parse/compile/render commands)") do
286
+ self.strict = false
287
+ end
288
+
289
+ parser.on("--analyze", "Enable analyze mode (for parse command) (default: true)") do
290
+ self.analyze = true
291
+ end
292
+
293
+ parser.on("--no-analyze", "Disable analyze mode (for parse command)") do
294
+ self.analyze = false
295
+ end
296
+
297
+ parser.on("--track-whitespace", "Enable whitespace tracking (for parse command) (default: false)") do
298
+ self.track_whitespace = true
299
+ end
300
+
301
+ parser.on("--tool TOOL", "Show config for specific tool: linter, formatter (for config command)") do |t|
302
+ self.tool = t.to_sym
303
+ end
304
+
305
+ parser.on("--arena-stats", "Print arena memory statistics (for lex/parse/analyze commands)") do
306
+ self.arena_stats = true
307
+ end
308
+
309
+ parser.on("--leak-check", "Check for memory leaks in lex/parse/extract operations (for analyze command)") do
310
+ self.leak_check = true
311
+ end
221
312
  end
222
313
  end
223
314
 
224
315
  def options
316
+ return if ["lint", "format", "print", "highlight", "lsp"].include?(@command)
317
+
225
318
  option_parser.parse!(@args)
226
319
  end
227
320
 
228
321
  private
229
322
 
323
+ def ci?
324
+ ENV["CI"] == "true" || ENV.key?("GITHUB_ACTIONS") || ENV.key?("BUILDKITE") || ENV.key?("JENKINS_URL") || ENV.key?("CIRCLECI") || ENV.key?("TRAVIS")
325
+ end
326
+
327
+ def find_node_binary(name)
328
+ local_bin = File.join(Dir.pwd, "node_modules", ".bin", name)
329
+ return local_bin if File.executable?(local_bin)
330
+
331
+ path_result = `which #{name} 2>/dev/null`.strip
332
+ return path_result unless path_result.empty?
333
+
334
+ nil
335
+ end
336
+
337
+ def node_available?
338
+ system("which node > /dev/null 2>&1")
339
+ end
340
+
341
+ def run_node_tool(binary_name, package_name)
342
+ unless node_available?
343
+ warn "Error: Node.js is required to run 'herb #{@command}'."
344
+ warn ""
345
+ warn "Install the tool:"
346
+ warn " npm install #{package_name}"
347
+ warn " yarn add #{package_name}"
348
+ warn " pnpm add #{package_name}"
349
+ warn " bun add #{package_name}"
350
+ warn ""
351
+ warn "Or install Node.js from https://nodejs.org"
352
+ exit 1
353
+ end
354
+
355
+ remaining_args = @args[1..]
356
+ binary = find_node_binary(binary_name)
357
+ node_version = `node --version 2>/dev/null`.strip
358
+
359
+ command_parts = if binary
360
+ [binary, *remaining_args]
361
+ else
362
+ ["npx", package_name, *remaining_args]
363
+ end
364
+
365
+ escaped_command = command_parts.map { |arg| arg.include?(" ") ? "\"#{arg}\"" : arg }.join(" ")
366
+
367
+ warn "Node.js: #{node_version}"
368
+ warn "Running: #{escaped_command}"
369
+ warn ""
370
+
371
+ exec(*command_parts)
372
+ end
373
+
230
374
  def print_error_summary(errors)
231
375
  puts
232
376
  puts white("#{bold(red("Errors"))} #{dimmed("(#{errors.size} total)")}")
@@ -254,6 +398,35 @@ class Herb::CLI
254
398
  end
255
399
  end
256
400
 
401
+ def generate_report
402
+ unless @file
403
+ puts "Usage: herb report <file>"
404
+ exit(1)
405
+ end
406
+
407
+ unless File.file?(@file)
408
+ puts "File not found: #{@file}"
409
+ exit(1)
410
+ end
411
+
412
+ project = Herb::Project.new(File.dirname(@file))
413
+ project.file_paths = [File.expand_path(@file)]
414
+ project.no_log_file = true
415
+ project.no_timing = true
416
+ project.silent = true
417
+ project.validate_ruby = true
418
+
419
+ original_stdout = $stdout
420
+ $stdout = StringIO.new
421
+ begin
422
+ project.analyze!
423
+ ensure
424
+ $stdout = original_stdout
425
+ end
426
+
427
+ project.print_file_report(@file)
428
+ end
429
+
257
430
  def compile_template
258
431
  require_relative "engine"
259
432
 
@@ -262,12 +435,14 @@ class Herb::CLI
262
435
  options[:filename] = @file if @file
263
436
  options[:escape] = no_escape ? false : true
264
437
  options[:freeze] = true if freeze
438
+ options[:strict] = strict.nil? || strict
265
439
 
266
440
  if debug
267
441
  options[:debug] = true
268
442
  options[:debug_filename] = @file if @file
269
443
  end
270
444
 
445
+ options[:validate_ruby] = true
271
446
  engine = Herb::Engine.new(file_content, options)
272
447
 
273
448
  if json
@@ -276,6 +451,7 @@ class Herb::CLI
276
451
  source: engine.src,
277
452
  filename: engine.filename,
278
453
  bufvar: engine.bufvar,
454
+ strict: options[:strict],
279
455
  }
280
456
 
281
457
  puts result.to_json
@@ -286,6 +462,24 @@ class Herb::CLI
286
462
  end
287
463
 
288
464
  exit(0)
465
+ rescue Herb::Engine::InvalidRubyError => e
466
+ if json
467
+ result = {
468
+ success: false,
469
+ error: e.message,
470
+ source: e.compiled_source,
471
+ filename: @file,
472
+ }
473
+ puts result.to_json
474
+ elsif silent
475
+ puts "Failed"
476
+ else
477
+ puts e.compiled_source if e.compiled_source
478
+ puts
479
+ puts e.message
480
+ end
481
+
482
+ exit(1)
289
483
  rescue Herb::Engine::CompilationError => e
290
484
  if json
291
485
  result = {
@@ -328,6 +522,7 @@ class Herb::CLI
328
522
  options[:filename] = @file if @file
329
523
  options[:escape] = no_escape ? false : true
330
524
  options[:freeze] = true if freeze
525
+ options[:strict] = strict.nil? || strict
331
526
 
332
527
  if debug
333
528
  options[:debug] = true
@@ -344,6 +539,7 @@ class Herb::CLI
344
539
  success: true,
345
540
  output: rendered_output,
346
541
  filename: engine.filename,
542
+ strict: options[:strict],
347
543
  }
348
544
 
349
545
  puts result.to_json
@@ -388,6 +584,120 @@ class Herb::CLI
388
584
  end
389
585
  end
390
586
 
587
+ def show_config
588
+ path = @file || "."
589
+ config = Herb::Configuration.load(path)
590
+
591
+ if tool
592
+ show_tool_config(config, path)
593
+ else
594
+ show_general_config(config, path)
595
+ end
596
+ end
597
+
598
+ def show_general_config(config, path)
599
+ puts bold("Herb Configuration")
600
+ puts
601
+ puts "#{bold("Project root:")} #{config.project_root || "(not found)"}"
602
+ puts "#{bold("Config file:")} #{config.config_path || "(using defaults)"}"
603
+ puts
604
+
605
+ puts bold("Include patterns:")
606
+ config.file_include_patterns.each { |p| puts " #{green("+")} #{p}" }
607
+ puts
608
+
609
+ puts bold("Exclude patterns:")
610
+ config.file_exclude_patterns.each { |p| puts " #{red("-")} #{p}" }
611
+ puts
612
+
613
+ all_matched = find_all_matching_files(path, config.file_include_patterns)
614
+ included_files = config.find_files(path)
615
+ excluded_files = all_matched - included_files
616
+
617
+ puts bold("Files (#{included_files.size} included, #{excluded_files.size} excluded):")
618
+ puts
619
+
620
+ show_file_lists(included_files, excluded_files, path, config.file_exclude_patterns)
621
+
622
+ puts
623
+ puts dimmed("Tip: Use --tool linter or --tool formatter to see tool-specific configuration")
624
+ end
625
+
626
+ def show_tool_config(config, path)
627
+ unless [:linter, :formatter].include?(tool)
628
+ puts red("Unknown tool: #{tool}")
629
+ puts "Valid tools: linter, formatter"
630
+ exit(1)
631
+ end
632
+
633
+ tool_config = config.send(tool)
634
+ include_patterns = config.include_patterns_for(tool)
635
+ exclude_patterns = config.exclude_patterns_for(tool)
636
+
637
+ puts bold("Herb Configuration for #{tool.to_s.capitalize}")
638
+ puts
639
+ puts "#{bold("Project root:")} #{config.project_root || "(not found)"}"
640
+ puts "#{bold("Config file:")} #{config.config_path || "(using defaults)"}"
641
+ puts
642
+
643
+ if tool_config["enabled"] == false
644
+ puts yellow("⚠ #{tool.to_s.capitalize} is disabled in configuration")
645
+ puts
646
+ end
647
+
648
+ puts bold("Include patterns (files + #{tool}):")
649
+ include_patterns.each { |p| puts " #{green("+")} #{p}" }
650
+ puts
651
+
652
+ puts bold("Exclude patterns (files + #{tool}):")
653
+ exclude_patterns.each { |p| puts " #{red("-")} #{p}" }
654
+ puts
655
+
656
+ all_matched = find_all_matching_files(path, include_patterns)
657
+ included_files = config.find_files_for_tool(tool, path)
658
+ excluded_files = all_matched - included_files
659
+
660
+ puts bold("Files for #{tool} (#{included_files.size} included, #{excluded_files.size} excluded):")
661
+ puts
662
+
663
+ show_file_lists(included_files, excluded_files, path, exclude_patterns)
664
+ end
665
+
666
+ def show_file_lists(included_files, excluded_files, path, exclude_patterns)
667
+ expanded_path = File.expand_path(path)
668
+
669
+ if included_files.any?
670
+ puts " #{bold(green("Included:"))}"
671
+ included_files.each do |f|
672
+ relative = f.sub("#{expanded_path}/", "")
673
+ puts " #{green("✓")} #{relative}"
674
+ end
675
+ puts
676
+ end
677
+
678
+ return unless excluded_files.any?
679
+
680
+ puts " #{bold(red("Excluded:"))}"
681
+ excluded_files.each do |f|
682
+ relative = f.sub("#{expanded_path}/", "")
683
+ reason = find_exclude_reason(relative, exclude_patterns)
684
+ puts " #{red("✗")} #{relative} #{dimmed("(#{reason})")}"
685
+ end
686
+ end
687
+
688
+ def find_all_matching_files(path, include_patterns)
689
+ expanded_path = File.expand_path(path)
690
+ include_patterns.flat_map do |pattern|
691
+ Dir[File.join(expanded_path, pattern)]
692
+ end.uniq
693
+ end
694
+
695
+ def find_exclude_reason(relative_path, exclude_patterns)
696
+ exclude_patterns.find do |pattern|
697
+ File.fnmatch?(pattern, relative_path, File::FNM_PATHNAME)
698
+ end || "excluded"
699
+ end
700
+
391
701
  def print_version
392
702
  puts Herb.version
393
703
  exit(0)