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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/ext/herb/nodes.c +6 -4
  4. data/lib/herb/3.0/herb.so +0 -0
  5. data/lib/herb/3.1/herb.so +0 -0
  6. data/lib/herb/3.2/herb.so +0 -0
  7. data/lib/herb/3.3/herb.so +0 -0
  8. data/lib/herb/3.4/herb.so +0 -0
  9. data/lib/herb/ast/helpers.rb +26 -0
  10. data/lib/herb/ast/nodes.rb +7 -3
  11. data/lib/herb/cli.rb +158 -1
  12. data/lib/herb/engine/compiler.rb +399 -0
  13. data/lib/herb/engine/debug_visitor.rb +321 -0
  14. data/lib/herb/engine/error_formatter.rb +420 -0
  15. data/lib/herb/engine/parser_error_overlay.rb +767 -0
  16. data/lib/herb/engine/validation_error_overlay.rb +182 -0
  17. data/lib/herb/engine/validation_errors.rb +65 -0
  18. data/lib/herb/engine/validator.rb +75 -0
  19. data/lib/herb/engine/validators/accessibility_validator.rb +31 -0
  20. data/lib/herb/engine/validators/nesting_validator.rb +95 -0
  21. data/lib/herb/engine/validators/security_validator.rb +71 -0
  22. data/lib/herb/engine.rb +366 -0
  23. data/lib/herb/project.rb +3 -3
  24. data/lib/herb/version.rb +1 -1
  25. data/lib/herb/visitor.rb +2 -0
  26. data/lib/herb.rb +2 -0
  27. data/sig/herb/ast/helpers.rbs +16 -0
  28. data/sig/herb/ast/nodes.rbs +4 -2
  29. data/sig/herb/engine/compiler.rbs +109 -0
  30. data/sig/herb/engine/debug.rbs +38 -0
  31. data/sig/herb/engine/debug_visitor.rbs +70 -0
  32. data/sig/herb/engine/error_formatter.rbs +47 -0
  33. data/sig/herb/engine/parser_error_overlay.rbs +41 -0
  34. data/sig/herb/engine/validation_error_overlay.rbs +35 -0
  35. data/sig/herb/engine/validation_errors.rbs +45 -0
  36. data/sig/herb/engine/validator.rbs +37 -0
  37. data/sig/herb/engine/validators/accessibility_validator.rbs +19 -0
  38. data/sig/herb/engine/validators/nesting_validator.rbs +25 -0
  39. data/sig/herb/engine/validators/security_validator.rbs +23 -0
  40. data/sig/herb/engine.rbs +72 -0
  41. data/sig/herb/visitor.rbs +2 -0
  42. data/sig/herb_c_extension.rbs +7 -0
  43. data/sig/serialized_ast_nodes.rbs +1 -0
  44. data/src/ast_nodes.c +2 -1
  45. data/src/ast_pretty_print.c +2 -1
  46. data/src/element_source.c +11 -0
  47. data/src/include/ast_nodes.h +3 -1
  48. data/src/include/element_source.h +13 -0
  49. data/src/include/version.h +1 -1
  50. data/src/parser.c +3 -0
  51. data/src/parser_helpers.c +1 -0
  52. metadata +30 -2
@@ -0,0 +1,767 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herb
4
+ class Engine
5
+ class ParserErrorOverlay
6
+ CONTEXT_LINES = 3
7
+
8
+ ERROR_CLASS_PRIORITRY = [
9
+ Herb::Errors::UnexpectedTokenError,
10
+ Herb::Errors::UnexpectedError,
11
+ Herb::Errors::RubyParseError,
12
+ Herb::Errors::QuotesMismatchError,
13
+ Herb::Errors::TagNamesMismatchError,
14
+ Herb::Errors::VoidElementClosingTagError,
15
+ Herb::Errors::UnclosedElementError,
16
+ Herb::Errors::MissingClosingTagError,
17
+ Herb::Errors::MissingOpeningTagError
18
+ ].freeze
19
+
20
+ def initialize(source, errors, filename: nil)
21
+ @source = source
22
+ @errors = errors.sort_by { |error|
23
+ [ERROR_CLASS_PRIORITRY.index(error.class) || -1, error.location.start.line, error.location.start.column]
24
+ }
25
+ @filename = filename || "unknown"
26
+ @lines = source.lines
27
+ end
28
+
29
+ def generate_html
30
+ return "" if @errors.empty?
31
+
32
+ error_count = @errors.length
33
+ error_title = error_count == 1 ? "Template Error" : "Template Errors (#{error_count})"
34
+
35
+ primary_error = @errors.first
36
+ error_message = primary_error.respond_to?(:message) ? primary_error.message : primary_error.to_s
37
+
38
+ <<~HTML
39
+ <div class="herb-parser-error-overlay">
40
+ <style>
41
+ .herb-parser-error-overlay * {
42
+ margin: 0;
43
+ padding: 0;
44
+ box-sizing: border-box;
45
+ }
46
+
47
+ .herb-parser-error-overlay {
48
+ position: fixed;
49
+ top: 0;
50
+ left: 0;
51
+ right: 0;
52
+ bottom: 0;
53
+ background: rgba(0, 0, 0, 0.8);
54
+ backdrop-filter: blur(4px);
55
+ z-index: 9999;
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ padding: 20px;
60
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
61
+ color: #e5e5e5;
62
+ line-height: 1.6;
63
+ }
64
+
65
+ .herb-parser-error-overlay .herb-error-container {
66
+ background: #000000;
67
+ border: 1px solid #374151;
68
+ border-radius: 12px;
69
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
70
+ max-width: 1200px;
71
+ max-height: 80vh;
72
+ width: 100%;
73
+ display: flex;
74
+ flex-direction: column;
75
+ overflow: hidden;
76
+ }
77
+
78
+ .herb-parser-error-overlay .herb-error-header {
79
+ background: linear-gradient(135deg, #dc2626, #b91c1c);
80
+ padding: 20px 24px;
81
+ border-bottom: 1px solid #374151;
82
+ border-radius: 12px 12px 0 0;
83
+ flex-shrink: 0;
84
+ display: flex;
85
+ justify-content: space-between;
86
+ align-items: flex-start;
87
+ gap: 16px;
88
+ }
89
+
90
+ .herb-parser-error-overlay .herb-error-header-content {
91
+ flex: 1;
92
+ min-width: 0;
93
+ }
94
+
95
+ .herb-parser-error-overlay .herb-error-title {
96
+ font-size: 18px;
97
+ font-weight: 600;
98
+ color: white;
99
+ margin-bottom: 8px;
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 12px;
103
+ }
104
+
105
+ .herb-parser-error-overlay .herb-error-icon {
106
+ width: 20px;
107
+ height: 20px;
108
+ background: white;
109
+ border-radius: 50%;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ font-size: 12px;
114
+ color: #dc2626;
115
+ font-weight: bold;
116
+ }
117
+
118
+ .herb-parser-error-overlay .herb-error-message {
119
+ font-size: 14px;
120
+ color: rgba(255, 255, 255, 0.9);
121
+ font-weight: 400;
122
+ }
123
+
124
+ .herb-parser-error-overlay .herb-error-message-section {
125
+ background: #1a1a1a;
126
+ border-bottom: 1px solid #374151;
127
+ padding: 12px 16px;
128
+ }
129
+
130
+ .herb-parser-error-overlay .herb-error-message-section .herb-error-message {
131
+ font-size: 13px;
132
+ color: #fbbf24;
133
+ font-weight: 500;
134
+ line-height: 1.4;
135
+ }
136
+
137
+ .herb-parser-error-overlay .herb-error-content {
138
+ flex: 1;
139
+ overflow-y: auto;
140
+ padding: 24px;
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: 24px;
144
+ }
145
+
146
+ .herb-parser-error-overlay .herb-code-section {
147
+ background: #111111;
148
+ border: 1px solid #374151;
149
+ border-radius: 8px;
150
+ }
151
+
152
+ .herb-parser-error-overlay .herb-code-header {
153
+ background: #262626;
154
+ padding: 12px 16px;
155
+ border-bottom: 1px solid #374151;
156
+ font-size: 13px;
157
+ color: #9ca3af;
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 8px;
161
+ }
162
+
163
+ .herb-parser-error-overlay .herb-file-icon {
164
+ width: 20px;
165
+ height: 20px;
166
+ background: #008000;
167
+ border-radius: 2px;
168
+ display: flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ font-size: 8px;
172
+ color: white;
173
+ padding: 2px;
174
+ }
175
+
176
+ .herb-parser-error-overlay .herb-code-content {
177
+ padding: 16px;
178
+ overflow-x: auto;
179
+ }
180
+
181
+ .herb-parser-error-overlay .herb-code-line {
182
+ display: flex;
183
+ align-items: flex-start;
184
+ min-height: 20px;
185
+ font-size: 13px;
186
+ line-height: 1.5;
187
+ }
188
+
189
+ .herb-parser-error-overlay .herb-line-number {
190
+ color: #6b7280;
191
+ width: 40px;
192
+ text-align: right;
193
+ padding-right: 16px;
194
+ user-select: none;
195
+ flex-shrink: 0;
196
+ }
197
+
198
+ .herb-parser-error-overlay .herb-line-content {
199
+ flex: 1;
200
+ white-space: pre;
201
+ font-family: inherit;
202
+ }
203
+
204
+ .herb-parser-error-overlay .herb-error-line {
205
+ background: rgba(220, 38, 38, 0.1);
206
+ border-left: 3px solid #dc2626;
207
+ margin: 0 -16px;
208
+ padding: 0 16px;
209
+ }
210
+
211
+ .herb-parser-error-overlay .herb-error-line .herb-line-number {
212
+ color: #dc2626;
213
+ font-weight: 600;
214
+ }
215
+
216
+ .herb-parser-error-overlay .herb-error-line .herb-line-content {
217
+ color: #fecaca;
218
+ }
219
+
220
+ .herb-parser-error-overlay .herb-error-pointer {
221
+ color: #dc2626;
222
+ font-weight: bold;
223
+ margin-left: 40px;
224
+ padding-left: 16px;
225
+ font-size: 12px;
226
+ }
227
+
228
+ .herb-parser-error-overlay .herb-section {
229
+ background: #111111;
230
+ border: 1px solid #374151;
231
+ border-radius: 8px;
232
+ overflow: hidden;
233
+ }
234
+
235
+ .herb-parser-error-overlay .herb-section-header {
236
+ background: #262626;
237
+ padding: 12px 16px;
238
+ border-bottom: 1px solid #374151;
239
+ cursor: pointer;
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: space-between;
243
+ font-size: 14px;
244
+ font-weight: 500;
245
+ color: #e5e5e5;
246
+ transition: background-color 0.2s;
247
+ }
248
+
249
+ .herb-parser-error-overlay .herb-section-header:hover {
250
+ background: #2d2d2d;
251
+ }
252
+
253
+ .herb-parser-error-overlay .herb-section-toggle {
254
+ transition: transform 0.2s;
255
+ }
256
+
257
+ .herb-parser-error-overlay .herb-section-toggle.collapsed {
258
+ transform: rotate(-90deg);
259
+ }
260
+
261
+ .herb-parser-error-overlay .herb-section-content {
262
+ padding: 16px;
263
+ font-size: 13px;
264
+ line-height: 1.6;
265
+ max-height: 300px;
266
+ overflow-y: auto;
267
+ }
268
+
269
+ .herb-parser-error-overlay .herb-section-content.collapsed {
270
+ display: none;
271
+ }
272
+
273
+ .herb-parser-error-overlay .herb-suggestions {
274
+ color: #d1d5db;
275
+ }
276
+
277
+ .herb-parser-error-overlay .herb-suggestion-item {
278
+ margin-bottom: 12px;
279
+ padding-left: 16px;
280
+ position: relative;
281
+ }
282
+
283
+ .herb-parser-error-overlay .herb-suggestion-item::before {
284
+ content: "•";
285
+ color: #10b981;
286
+ position: absolute;
287
+ left: 0;
288
+ font-weight: bold;
289
+ }
290
+
291
+ .herb-parser-error-overlay .herb-suggestion-title {
292
+ color: #10b981;
293
+ font-weight: 500;
294
+ margin-bottom: 4px;
295
+ }
296
+
297
+ /* Syntax highlighting */
298
+ .herb-parser-error-overlay .herb-keyword { color: #c678dd; }
299
+ .herb-parser-error-overlay .herb-string { color: #98c379; }
300
+ .herb-parser-error-overlay .herb-comment { color: #5c6370; font-style: italic; }
301
+ .herb-parser-error-overlay .herb-tag { color: #e06c75; }
302
+ .herb-parser-error-overlay .herb-attr { color: #d19a66; }
303
+ .herb-parser-error-overlay .herb-value { color: #98c379; }
304
+ .herb-parser-error-overlay .herb-erb {
305
+ color: #61dafb;
306
+ background: rgba(97, 218, 251, 0.1);
307
+ padding: 2px 4px;
308
+ border-radius: 3px;
309
+ }
310
+
311
+ @media (max-width: 768px) {
312
+ .herb-parser-error-overlay {
313
+ padding: 10px;
314
+ }
315
+
316
+ .herb-parser-error-overlay .herb-error-container {
317
+ max-height: 90vh;
318
+ }
319
+
320
+ .herb-parser-error-overlay .herb-error-content {
321
+ padding: 16px;
322
+ gap: 16px;
323
+ }
324
+
325
+ .herb-parser-error-overlay .herb-error-header {
326
+ padding: 16px;
327
+ flex-direction: column;
328
+ align-items: flex-start;
329
+ gap: 12px;
330
+ }
331
+ }
332
+ </style>
333
+
334
+ <div class="herb-error-container">
335
+ <div class="herb-error-header">
336
+ <div class="herb-error-header-content">
337
+ <div class="herb-error-title">
338
+ <div class="herb-error-icon">!</div>
339
+ #{escape_html(error_title)}
340
+ </div>
341
+ <div class="herb-error-message">
342
+ #{escape_html(error_message)}
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <div class="herb-error-content">
348
+ #{generate_error_sections}
349
+ </div>
350
+ </div>
351
+
352
+ <script>
353
+ (function() {
354
+ function toggleSection(sectionId) {
355
+ const content = document.getElementById(sectionId + '-content');
356
+ const toggle = document.getElementById(sectionId + '-toggle');
357
+
358
+ if (content && toggle) {
359
+ if (content.classList.contains('collapsed')) {
360
+ content.classList.remove('collapsed');
361
+ toggle.classList.remove('collapsed');
362
+ toggle.textContent = '▼';
363
+ } else {
364
+ content.classList.add('collapsed');
365
+ toggle.classList.add('collapsed');
366
+ toggle.textContent = '▶';
367
+ }
368
+ }
369
+ }
370
+
371
+ function closeErrorOverlay() {
372
+ const overlay = document.querySelector('.herb-parser-error-overlay');
373
+ if (overlay) overlay.style.display = 'none';
374
+ }
375
+
376
+ // Setup close button
377
+ const closeBtn = document.querySelector('.herb-close-button');
378
+ if (closeBtn) {
379
+ closeBtn.addEventListener('click', closeErrorOverlay);
380
+ }
381
+
382
+ // Close when clicking on backdrop (outside the container)
383
+ const overlay = document.querySelector('.herb-parser-error-overlay');
384
+ const container = document.querySelector('.herb-error-container');
385
+ if (overlay && container) {
386
+ overlay.addEventListener('click', function(e) {
387
+ if (e.target === overlay) {
388
+ closeErrorOverlay();
389
+ }
390
+ });
391
+ }
392
+
393
+ // Setup section toggles
394
+ document.querySelectorAll('.herb-section-header').forEach(header => {
395
+ header.addEventListener('click', function() {
396
+ const sectionId = this.getAttribute('data-section-id');
397
+ if (sectionId) toggleSection(sectionId);
398
+ });
399
+ });
400
+
401
+ // Close on Escape key
402
+ document.addEventListener('keydown', function(e) {
403
+ if (e.key === 'Escape') {
404
+ closeErrorOverlay();
405
+ }
406
+ });
407
+
408
+ // Prevent body scroll when overlay is open
409
+ document.body.style.overflow = 'hidden';
410
+
411
+ // Restore scroll when closed (cleanup for navigation)
412
+ const overlay = document.querySelector('.herb-parser-error-overlay');
413
+ if (overlay) {
414
+ const observer = new MutationObserver(function(mutations) {
415
+ mutations.forEach(function(mutation) {
416
+ if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
417
+ if (overlay.style.display === 'none') {
418
+ document.body.style.overflow = '';
419
+ }
420
+ }
421
+ });
422
+ });
423
+ observer.observe(overlay, { attributes: true });
424
+ }
425
+ })();
426
+ </script>
427
+ </div>
428
+ HTML
429
+ end
430
+
431
+ private
432
+
433
+ def generate_error_sections
434
+ sections = [] #: Array[String]
435
+
436
+ @errors.each_with_index do |error, index|
437
+ sections << generate_code_section(error, index)
438
+ end
439
+
440
+ suggestions = @errors.map { |error| get_error_suggestion(error) }.compact
441
+
442
+ sections << generate_suggestions_section(suggestions) if suggestions.any?
443
+
444
+ sections.uniq.join("\n")
445
+ end
446
+
447
+ def generate_code_section(error, index)
448
+ location = error.respond_to?(:location) && error.location ? error.location : nil
449
+ line_num = 1
450
+ col_num = 1
451
+
452
+ if location.respond_to?(:start) && location.is_a?(Herb::Location) && location.start
453
+ line_num = location.start.line
454
+ col_num = location.start.column
455
+ end
456
+
457
+ error_class = error.class.name.split("::").last.gsub(/Error$/, "")
458
+ error_message = error.respond_to?(:message) ? error.message : error.to_s
459
+
460
+ header_text = if @errors.length == 1
461
+ "#{@filename}:#{line_num}:#{col_num}"
462
+ else
463
+ "Error #{index + 1}: #{error_class} - #{@filename}:#{line_num}:#{col_num}"
464
+ end
465
+
466
+ code_lines = generate_code_lines(error, line_num, col_num)
467
+
468
+ <<~HTML
469
+ <div class="herb-code-section">
470
+ <div class="herb-code-header">
471
+ <div class="herb-file-icon">ERB</div>
472
+ #{escape_html(header_text)}
473
+ </div>
474
+ <div class="herb-error-message-section">
475
+ <div class="herb-error-message">#{escape_html(error_message)}</div>
476
+ </div>
477
+ <div class="herb-code-content">
478
+ #{code_lines}
479
+ </div>
480
+ </div>
481
+ HTML
482
+ end
483
+
484
+ def generate_code_lines(error, line_num, col_num)
485
+ start_line = [line_num - CONTEXT_LINES, 1].max
486
+ end_line = [line_num + CONTEXT_LINES, @lines.length].min
487
+
488
+ lines_html = [] #: Array[String]
489
+
490
+ (start_line..end_line).each do |i|
491
+ line = @lines[i - 1] || ""
492
+ line_str = line.chomp
493
+ is_error_line = i == line_num
494
+
495
+ line_class = is_error_line ? "herb-code-line herb-error-line" : "herb-code-line"
496
+
497
+ lines_html << <<~HTML
498
+ <div class="#{line_class}">
499
+ <div class="herb-line-number">#{i}</div>
500
+ <div class="herb-line-content">#{syntax_highlight(line_str)}</div>
501
+ </div>
502
+ HTML
503
+
504
+ next unless is_error_line && col_num.positive?
505
+
506
+ pointer_text = "^#{"~" * [line_str.length - col_num, 0].max}"
507
+ hint = get_inline_hint(error)
508
+ pointer_display = hint ? "#{pointer_text} #{hint}" : pointer_text
509
+
510
+ lines_html << <<~HTML
511
+ <div class="herb-error-pointer">#{" " * (col_num - 1)}#{escape_html(pointer_display)}</div>
512
+ HTML
513
+ end
514
+
515
+ lines_html.join("\n")
516
+ end
517
+
518
+ def generate_suggestions_section(suggestions)
519
+ suggestion_items = suggestions.map do |suggestion|
520
+ title, description = suggestion.split(": ", 2)
521
+ title ||= "Suggestion"
522
+ description ||= suggestion
523
+
524
+ <<~HTML
525
+ <div class="herb-suggestion-item">
526
+ <div class="herb-suggestion-title">#{escape_html(title)}</div>
527
+ <div>#{escape_html(description)}</div>
528
+ </div>
529
+ HTML
530
+ end
531
+
532
+ section_id = "suggestions-#{rand(1000)}"
533
+
534
+ <<~HTML
535
+ <div class="herb-section">
536
+ <div class="herb-section-header" data-section-id="#{section_id}">
537
+ Suggestions
538
+ <span class="herb-section-toggle" id="#{section_id}-toggle">▼</span>
539
+ </div>
540
+ <div class="herb-section-content" id="#{section_id}-content">
541
+ <div class="herb-suggestions">
542
+ #{suggestion_items.join}
543
+ </div>
544
+ </div>
545
+ </div>
546
+ HTML
547
+ end
548
+
549
+ def syntax_highlight(code)
550
+ lex_result = ::Herb.lex(code)
551
+
552
+ return escape_html(code) if lex_result.errors.any?
553
+
554
+ tokens = lex_result.value
555
+ highlight_with_tokens(tokens, code)
556
+ rescue StandardError
557
+ escape_html(code)
558
+ end
559
+
560
+ def highlight_with_tokens(tokens, code)
561
+ return escape_html(code) if tokens.empty?
562
+
563
+ highlighted = ""
564
+ last_end = 0
565
+
566
+ state = {
567
+ in_tag: false,
568
+ in_comment: false,
569
+ tag_name: "",
570
+ is_closing_tag: false,
571
+ expecting_attribute_name: false,
572
+ expecting_attribute_value: false,
573
+ in_quotes: false,
574
+ }
575
+
576
+ tokens.each_with_index do |token, i|
577
+ next_token = tokens[i + 1]
578
+ prev_token = tokens[i - 1]
579
+
580
+ start_offset = get_character_offset(code, token.location.start.line, token.location.start.column)
581
+ end_offset = get_character_offset(code, token.location.end.line, token.location.end.column)
582
+
583
+ highlighted += escape_html(code[last_end...start_offset]) if start_offset > last_end
584
+
585
+ token_text = code[start_offset...end_offset]
586
+
587
+ update_highlighting_state(state, token, token_text, next_token, prev_token)
588
+
589
+ css_class = get_token_css_class(state, token, token_text)
590
+
591
+ highlighted += if css_class
592
+ "<span class=\"herb-#{css_class}\">#{escape_html(token_text)}</span>"
593
+ else
594
+ escape_html(token_text)
595
+ end
596
+
597
+ last_end = end_offset
598
+ end
599
+
600
+ highlighted += escape_html(code[last_end..]) if last_end < code.length
601
+
602
+ highlighted
603
+ end
604
+
605
+ def get_character_offset(code, line, column)
606
+ lines = code.lines
607
+ offset = 0
608
+
609
+ (1...line).each do |line_num|
610
+ offset += lines[line_num - 1]&.length || 0
611
+ end
612
+
613
+ offset + column
614
+ end
615
+
616
+ def update_highlighting_state(state, token, token_text, _next_token, _prev_token)
617
+ case token.type
618
+ when "TOKEN_HTML_TAG_START"
619
+ state[:in_tag] = true
620
+ state[:is_closing_tag] = false
621
+ state[:expecting_attribute_name] = false
622
+ state[:expecting_attribute_value] = false
623
+
624
+ when "TOKEN_HTML_TAG_START_CLOSE"
625
+ state[:in_tag] = true
626
+ state[:is_closing_tag] = true
627
+ state[:expecting_attribute_name] = false
628
+ state[:expecting_attribute_value] = false
629
+
630
+ when "TOKEN_HTML_TAG_END", "TOKEN_HTML_TAG_SELF_CLOSE"
631
+ state[:in_tag] = false
632
+ state[:tag_name] = ""
633
+ state[:is_closing_tag] = false
634
+ state[:expecting_attribute_name] = false
635
+ state[:expecting_attribute_value] = false
636
+
637
+ when "TOKEN_IDENTIFIER"
638
+ if state[:in_tag] && state[:tag_name].empty?
639
+ state[:tag_name] = token_text
640
+ state[:expecting_attribute_name] = !state[:is_closing_tag]
641
+ elsif state[:in_tag] && state[:expecting_attribute_name]
642
+ state[:expecting_attribute_name] = false
643
+ state[:expecting_attribute_value] = true
644
+ end
645
+
646
+ when "TOKEN_EQUALS"
647
+ state[:expecting_attribute_value] = true if state[:in_tag]
648
+
649
+ when "TOKEN_QUOTE"
650
+ if state[:in_tag]
651
+ state[:in_quotes] = !state[:in_quotes]
652
+ unless state[:in_quotes]
653
+ state[:expecting_attribute_name] = true
654
+ state[:expecting_attribute_value] = false
655
+ end
656
+ end
657
+
658
+ when "TOKEN_WHITESPACE"
659
+ if state[:in_tag] && !state[:in_quotes] && !state[:tag_name].empty?
660
+ state[:expecting_attribute_name] = true
661
+ state[:expecting_attribute_value] = false
662
+ end
663
+
664
+ when "TOKEN_HTML_COMMENT_START"
665
+ state[:in_comment] = true
666
+
667
+ when "TOKEN_HTML_COMMENT_END"
668
+ state[:in_comment] = false
669
+ end
670
+ end
671
+
672
+ def get_token_css_class(state, token, token_text)
673
+ if state[:in_comment] && !["TOKEN_HTML_COMMENT_START", "TOKEN_HTML_COMMENT_END", "TOKEN_ERB_START",
674
+ "TOKEN_ERB_CONTENT", "TOKEN_ERB_END"].include?(token.type)
675
+ return "comment"
676
+ end
677
+
678
+ case token.type
679
+ when "TOKEN_ERB_START", "TOKEN_ERB_CONTENT", "TOKEN_ERB_END"
680
+ "erb"
681
+ when "TOKEN_HTML_COMMENT_START", "TOKEN_HTML_COMMENT_END"
682
+ "comment"
683
+ when "TOKEN_HTML_TAG_START", "TOKEN_HTML_TAG_END", "TOKEN_HTML_TAG_START_CLOSE", "TOKEN_HTML_TAG_SELF_CLOSE"
684
+ "tag"
685
+ when "TOKEN_IDENTIFIER"
686
+ if state[:in_tag] && token_text == state[:tag_name]
687
+ "tag"
688
+ elsif state[:in_tag] && (state[:expecting_attribute_name] || state[:expecting_attribute_value])
689
+ state[:in_quotes] ? "value" : "attr"
690
+ end
691
+ when "TOKEN_QUOTE"
692
+ state[:in_tag] ? "value" : nil
693
+ when "TOKEN_STRING"
694
+ state[:in_tag] ? "value" : "string"
695
+ end
696
+ end
697
+
698
+ def get_error_suggestion(error)
699
+ case error
700
+ when Herb::Errors::MissingClosingTagError
701
+ if error.respond_to?(:opening_tag) && error.opening_tag
702
+ "Add missing closing tag: Add </#{error.opening_tag.value}> to close the opening tag"
703
+ else
704
+ "Add missing closing tag: Add the missing closing tag"
705
+ end
706
+ when Herb::Errors::MissingOpeningTagError
707
+ if error.respond_to?(:closing_tag) && error.closing_tag
708
+ "Add missing opening tag: Add <#{error.closing_tag.value}> before the closing tag"
709
+ else
710
+ "Add missing opening tag: Add the missing opening tag"
711
+ end
712
+ when Herb::Errors::TagNamesMismatchError
713
+ if error.respond_to?(:opening_tag) && error.respond_to?(:closing_tag) && error.opening_tag && error.closing_tag
714
+ "Fix tag mismatch: Change </#{error.closing_tag.value}> to </#{error.opening_tag.value}>"
715
+ else
716
+ "Fix tag mismatch: Ensure opening and closing tags match"
717
+ end
718
+ when Herb::Errors::VoidElementClosingTagError
719
+ if error.respond_to?(:tag_name) && error.tag_name
720
+ "Remove illegal closing tag: Remove the closing tag for void element <#{error.tag_name.value}>"
721
+ else
722
+ "Remove illegal closing tag: Remove the closing tag for this void element"
723
+ end
724
+ when Herb::Errors::UnclosedElementError
725
+ if error.respond_to?(:opening_tag) && error.opening_tag
726
+ "Close unclosed element: Add </#{error.opening_tag.value}> before the end of the template"
727
+ else
728
+ "Close unclosed element: Close the unclosed element"
729
+ end
730
+ when Herb::Errors::RubyParseError
731
+ "Fix Ruby syntax: Check your Ruby syntax inside the ERB tag"
732
+ when Herb::Errors::QuotesMismatchError
733
+ "Fix quote mismatch: Use matching quotes for attribute values"
734
+ else
735
+ message = error.respond_to?(:message) ? error.message : error.to_s
736
+ "Fix error: #{message}"
737
+ end
738
+ end
739
+
740
+ def get_inline_hint(error)
741
+ case error
742
+ when Herb::Errors::MissingClosingTagError
743
+ "← Missing closing tag"
744
+ when Herb::Errors::TagNamesMismatchError
745
+ "← Tag mismatch"
746
+ when Herb::Errors::UnclosedElementError
747
+ "← Unclosed element"
748
+ when Herb::Errors::VoidElementClosingTagError
749
+ "← Void element cannot be closed"
750
+ when Herb::Errors::QuotesMismatchError
751
+ "← Quote mismatch"
752
+ when Herb::Errors::RubyParseError
753
+ "← Ruby syntax error"
754
+ end
755
+ end
756
+
757
+ def escape_html(text)
758
+ text.to_s
759
+ .gsub("&", "&amp;")
760
+ .gsub("<", "&lt;")
761
+ .gsub(">", "&gt;")
762
+ .gsub('"', "&quot;")
763
+ .gsub("'", "&#39;")
764
+ end
765
+ end
766
+ end
767
+ end