herb 0.6.1-x86-linux-musl → 0.7.0-x86-linux-musl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/ext/herb/nodes.c +6 -4
- data/lib/herb/3.0/herb.so +0 -0
- data/lib/herb/3.1/herb.so +0 -0
- data/lib/herb/3.2/herb.so +0 -0
- data/lib/herb/3.3/herb.so +0 -0
- data/lib/herb/3.4/herb.so +0 -0
- data/lib/herb/ast/helpers.rb +26 -0
- data/lib/herb/ast/nodes.rb +7 -3
- data/lib/herb/cli.rb +158 -1
- data/lib/herb/engine/compiler.rb +399 -0
- data/lib/herb/engine/debug_visitor.rb +321 -0
- data/lib/herb/engine/error_formatter.rb +420 -0
- data/lib/herb/engine/parser_error_overlay.rb +767 -0
- data/lib/herb/engine/validation_error_overlay.rb +182 -0
- data/lib/herb/engine/validation_errors.rb +65 -0
- data/lib/herb/engine/validator.rb +75 -0
- data/lib/herb/engine/validators/accessibility_validator.rb +31 -0
- data/lib/herb/engine/validators/nesting_validator.rb +95 -0
- data/lib/herb/engine/validators/security_validator.rb +71 -0
- data/lib/herb/engine.rb +366 -0
- data/lib/herb/project.rb +3 -3
- data/lib/herb/version.rb +1 -1
- data/lib/herb/visitor.rb +2 -0
- data/lib/herb.rb +2 -0
- data/sig/herb/ast/helpers.rbs +16 -0
- data/sig/herb/ast/nodes.rbs +4 -2
- data/sig/herb/engine/compiler.rbs +109 -0
- data/sig/herb/engine/debug.rbs +38 -0
- data/sig/herb/engine/debug_visitor.rbs +70 -0
- data/sig/herb/engine/error_formatter.rbs +47 -0
- data/sig/herb/engine/parser_error_overlay.rbs +41 -0
- data/sig/herb/engine/validation_error_overlay.rbs +35 -0
- data/sig/herb/engine/validation_errors.rbs +45 -0
- data/sig/herb/engine/validator.rbs +37 -0
- data/sig/herb/engine/validators/accessibility_validator.rbs +19 -0
- data/sig/herb/engine/validators/nesting_validator.rbs +25 -0
- data/sig/herb/engine/validators/security_validator.rbs +23 -0
- data/sig/herb/engine.rbs +72 -0
- data/sig/herb/visitor.rbs +2 -0
- data/sig/herb_c_extension.rbs +7 -0
- data/sig/serialized_ast_nodes.rbs +1 -0
- data/src/ast_nodes.c +2 -1
- data/src/ast_pretty_print.c +2 -1
- data/src/element_source.c +11 -0
- data/src/include/ast_nodes.h +3 -1
- data/src/include/element_source.h +13 -0
- data/src/include/version.h +1 -1
- data/src/parser.c +3 -0
- data/src/parser_helpers.c +1 -0
- metadata +30 -2
@@ -0,0 +1,420 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require "tempfile"
|
5
|
+
|
6
|
+
module Herb
|
7
|
+
class Engine
|
8
|
+
class ErrorFormatter
|
9
|
+
CONTEXT_LINES = 3
|
10
|
+
|
11
|
+
def initialize(source, errors, options = {})
|
12
|
+
@source = source
|
13
|
+
@errors = errors
|
14
|
+
@filename = options[:filename] || "[source]"
|
15
|
+
@lines = source.lines
|
16
|
+
@use_highlighter = options.fetch(:use_highlighter, true)
|
17
|
+
@highlighter_path = options[:highlighter_path] || find_highlighter_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_all
|
21
|
+
return "No errors found" if @errors.empty?
|
22
|
+
|
23
|
+
if @use_highlighter && @highlighter_path && can_use_highlighter?
|
24
|
+
format_all_with_highlighter
|
25
|
+
else
|
26
|
+
format_all_without_highlighter
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def format_all_with_highlighter
|
31
|
+
output = String.new
|
32
|
+
output << "HTML+ERB Compilation Errors:\n"
|
33
|
+
output << ("=" * 60) << "\n\n"
|
34
|
+
|
35
|
+
require "tempfile"
|
36
|
+
temp_file = Tempfile.new(["herb_error", ".html.erb"])
|
37
|
+
temp_file.write(@source)
|
38
|
+
temp_file.close
|
39
|
+
|
40
|
+
begin
|
41
|
+
highlighted_output = run_highlighter_with_diagnostics(temp_file.path, CONTEXT_LINES)
|
42
|
+
|
43
|
+
if highlighted_output
|
44
|
+
output << highlighted_output
|
45
|
+
else
|
46
|
+
errors_by_line = @errors.group_by do |error|
|
47
|
+
location = error.is_a?(Hash) ? error[:location] : error.location
|
48
|
+
location&.start&.line
|
49
|
+
end.compact
|
50
|
+
|
51
|
+
errors_by_line.each_with_index do |(line_num, line_errors), group_index|
|
52
|
+
output << "Error Group ##{group_index + 1} (Line #{line_num}):\n"
|
53
|
+
output << ("-" * 40) << "\n"
|
54
|
+
|
55
|
+
line_errors.each_with_index do |error, index|
|
56
|
+
output << format_error_header(error, index + 1)
|
57
|
+
end
|
58
|
+
|
59
|
+
output << "\nSource Context:\n"
|
60
|
+
|
61
|
+
highlighted_basic = run_highlighter(temp_file.path, line_num, CONTEXT_LINES)
|
62
|
+
|
63
|
+
output << (highlighted_basic || format_source_context_basic(line_errors.first))
|
64
|
+
|
65
|
+
output << "\n"
|
66
|
+
output << format_suggestions(line_errors)
|
67
|
+
output << "\n" unless group_index == errors_by_line.length - 1
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
output << "\n" << ("=" * 60) << "\n"
|
72
|
+
output << "Total errors: #{@errors.length}\n"
|
73
|
+
output << "Compilation failed. Please fix the errors above.\n"
|
74
|
+
ensure
|
75
|
+
temp_file.unlink
|
76
|
+
end
|
77
|
+
|
78
|
+
output
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_all_without_highlighter
|
82
|
+
output = String.new
|
83
|
+
output << "HTML+ERB Compilation Errors:\n"
|
84
|
+
output << ("=" * 60) << "\n\n"
|
85
|
+
|
86
|
+
@errors.each_with_index do |error, index|
|
87
|
+
output << format_error(error, index + 1)
|
88
|
+
output << "\n" unless index == @errors.length - 1
|
89
|
+
end
|
90
|
+
|
91
|
+
output << "\n" << ("=" * 60) << "\n"
|
92
|
+
output << "Total errors: #{@errors.length}\n"
|
93
|
+
output << "Compilation failed. Please fix the errors above.\n"
|
94
|
+
|
95
|
+
output
|
96
|
+
end
|
97
|
+
|
98
|
+
def format_error(error, number)
|
99
|
+
output = String.new
|
100
|
+
|
101
|
+
output << "Error ##{number}: #{error.class.name.split("::").last.gsub(/Error$/, "")}\n"
|
102
|
+
output << ("-" * 40) << "\n"
|
103
|
+
|
104
|
+
if error.location
|
105
|
+
output << " File: #{@filename}\n"
|
106
|
+
output << " Location: Line #{error.location.start.line}, Column #{error.location.start.column}\n"
|
107
|
+
end
|
108
|
+
|
109
|
+
output << " Message: #{error.message}\n\n"
|
110
|
+
output << format_source_context(error) if error.location
|
111
|
+
output << format_error_details(error)
|
112
|
+
|
113
|
+
output
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def format_source_context(error)
|
119
|
+
output = String.new
|
120
|
+
location = error.is_a?(Hash) ? error[:location] : error.location
|
121
|
+
line_num = location.start.line
|
122
|
+
col_num = location.start.column
|
123
|
+
|
124
|
+
start_line = [line_num - CONTEXT_LINES, 1].max
|
125
|
+
end_line = [line_num + CONTEXT_LINES, @lines.length].min
|
126
|
+
|
127
|
+
output << " Source:\n"
|
128
|
+
|
129
|
+
(start_line..end_line).each do |i|
|
130
|
+
line = @lines[i - 1]
|
131
|
+
line_str = line.chomp
|
132
|
+
line_prefix = format(" %4d | ", i)
|
133
|
+
|
134
|
+
if i == line_num
|
135
|
+
output << "\e[31m"
|
136
|
+
output << line_prefix
|
137
|
+
output << line_str
|
138
|
+
output << "\e[0m\n"
|
139
|
+
|
140
|
+
if col_num.positive?
|
141
|
+
pointer = "#{" " * (line_prefix.length + col_num - 1)}^"
|
142
|
+
|
143
|
+
if location.end.column && location.end.column > col_num
|
144
|
+
underline_length = location.end.column - col_num
|
145
|
+
pointer << ("~" * [underline_length - 1, 0].max)
|
146
|
+
end
|
147
|
+
|
148
|
+
output << "\e[31m#{pointer}\e[0m"
|
149
|
+
|
150
|
+
output << " #{format_inline_hint(error)}" if inline_hint?(error)
|
151
|
+
output << "\n"
|
152
|
+
end
|
153
|
+
else
|
154
|
+
output << "\e[90m"
|
155
|
+
output << line_prefix
|
156
|
+
output << line_str
|
157
|
+
output << "\e[0m\n"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
output << "\n"
|
162
|
+
output
|
163
|
+
end
|
164
|
+
|
165
|
+
def format_error_details(error)
|
166
|
+
output = String.new
|
167
|
+
|
168
|
+
case error
|
169
|
+
when Herb::Errors::MissingClosingTagError
|
170
|
+
if error.opening_tag
|
171
|
+
output << " Opening tag: <#{error.opening_tag.value}> at line #{error.opening_tag.location.start.line}\n"
|
172
|
+
output << " Expected: </#{error.opening_tag.value}>\n"
|
173
|
+
output << " Suggestion: Add the closing tag or use a self-closing tag\n"
|
174
|
+
end
|
175
|
+
|
176
|
+
when Herb::Errors::MissingOpeningTagError
|
177
|
+
if error.closing_tag
|
178
|
+
output << " Closing tag: </#{error.closing_tag.value}> at line #{error.closing_tag.location.start.line}\n"
|
179
|
+
output << " Suggestion: Add the corresponding opening tag or remove this closing tag\n"
|
180
|
+
end
|
181
|
+
|
182
|
+
when Herb::Errors::TagNamesMismatchError
|
183
|
+
if error.opening_tag && error.closing_tag
|
184
|
+
output << " Opening tag: <#{error.opening_tag.value}> at line #{error.opening_tag.location.start.line}\n"
|
185
|
+
output << " Closing tag: </#{error.closing_tag.value}> at line #{error.closing_tag.location.start.line}\n"
|
186
|
+
output << " Suggestion: Change the closing tag to </#{error.opening_tag.value}>\n"
|
187
|
+
end
|
188
|
+
|
189
|
+
when Herb::Errors::VoidElementClosingTagError
|
190
|
+
if error.tag_name
|
191
|
+
output << " Void element: <#{error.tag_name.value}>\n"
|
192
|
+
output << " Note: Void elements like <br>, <img>, <input> cannot have closing tags\n"
|
193
|
+
output << " Suggestion: Remove the closing tag or use <#{error.tag_name.value} />\n"
|
194
|
+
end
|
195
|
+
|
196
|
+
when Herb::Errors::UnclosedElementError
|
197
|
+
if error.opening_tag
|
198
|
+
output << " Opening tag: <#{error.opening_tag.value}> at line #{error.opening_tag.location.start.line}\n"
|
199
|
+
output << " Note: This element was never closed before the end of the document\n"
|
200
|
+
output << " Suggestion: Add </#{error.opening_tag.value}> before the end of the template\n"
|
201
|
+
end
|
202
|
+
|
203
|
+
when Herb::Errors::RubyParseError
|
204
|
+
output << " Ruby error: #{error.diagnostic_id}\n"
|
205
|
+
output << " Level: #{error.level}\n"
|
206
|
+
output << " Details: #{error.error_message}\n"
|
207
|
+
output << " Suggestion: Check your Ruby syntax inside the ERB tag\n"
|
208
|
+
|
209
|
+
when Herb::Errors::QuotesMismatchError
|
210
|
+
if error.opening_quote && error.closing_quote
|
211
|
+
output << " Opening quote: #{error.opening_quote.value}\n"
|
212
|
+
output << " Closing quote: #{error.closing_quote.value}\n"
|
213
|
+
output << " Suggestion: Use matching quotes for attribute values\n"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
output
|
218
|
+
end
|
219
|
+
|
220
|
+
def inline_hint?(error)
|
221
|
+
case error
|
222
|
+
when Herb::Errors::MissingClosingTagError,
|
223
|
+
Herb::Errors::TagNamesMismatchError,
|
224
|
+
Herb::Errors::UnclosedElementError
|
225
|
+
true
|
226
|
+
else
|
227
|
+
false
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def format_inline_hint(error)
|
232
|
+
case error
|
233
|
+
when Herb::Errors::MissingClosingTagError
|
234
|
+
"← Missing closing tag"
|
235
|
+
when Herb::Errors::TagNamesMismatchError
|
236
|
+
"← Tag mismatch"
|
237
|
+
when Herb::Errors::UnclosedElementError
|
238
|
+
"← Unclosed element"
|
239
|
+
else
|
240
|
+
""
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def find_highlighter_path
|
245
|
+
possible_paths = [
|
246
|
+
File.expand_path("../../../javascript/packages/highlighter/bin/herb-highlight", __dir__ || "."),
|
247
|
+
"herb-highlight" # In PATH
|
248
|
+
]
|
249
|
+
|
250
|
+
possible_paths.find { |path| File.executable?(path) || system("which #{path} > /dev/null 2>&1") }
|
251
|
+
end
|
252
|
+
|
253
|
+
def can_use_highlighter?
|
254
|
+
return false unless @highlighter_path
|
255
|
+
return false unless File.exist?(@highlighter_path) || system("which #{@highlighter_path} > /dev/null 2>&1")
|
256
|
+
|
257
|
+
system("node --version > /dev/null 2>&1")
|
258
|
+
end
|
259
|
+
|
260
|
+
def run_highlighter(file_path, line_num, context_lines)
|
261
|
+
return nil unless @highlighter_path && can_use_highlighter?
|
262
|
+
|
263
|
+
cmd = "#{@highlighter_path} --focus #{line_num} --context-lines #{context_lines} \"#{file_path}\""
|
264
|
+
|
265
|
+
begin
|
266
|
+
output = `#{cmd} 2>/dev/null`
|
267
|
+
status = $CHILD_STATUS
|
268
|
+
return output.gsub(file_path, @filename) if status&.success? && !output.strip.empty?
|
269
|
+
rescue StandardError
|
270
|
+
# Silently fall back to basic formatting if highlighter fails
|
271
|
+
end
|
272
|
+
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
|
276
|
+
def run_highlighter_with_diagnostics(file_path, context_lines = 2)
|
277
|
+
return nil unless @highlighter_path && can_use_highlighter?
|
278
|
+
|
279
|
+
diagnostics = @errors.map { |error| herb_error_to_diagnostic(error) }
|
280
|
+
|
281
|
+
require "tempfile"
|
282
|
+
require "json"
|
283
|
+
|
284
|
+
diagnostics_file = Tempfile.new(["herb_diagnostics", ".json"])
|
285
|
+
diagnostics_file.write(JSON.pretty_generate(diagnostics))
|
286
|
+
diagnostics_file.close
|
287
|
+
|
288
|
+
begin
|
289
|
+
cmd = "#{@highlighter_path} --diagnostics \"#{diagnostics_file.path}\" --split-diagnostics --context-lines #{context_lines} \"#{file_path}\""
|
290
|
+
|
291
|
+
output = `#{cmd} 2>/dev/null`
|
292
|
+
status = $CHILD_STATUS
|
293
|
+
|
294
|
+
return output.gsub(file_path, @filename) if status&.success? && !output.strip.empty?
|
295
|
+
rescue StandardError
|
296
|
+
# Silently fall back to basic formatting if highlighter fails
|
297
|
+
ensure
|
298
|
+
diagnostics_file.unlink
|
299
|
+
end
|
300
|
+
|
301
|
+
nil
|
302
|
+
end
|
303
|
+
|
304
|
+
def herb_error_to_diagnostic(error)
|
305
|
+
if error.is_a?(Hash)
|
306
|
+
location = error[:location]
|
307
|
+
{
|
308
|
+
message: error[:message],
|
309
|
+
location: {
|
310
|
+
start: {
|
311
|
+
line: location&.start&.line || 1,
|
312
|
+
column: location&.start&.column || 1,
|
313
|
+
},
|
314
|
+
end: {
|
315
|
+
line: location&.end&.line || location&.start&.line || 1,
|
316
|
+
column: location&.end&.column || location&.start&.column || 1,
|
317
|
+
},
|
318
|
+
},
|
319
|
+
severity: error[:severity] || "error",
|
320
|
+
code: error[:code] || "UnknownError",
|
321
|
+
source: error[:source] || "herb-validator",
|
322
|
+
}
|
323
|
+
else
|
324
|
+
severity = case error
|
325
|
+
when Herb::Errors::RubyParseError
|
326
|
+
error.level == "error" ? "error" : "warning"
|
327
|
+
else
|
328
|
+
"error"
|
329
|
+
end
|
330
|
+
|
331
|
+
{
|
332
|
+
message: error.message,
|
333
|
+
location: {
|
334
|
+
start: {
|
335
|
+
line: error.location&.start&.line || 1,
|
336
|
+
column: error.location&.start&.column || 1,
|
337
|
+
},
|
338
|
+
end: {
|
339
|
+
line: error.location&.end&.line || error.location&.start&.line || 1,
|
340
|
+
column: error.location&.end&.column || error.location&.start&.column || 1,
|
341
|
+
},
|
342
|
+
},
|
343
|
+
severity: severity,
|
344
|
+
code: error.class.name.split("::").last.gsub(/Error$/, ""),
|
345
|
+
source: "herb-compiler",
|
346
|
+
}
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def format_error_header(error, number)
|
351
|
+
output = String.new
|
352
|
+
output << if error.is_a?(Hash)
|
353
|
+
" #{number}. #{error[:code] || "UnknownError"}: #{error[:message]}\n"
|
354
|
+
else
|
355
|
+
" #{number}. #{error.class.name.split("::").last.gsub(/Error$/, "")}: #{error.message}\n"
|
356
|
+
end
|
357
|
+
|
358
|
+
location = error.is_a?(Hash) ? error[:location] : error.location
|
359
|
+
output << " Location: Line #{location.start.line}, Column #{location.start.column}\n" if location
|
360
|
+
|
361
|
+
output
|
362
|
+
end
|
363
|
+
|
364
|
+
def format_suggestions(errors)
|
365
|
+
output = String.new
|
366
|
+
output << "Suggestions:\n"
|
367
|
+
|
368
|
+
errors.each do |error|
|
369
|
+
suggestion = get_error_suggestion(error)
|
370
|
+
output << " • #{suggestion}\n" if suggestion
|
371
|
+
end
|
372
|
+
|
373
|
+
output
|
374
|
+
end
|
375
|
+
|
376
|
+
def format_source_context_basic(error)
|
377
|
+
format_source_context(error)
|
378
|
+
end
|
379
|
+
|
380
|
+
def get_error_suggestion(error)
|
381
|
+
case error
|
382
|
+
when Herb::Errors::MissingClosingTagError
|
383
|
+
if error.opening_tag
|
384
|
+
"Add </#{error.opening_tag.value}> to close the opening tag"
|
385
|
+
else
|
386
|
+
"Add the missing closing tag"
|
387
|
+
end
|
388
|
+
when Herb::Errors::MissingOpeningTagError
|
389
|
+
if error.closing_tag
|
390
|
+
"Add <#{error.closing_tag.value}> before the closing tag"
|
391
|
+
else
|
392
|
+
"Add the missing opening tag"
|
393
|
+
end
|
394
|
+
when Herb::Errors::TagNamesMismatchError
|
395
|
+
if error.opening_tag && error.closing_tag
|
396
|
+
"Change </#{error.closing_tag.value}> to </#{error.opening_tag.value}>"
|
397
|
+
else
|
398
|
+
"Fix the tag name mismatch"
|
399
|
+
end
|
400
|
+
when Herb::Errors::VoidElementClosingTagError
|
401
|
+
if error.tag_name
|
402
|
+
"Remove the closing tag for void element <#{error.tag_name.value}>"
|
403
|
+
else
|
404
|
+
"Remove the closing tag for this void element"
|
405
|
+
end
|
406
|
+
when Herb::Errors::UnclosedElementError
|
407
|
+
if error.opening_tag
|
408
|
+
"Add </#{error.opening_tag.value}> before the end of the template"
|
409
|
+
else
|
410
|
+
"Close the unclosed element"
|
411
|
+
end
|
412
|
+
when Herb::Errors::RubyParseError
|
413
|
+
"Check your Ruby syntax inside the ERB tag"
|
414
|
+
when Herb::Errors::QuotesMismatchError
|
415
|
+
"Use matching quotes for attribute values"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|