@marko/tree-sitter 0.0.0 → 0.2.0

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.
package/src/scanner.c ADDED
@@ -0,0 +1,3584 @@
1
+ // Tree-sitter external scanner for Marko: a faithful port of htmljs-parser's
2
+ // state machine (../../src). All terminals are produced here; grammar.js only
3
+ // sequences them.
4
+ //
5
+ // Mapping from parser states to this file:
6
+ // CONCISE_HTML_CONTENT -> scan_concise (+ dedent handling)
7
+ // HTML_CONTENT -> scan_html_content
8
+ // PARSED_TEXT_CONTENT -> scan_parsed_text
9
+ // BEGIN_DELIMITED_HTML_BLOCK -> BLOCK_OPEN / frame "fresh" handling
10
+ // OPEN_TAG / TAG_NAME / ATTRIBUTE -> scan_open_tag
11
+ // EXPRESSION (+ STRING / TEMPLATE_STRING / REGULAR_EXPRESSION /
12
+ // JS_COMMENT_*) -> scan_expr_inner
13
+ //
14
+ // Errors: wherever the parser calls emitError (and then stops parsing), this
15
+ // scanner refuses to produce a token, so tree-sitter emits an ERROR node and
16
+ // the rest of the input is not interpreted - mirroring the parser.
17
+ //
18
+ // tree-sitter's lexer cannot rewind: a token's start is fixed by the first
19
+ // non-skipped advance and mark_end can only mark the current position. The
20
+ // EStream type below layers a logical position + lookahead queue on top so
21
+ // the parser's lookAheadFor/lookBehindFor logic can be ported directly.
22
+
23
+ #include "tree_sitter/parser.h"
24
+
25
+ #include <stdbool.h>
26
+ #include <stdint.h>
27
+ #include <stdlib.h>
28
+ #include <string.h>
29
+
30
+ #ifdef TSMARKO_DEBUG
31
+ #include <stdio.h>
32
+ #endif
33
+
34
+ enum TokenType {
35
+ TEXT,
36
+ PLACEHOLDER_START, // "${"
37
+ PLACEHOLDER_START_RAW, // "$!{"
38
+ INTERP_START, // "${" within tag names
39
+ PLACEHOLDER_EXPR,
40
+ PLACEHOLDER_END, // "}"
41
+ HTML_COMMENT,
42
+ LINE_COMMENT,
43
+ BLOCK_COMMENT,
44
+ CDATA,
45
+ DOCTYPE,
46
+ DECLARATION,
47
+ SCRIPTLET_START, // "$" html mode
48
+ SCRIPTLET_START_CONCISE, // "$" concise mode
49
+ SCRIPTLET_BLOCK_OPEN, // "{"
50
+ SCRIPTLET_BLOCK_EXPR,
51
+ SCRIPTLET_BLOCK_CLOSE, // "}" [ws ";"]
52
+ SCRIPTLET_EXPR,
53
+ BLOCK_OPEN, // "--"...
54
+ BLOCK_CLOSE,
55
+ OPEN_TAG_START, // "<"
56
+ TAG_NAME_FRAGMENT,
57
+ TAG_NAME_EMPTY,
58
+ SHORTHAND_ID_START, // "#"
59
+ SHORTHAND_CLASS_START, // "."
60
+ STATEMENT_EXPR,
61
+ TAG_VAR_START, // "/"
62
+ VAR_PATTERN,
63
+ VAR_COLON, // ":" before a tag var type
64
+ VAR_TYPE,
65
+ ARGS_OPEN, // "("
66
+ ARGS_EXPR,
67
+ ARGS_CLOSE, // ")"
68
+ PARAMS_OPEN, // "|"
69
+ PARAM_PATTERN,
70
+ PARAM_COLON, // ":" before a parameter type
71
+ PARAM_TYPE,
72
+ PARAM_EQ, // "=" before a parameter default
73
+ PARAM_DEFAULT,
74
+ PARAM_COMMA, // ","
75
+ PARAMS_CLOSE, // "|"
76
+ TYPE_ARGS_OPEN, // "<" adjacent to tag name
77
+ TYPE_PARAMS_OPEN, // "<"
78
+ TYPE_EXPR,
79
+ TYPES_CLOSE, // ">"
80
+ ATTR_GROUP_OPEN, // "["
81
+ ATTR_GROUP_CLOSE, // "]"
82
+ ATTR_NAME,
83
+ ATTR_EQ, // "="
84
+ ATTR_BOUND_EQ, // ":="
85
+ ATTR_SPREAD_START, // "..."
86
+ ATTR_VALUE_EXPR,
87
+ METHOD_BODY_OPEN, // "{"
88
+ METHOD_BODY_EXPR,
89
+ METHOD_BODY_CLOSE, // "}"
90
+ OPEN_TAG_END, // ">"
91
+ OPEN_TAG_END_SELF, // "/>"
92
+ CONCISE_OPEN_TAG_END, // zero-width or ";"
93
+ CLOSE_TAG_START, // "</"
94
+ CLOSE_TAG_NAME,
95
+ CLOSE_TAG_END, // ">"
96
+ ELEMENT_END, // zero-width
97
+ TAG_COMMENT, // hidden: comments inside open tags (no events)
98
+ ESCAPE, // hidden: omitted backslashes before "${"
99
+ ERROR_SENTINEL,
100
+ };
101
+
102
+ // --------------------------------------------------------------------------
103
+ // Character helpers (port of util/util.ts)
104
+ // --------------------------------------------------------------------------
105
+
106
+ static inline bool is_ws(int32_t c) { return c <= ' ' && c >= 0; }
107
+ static inline bool is_line(int32_t c) { return c == '\n' || c == '\r'; }
108
+ static inline bool is_indent_code(int32_t c) { return c == ' ' || c == '\t'; }
109
+ static inline bool is_word_code(int32_t c) {
110
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
111
+ (c >= '0' && c <= '9') || c == '$' || c == '_';
112
+ }
113
+ static inline bool is_word_or_period_code(int32_t c) {
114
+ return c == '.' || is_word_code(c);
115
+ }
116
+
117
+ #define HASH_INIT 2166136261u
118
+ static inline uint32_t hash_push(uint32_t h, int32_t c) {
119
+ return (h ^ (uint32_t)(c & 0xff)) * 16777619u;
120
+ }
121
+ static uint32_t hash_str(const char *str) {
122
+ uint32_t h = HASH_INIT;
123
+ for (; *str; str++) h = hash_push(h, (int32_t)*str);
124
+ return h;
125
+ }
126
+
127
+ // --------------------------------------------------------------------------
128
+ // State
129
+ // --------------------------------------------------------------------------
130
+
131
+ enum TagKind { TAG_HTML = 0, TAG_TEXT = 1, TAG_VOID = 2, TAG_STATEMENT = 3 };
132
+
133
+ enum ContentKind { CONTENT_HTML = 0, CONTENT_PARSED_TEXT = 1 };
134
+
135
+ enum FrameFresh {
136
+ FRESH_NONE = 0,
137
+ FRESH_SINGLE, // "-- x": skip one char, then content
138
+ FRESH_DELIM_LINE, // "--\n": skip to EOL + newline, then first block line
139
+ };
140
+
141
+ typedef struct {
142
+ uint32_t name_hash; // hash of the tag name's source text
143
+ uint32_t full_hash; // name + shorthand source text (for </div.foo>)
144
+ uint32_t indent_hash;
145
+ uint32_t nested_indent_hash;
146
+ uint16_t name_len;
147
+ uint16_t full_len;
148
+ uint16_t indent_len;
149
+ uint16_t nested_indent_len;
150
+ uint8_t type; // enum TagKind
151
+ uint8_t flags; // TF_*
152
+ } TagFrame;
153
+
154
+ #define TF_CONCISE 1u
155
+ #define TF_BEGIN_MIXED 2u
156
+ #define TF_HAS_NESTED_INDENT 4u
157
+ #define TF_NAME_INTERP 8u
158
+
159
+ typedef struct {
160
+ uint32_t indent_hash;
161
+ uint16_t indent_len;
162
+ uint16_t delimiter_len; // hyphen count; 0 => not a delimited block
163
+ uint8_t content; // enum ContentKind
164
+ uint8_t flags; // CF_*
165
+ uint8_t fresh; // enum FrameFresh
166
+ uint8_t pstring_quote; // 0 | '"' | '\'' (PARSED_STRING resumes here)
167
+ } ContentFrame;
168
+
169
+ #define CF_SINGLE_LINE 1u
170
+ #define CF_IS_BLOCK 2u // has BLOCK_OPEN/BLOCK_CLOSE tokens (html_block node)
171
+
172
+ #define MAX_TAGS 24
173
+ #define MAX_CONTENTS 12
174
+
175
+ // Position bookkeeping replacing the parser's backwards reads of the source.
176
+ // Committed in lock-step with mark_end (see `mark`).
177
+ typedef struct {
178
+ uint8_t prev_char;
179
+ uint8_t prev_nonws_char;
180
+ uint8_t line_only_ws; // only whitespace since the last newline
181
+ uint16_t cur_indent_len;
182
+ uint32_t cur_indent_hash;
183
+ } Cursor;
184
+
185
+ typedef struct {
186
+ Cursor cur; // live: tracks every logically consumed/skipped char
187
+ Cursor saved; // as of the last mark_end: what gets serialized
188
+
189
+ uint8_t started; // BOM handled
190
+ uint8_t begin_mixed_mode;
191
+ uint8_t ending_mixed_at_eol;
192
+ uint8_t semicolon_line; // after `tag;` only comments may follow
193
+ uint8_t pending_element_ends;
194
+ uint8_t suppress_placeholder; // next "${" is literal text (escaped)
195
+ uint8_t fail_next; // emit an error on the next scan (comment trailing)
196
+ uint8_t tag_comma_continue; // concise open tag continues after , at EOL
197
+
198
+ // open tag state (only one tag can be open at a time)
199
+ uint8_t tag_open;
200
+ uint8_t tag_name_done;
201
+ uint8_t in_shorthand;
202
+ uint8_t adjacent_lt; // "<" directly terminated the tag name
203
+ uint8_t statement_pending;
204
+ uint8_t in_attr_group;
205
+ uint8_t attr_active;
206
+ uint8_t attr_stage; // ATTR_*
207
+ uint8_t attr_has_args;
208
+ uint8_t attr_has_type_params;
209
+ uint8_t tag_has_attrs;
210
+ uint8_t tag_has_args;
211
+ uint8_t tag_has_params;
212
+ uint8_t tag_has_shorthand_id;
213
+ uint8_t tag_pending_types; // tag-level <T> awaiting | or (
214
+ uint8_t types_was_args; // current <...> is a type-args (adjacent)
215
+ uint8_t expect_method_open; // attr-level <T> closed; "(" must follow
216
+ uint8_t block_after_tag; // "--" ended a concise tag: indent extension
217
+
218
+ uint8_t tag_len;
219
+ uint8_t content_len;
220
+ TagFrame tags[MAX_TAGS];
221
+ ContentFrame contents[MAX_CONTENTS];
222
+
223
+ // scratch (not serialized)
224
+ char *buf;
225
+ uint32_t buf_len, buf_cap;
226
+ } Scanner;
227
+
228
+ // ATTR_STAGE mirror of states/ATTRIBUTE.ts
229
+ enum {
230
+ ATTR_UNKNOWN = 0,
231
+ ATTR_NAME_STAGE,
232
+ ATTR_VALUE_STAGE,
233
+ ATTR_ARGUMENT_STAGE,
234
+ ATTR_TYPE_PARAMS_STAGE,
235
+ ATTR_BLOCK_STAGE,
236
+ };
237
+
238
+ // isConcise: only HTML_CONTENT.enter flips the parser out of concise mode;
239
+ // PARSED_TEXT_CONTENT keeps the current mode. So concise mode holds unless
240
+ // an html content frame is on the stack.
241
+ static inline bool is_concise(Scanner *s) {
242
+ for (uint8_t i = 0; i < s->content_len; i++) {
243
+ if (s->contents[i].content == CONTENT_HTML) return false;
244
+ }
245
+ return true;
246
+ }
247
+ static inline TagFrame *top_tag(Scanner *s) {
248
+ return s->tag_len ? &s->tags[s->tag_len - 1] : NULL;
249
+ }
250
+ static inline ContentFrame *top_content(Scanner *s) {
251
+ return s->content_len ? &s->contents[s->content_len - 1] : NULL;
252
+ }
253
+
254
+ static void cursor_advance(Cursor *cur, int32_t c) {
255
+ cur->prev_char = (uint8_t)(c & 0xff);
256
+ if (is_line(c)) {
257
+ cur->line_only_ws = 1;
258
+ cur->cur_indent_len = 0;
259
+ cur->cur_indent_hash = HASH_INIT;
260
+ } else if (is_ws(c)) {
261
+ if (cur->line_only_ws) {
262
+ cur->cur_indent_len++;
263
+ cur->cur_indent_hash = hash_push(cur->cur_indent_hash, c);
264
+ }
265
+ } else {
266
+ cur->line_only_ws = 0;
267
+ cur->prev_nonws_char = (uint8_t)(c & 0xff);
268
+ }
269
+ }
270
+
271
+ // Skip a trivia char (not part of any token; token start moves past it).
272
+ static inline void skipc(Scanner *s, TSLexer *lexer) {
273
+ cursor_advance(&s->cur, lexer->lookahead);
274
+ lexer->advance(lexer, true);
275
+ }
276
+
277
+ // Mark the current lexer position as the token end and commit the cursor.
278
+ // Only call when the lexer position equals the logical position.
279
+ static inline void mark(Scanner *s, TSLexer *lexer) {
280
+ lexer->mark_end(lexer);
281
+ s->saved = s->cur;
282
+ }
283
+
284
+ static inline bool at_eof(TSLexer *lexer) { return lexer->eof(lexer); }
285
+
286
+ static inline void buf_push(Scanner *s, int32_t c) {
287
+ if (s->buf_len == s->buf_cap) {
288
+ s->buf_cap = s->buf_cap ? s->buf_cap * 2 : 256;
289
+ s->buf = (char *)realloc(s->buf, s->buf_cap);
290
+ }
291
+ s->buf[s->buf_len++] = (char)(c & 0xff);
292
+ }
293
+
294
+ // --------------------------------------------------------------------------
295
+ // EStream: logical stream with lookahead over the TSLexer
296
+ // --------------------------------------------------------------------------
297
+
298
+ typedef struct {
299
+ Scanner *s;
300
+ TSLexer *lexer;
301
+ uint32_t start; // s->buf index where the current construct begins
302
+ uint32_t marked_len; // buf_len at the last committed mark
303
+ uint8_t before_token; // the char immediately before the token start
304
+ int32_t *la;
305
+ uint32_t la_len, la_pos, la_cap;
306
+ bool automark; // mark after every consumed char (expressions)
307
+ } EStream;
308
+
309
+ static void es_init(EStream *es, Scanner *s, TSLexer *lexer, bool automark) {
310
+ memset(es, 0, sizeof(*es));
311
+ es->s = s;
312
+ es->lexer = lexer;
313
+ es->automark = automark;
314
+ es->before_token = s->cur.prev_char;
315
+ s->buf_len = 0;
316
+ }
317
+
318
+ static void es_free(EStream *es) { free(es->la); }
319
+
320
+ // Peek the k-th char ahead of the logical position (k=0 -> current char).
321
+ // Returns -1 at EOF. The queue holds chars the lexer has advanced past;
322
+ // lexer->lookahead is the next unqueued char, so peeking k chars ahead only
323
+ // advances the lexer k times (the last char inspected stays unconsumed).
324
+ static int32_t es_peek(EStream *es, uint32_t k) {
325
+ uint32_t avail = es->la_len - es->la_pos;
326
+ if (k < avail) return es->la[es->la_pos + k];
327
+ while (avail < k) {
328
+ if (at_eof(es->lexer)) return -1;
329
+ if (es->la_len == es->la_cap) {
330
+ es->la_cap = es->la_cap ? es->la_cap * 2 : 64;
331
+ es->la = (int32_t *)realloc(es->la, es->la_cap * sizeof(int32_t));
332
+ }
333
+ es->la[es->la_len++] = es->lexer->lookahead;
334
+ es->lexer->advance(es->lexer, false);
335
+ avail++;
336
+ }
337
+ return at_eof(es->lexer) ? -1 : es->lexer->lookahead;
338
+ }
339
+
340
+ static inline bool es_drained(EStream *es) { return es->la_pos == es->la_len; }
341
+
342
+ // Logically consume one char. Marks the token end when the lookahead queue
343
+ // drains (so the committed end always sits at a logical position).
344
+ static void es_consume(EStream *es, bool always_mark) {
345
+ int32_t c;
346
+ if (es->la_pos < es->la_len) {
347
+ c = es->la[es->la_pos++];
348
+ if (es->la_pos == es->la_len) es->la_pos = es->la_len = 0;
349
+ } else {
350
+ if (at_eof(es->lexer)) return;
351
+ c = es->lexer->lookahead;
352
+ es->lexer->advance(es->lexer, false);
353
+ }
354
+ buf_push(es->s, c);
355
+ cursor_advance(&es->s->cur, c);
356
+ if (es->la_pos == es->la_len && (always_mark || es->automark)) {
357
+ mark(es->s, es->lexer);
358
+ es->marked_len = es->s->buf_len;
359
+ }
360
+ }
361
+
362
+ static inline void es_next(EStream *es) { es_consume(es, false); }
363
+ static inline void es_next_mark(EStream *es) { es_consume(es, true); }
364
+
365
+ // Explicitly mark at the current logical position if no lookahead is pending.
366
+ static void es_mark(EStream *es) {
367
+ if (es_drained(es)) {
368
+ mark(es->s, es->lexer);
369
+ es->marked_len = es->s->buf_len;
370
+ }
371
+ }
372
+
373
+ static int32_t es_at(EStream *es, int64_t i) {
374
+ Scanner *s = es->s;
375
+ if (i >= 0 && (uint64_t)i < s->buf_len) return (int32_t)(uint8_t)s->buf[i];
376
+ // The parser reads the raw document before the expression (eg the ":"
377
+ // before a tag var type keeps whitespace continuation working); one char
378
+ // of history is tracked.
379
+ if (i == -1 && es->before_token) return (int32_t)es->before_token;
380
+ return -1;
381
+ }
382
+
383
+ static int32_t es_prev_char(EStream *es) {
384
+ Scanner *s = es->s;
385
+ if (s->buf_len > 0) return (int32_t)(uint8_t)s->buf[s->buf_len - 1];
386
+ return s->cur.prev_char;
387
+ }
388
+
389
+ static int32_t es_prev_nonws(EStream *es) {
390
+ Scanner *s = es->s;
391
+ for (int64_t i = (int64_t)s->buf_len - 1; i >= 0; i--) {
392
+ int32_t c = (int32_t)(uint8_t)s->buf[i];
393
+ if (!is_ws(c)) return c;
394
+ }
395
+ return s->cur.prev_nonws_char;
396
+ }
397
+
398
+ // --------------------------------------------------------------------------
399
+ // Expression scanning (port of states/EXPRESSION.ts)
400
+ // --------------------------------------------------------------------------
401
+
402
+ enum Term {
403
+ TERM_NONE = 0,
404
+ TERM_CLOSE_PAREN,
405
+ TERM_CLOSE_CURLY,
406
+ TERM_CLOSE_ANGLE,
407
+ TERM_PIPE,
408
+ TERM_HTML_ATTR_NAME,
409
+ TERM_CONCISE_ATTR_NAME,
410
+ TERM_CONCISE_GROUPED_ATTR_NAME,
411
+ TERM_HTML_ATTR_VALUE,
412
+ TERM_CONCISE_ATTR_VALUE,
413
+ TERM_CONCISE_GROUPED_ATTR_VALUE,
414
+ TERM_HTML_TAG_VAR,
415
+ TERM_CONCISE_TAG_VAR,
416
+ TERM_PARAM_PATTERN, // "|" "," ":" "=" at depth 0
417
+ TERM_PARAM_TYPE, // "|" "," "=" at depth 0
418
+ TERM_PARAM_DEFAULT, // "|" "," at depth 0
419
+ };
420
+
421
+ typedef struct {
422
+ bool operators;
423
+ bool terminated_by_eol;
424
+ bool terminated_by_whitespace;
425
+ bool consume_indented;
426
+ bool in_type;
427
+ bool force_type;
428
+ bool split_at_type_colon; // stop before the ":" that enters type mode
429
+ bool resume_lookbehind; // token resumes a split expression: operator
430
+ // lookbehind may cross the token start
431
+ uint8_t terminator;
432
+ } ExprCfg;
433
+
434
+ typedef struct {
435
+ ExprCfg cfg;
436
+ uint8_t group[128];
437
+ uint32_t group_len;
438
+ int32_t ternary_depth;
439
+ bool in_type;
440
+ bool force_type;
441
+ bool was_comment;
442
+ } ExprState;
443
+
444
+ // shouldTerminate(code, data, pos, expression); `k` is how far ahead of the
445
+ // logical position `code` sits (0 = current char).
446
+ static bool should_terminate(EStream *es, ExprState *e, int32_t code,
447
+ uint32_t k) {
448
+ int32_t next = es_peek(es, k + 1);
449
+ int32_t prev = k == 0 ? es_prev_char(es) : es_peek(es, k - 1);
450
+ bool at_start = k == 0 && es->s->buf_len == es->start;
451
+
452
+ switch (e->cfg.terminator) {
453
+ case TERM_NONE:
454
+ return false;
455
+ case TERM_CLOSE_PAREN:
456
+ return code == ')';
457
+ case TERM_CLOSE_CURLY:
458
+ return code == '}';
459
+ case TERM_CLOSE_ANGLE:
460
+ return code == '>';
461
+ case TERM_PIPE:
462
+ return code == '|';
463
+ case TERM_HTML_ATTR_NAME:
464
+ switch (code) {
465
+ case ',': case '=': case '(': case '>': case '<':
466
+ return true;
467
+ case ':':
468
+ return next == '=';
469
+ case '/':
470
+ return next == '>';
471
+ default:
472
+ return false;
473
+ }
474
+ case TERM_CONCISE_ATTR_NAME:
475
+ switch (code) {
476
+ case ',': case '=': case '(': case ';': case '<':
477
+ return true;
478
+ case ':':
479
+ return next == '=';
480
+ case '-':
481
+ return next == '-' && is_ws(prev);
482
+ default:
483
+ return false;
484
+ }
485
+ case TERM_CONCISE_GROUPED_ATTR_NAME:
486
+ switch (code) {
487
+ case ',': case '=': case '(': case ']': case '<':
488
+ return true;
489
+ case ':':
490
+ return next == '=';
491
+ default:
492
+ return false;
493
+ }
494
+ case TERM_HTML_ATTR_VALUE:
495
+ switch (code) {
496
+ case ',':
497
+ return true;
498
+ case '/':
499
+ return next == '>';
500
+ case '>':
501
+ // `=>` continues unless it is the very start of the expression.
502
+ return at_start || prev != '=';
503
+ default:
504
+ return false;
505
+ }
506
+ case TERM_CONCISE_ATTR_VALUE:
507
+ switch (code) {
508
+ case ',': case ';':
509
+ return true;
510
+ case '-':
511
+ return next == '-' && is_ws(prev);
512
+ default:
513
+ return false;
514
+ }
515
+ case TERM_CONCISE_GROUPED_ATTR_VALUE:
516
+ return code == ',' || code == ']';
517
+ case TERM_HTML_TAG_VAR:
518
+ switch (code) {
519
+ case '|': case ',': case '=': case '(': case '>':
520
+ return true;
521
+ case '<':
522
+ return !e->in_type;
523
+ case ':':
524
+ return next == '=';
525
+ case '/':
526
+ return next == '>';
527
+ default:
528
+ return false;
529
+ }
530
+ case TERM_CONCISE_TAG_VAR:
531
+ switch (code) {
532
+ case ',': case '=': case '|': case '(': case ';':
533
+ return true;
534
+ case '<':
535
+ return !e->in_type;
536
+ case '-':
537
+ return next == '-';
538
+ case ':':
539
+ return next == '=';
540
+ default:
541
+ return false;
542
+ }
543
+ case TERM_PARAM_PATTERN:
544
+ return code == '|' || code == ',' || code == ':' || code == '=';
545
+ case TERM_PARAM_TYPE:
546
+ return code == '|' || code == ',' || code == '=';
547
+ case TERM_PARAM_DEFAULT:
548
+ return code == '|' || code == ',';
549
+ }
550
+ return false;
551
+ }
552
+
553
+ static const char *const UNARY_KEYWORDS[] = {
554
+ "async", "await", "class", "function", "new", "typeof", "void", NULL,
555
+ };
556
+ static const char *const TS_UNARY_KEYWORDS[] = {
557
+ "async", "await", "class", "function", "new", "typeof", "void",
558
+ "asserts", "infer", "is", "keyof", "readonly", "unique", NULL,
559
+ };
560
+ static const char *const BINARY_KEYWORDS[] = {
561
+ "as", "extends", "instanceof", "in", "satisfies", NULL,
562
+ };
563
+
564
+ static int64_t look_behind_for(EStream *es, int64_t pos, const char *str) {
565
+ int64_t len = (int64_t)strlen(str);
566
+ int64_t end_pos = pos - len + 1;
567
+ if (end_pos < 0) return -1;
568
+ for (int64_t i = 0; i < len; i++) {
569
+ if (es_at(es, end_pos + i) != (int32_t)str[i]) return -1;
570
+ }
571
+ return end_pos;
572
+ }
573
+
574
+ static int64_t look_behind_while_ws(EStream *es, int64_t pos) {
575
+ int64_t i = pos;
576
+ while (i >= 0) {
577
+ int32_t c = es_at(es, i);
578
+ if (c < 0 || !is_ws(c)) return i + 1;
579
+ i--;
580
+ }
581
+ return 0;
582
+ }
583
+
584
+ // lookBehindForOperator: `pos` is a buf index just past the candidate.
585
+ static int64_t look_behind_for_operator(EStream *es, ExprState *e,
586
+ int64_t pos) {
587
+ int64_t cur_pos = pos - 1;
588
+ int32_t code = es_at(es, cur_pos);
589
+
590
+ switch (code) {
591
+ case '&': case '*': case '^': case ':': case '=': case '!':
592
+ case '<': case '%': case '|': case '?': case '~':
593
+ // The operator may sit just before the token start (cur_pos == -1, eg
594
+ // resuming a tag var type after its ":"); callers only test != -1.
595
+ return cur_pos < 0 ? 0 : cur_pos;
596
+
597
+ case '>':
598
+ if (es_at(es, cur_pos - 1) == '=') return cur_pos - 1;
599
+ return e->in_type ? -1 : (cur_pos < 0 ? 0 : cur_pos);
600
+
601
+ case '.': {
602
+ // `.` followed (over whitespace) by an identifier continues.
603
+ // Chars at/past the logical position live in the lookahead queue.
604
+ int64_t buf_len = (int64_t)es->s->buf_len;
605
+ int64_t i = pos;
606
+ int32_t c;
607
+ for (;;) {
608
+ c = i < buf_len ? es_at(es, i) : es_peek(es, (uint32_t)(i - buf_len));
609
+ if (c >= 0 && is_ws(c)) i++;
610
+ else break;
611
+ }
612
+ return (c >= 0 && is_word_code(c)) ? i : -1;
613
+ }
614
+
615
+ case '+':
616
+ case '-': {
617
+ if (es_at(es, cur_pos - 1) == code) {
618
+ // eg "typeof++ x"
619
+ return look_behind_for_operator(es, e,
620
+ look_behind_while_ws(es, cur_pos - 2));
621
+ }
622
+ return cur_pos;
623
+ }
624
+
625
+ default: {
626
+ if (code < 'a' || code > 'z') return -1;
627
+ const char *const *keywords =
628
+ e->in_type ? TS_UNARY_KEYWORDS : UNARY_KEYWORDS;
629
+ for (int i = 0; keywords[i]; i++) {
630
+ int64_t keyword_pos = look_behind_for(es, cur_pos, keywords[i]);
631
+ if (keyword_pos != -1) {
632
+ int32_t before = keyword_pos > 0 ? es_at(es, keyword_pos - 1) : -1;
633
+ return is_word_or_period_code(before) ? -1 : keyword_pos;
634
+ }
635
+ }
636
+ return -1;
637
+ }
638
+ }
639
+ }
640
+
641
+ // lookAheadForOperator: examines chars `k` ahead. Returns chars to consume
642
+ // (the new logical position relative to now) or -1.
643
+ static int64_t look_ahead_for_operator(EStream *es, ExprState *e, uint32_t k) {
644
+ int32_t c = es_peek(es, k);
645
+ switch (c) {
646
+ case '&': case '*': case '^': case '!': case '<': case '%':
647
+ case '|': case '~': case '+': case '-':
648
+ return (int64_t)k + 1;
649
+
650
+ case '/': case '{': case '(': case '>': case '?': case ':': case '=':
651
+ return (int64_t)k; // defer to the base expression state
652
+
653
+ case '.': {
654
+ uint32_t k2 = k + 1;
655
+ while (true) {
656
+ int32_t c2 = es_peek(es, k2);
657
+ if (c2 >= 0 && is_ws(c2)) k2++;
658
+ else return (c2 >= 0 && is_word_code(c2)) ? (int64_t)k2 : -1;
659
+ }
660
+ }
661
+
662
+ default: {
663
+ if (c < 'a' || c > 'z') return -1;
664
+ for (int i = 0; BINARY_KEYWORDS[i]; i++) {
665
+ const char *kw = BINARY_KEYWORDS[i];
666
+ size_t len = strlen(kw);
667
+ bool match_kw = true;
668
+ for (size_t j = 0; j < len; j++) {
669
+ if (es_peek(es, k + (uint32_t)j) != (int32_t)kw[j]) {
670
+ match_kw = false;
671
+ break;
672
+ }
673
+ }
674
+ if (!match_kw) continue;
675
+ int32_t c_after = es_peek(es, k + (uint32_t)len);
676
+ if (c_after < 0 || !is_ws(c_after)) break;
677
+
678
+ // skip whitespace; an operand must exist before EOF
679
+ uint32_t k2 = k + (uint32_t)len + 1;
680
+ int32_t c2;
681
+ while ((c2 = es_peek(es, k2)) >= 0 && is_ws(c2)) k2++;
682
+ if (c2 < 0) break;
683
+
684
+ switch (c2) {
685
+ case ':': case ',': case '=': case '/': case '>': case ';':
686
+ break;
687
+ default:
688
+ if (!e->in_type &&
689
+ (strcmp(kw, "as") == 0 || strcmp(kw, "satisfies") == 0)) {
690
+ e->in_type = true;
691
+ if (!(e->ternary_depth || e->group_len)) e->force_type = true;
692
+ }
693
+ return (int64_t)k2;
694
+ }
695
+ break;
696
+ }
697
+ return -1;
698
+ }
699
+ }
700
+ }
701
+
702
+ static bool can_follow_division(int32_t c) {
703
+ if (is_word_code(c)) return true;
704
+ switch (c) {
705
+ case '`': case '\'': case '"': case '%': case ')': case '.':
706
+ case '<': case ']': case '}':
707
+ return true;
708
+ default:
709
+ return false;
710
+ }
711
+ }
712
+
713
+ static void es_consume_ws(EStream *es) {
714
+ while (true) {
715
+ int32_t c = es_peek(es, 0);
716
+ if (c >= 0 && is_ws(c)) es_next(es);
717
+ else break;
718
+ }
719
+ }
720
+
721
+ // checkForOperators(parser, expression, eol)
722
+ static bool check_for_operators(EStream *es, ExprState *e, bool eol) {
723
+ if (!e->cfg.operators) return false;
724
+ Scanner *s = es->s;
725
+
726
+ if (look_behind_for_operator(es, e, (int64_t)s->buf_len) != -1) {
727
+ es_consume_ws(es);
728
+ return true;
729
+ }
730
+
731
+ bool terminated_by_eol = e->cfg.terminated_by_eol || is_concise(s);
732
+ if (!(terminated_by_eol && eol)) {
733
+ uint32_t k = 1;
734
+ while (true) {
735
+ int32_t c = es_peek(es, k);
736
+ if (c >= 0 && (terminated_by_eol ? is_indent_code(c) : is_ws(c))) k++;
737
+ else break;
738
+ }
739
+
740
+ int32_t next_c = es_peek(es, k);
741
+ if (next_c >= 0 && !should_terminate(es, e, next_c, k)) {
742
+ int64_t consume = look_ahead_for_operator(es, e, k);
743
+ if (consume != -1) {
744
+ for (int64_t i = 0; i < consume; i++) es_next(es);
745
+ return true;
746
+ }
747
+ }
748
+ }
749
+
750
+ return false;
751
+ }
752
+
753
+ static bool scan_expr_inner(EStream *es, ExprState *e);
754
+
755
+ // STRING state
756
+ static bool sub_scan_string(EStream *es, int32_t quote) {
757
+ while (true) {
758
+ int32_t c = es_peek(es, 0);
759
+ if (c < 0) return false; // EOF: INVALID_STRING
760
+ if (c == '\\') {
761
+ es_next(es);
762
+ if (es_peek(es, 0) < 0) return false;
763
+ es_next(es);
764
+ } else if (c == quote) {
765
+ es_next(es);
766
+ return true;
767
+ } else {
768
+ es_next(es);
769
+ }
770
+ }
771
+ }
772
+
773
+ // TEMPLATE_STRING state (shared by expressions and parsed text)
774
+ static bool sub_scan_template(EStream *es) {
775
+ while (true) {
776
+ int32_t c = es_peek(es, 0);
777
+ if (c < 0) return false; // EOF: INVALID_TEMPLATE_STRING
778
+ if (c == '$') {
779
+ if (es_peek(es, 1) == '{') {
780
+ es_next(es);
781
+ es_next(es);
782
+ uint32_t before = es->s->buf_len;
783
+ uint32_t prev_start = es->start;
784
+ es->start = es->s->buf_len;
785
+ ExprState nested;
786
+ memset(&nested, 0, sizeof(nested));
787
+ nested.cfg.terminator = TERM_CLOSE_CURLY;
788
+ bool ok = scan_expr_inner(es, &nested);
789
+ es->start = prev_start;
790
+ if (!ok) return false;
791
+ if (es->s->buf_len == before) return false; // MALFORMED_PLACEHOLDER
792
+ if (es_peek(es, 0) != '}') return false;
793
+ es_next(es); // skip }
794
+ } else {
795
+ es_next(es);
796
+ }
797
+ } else if (c == '\\') {
798
+ es_next(es);
799
+ if (es_peek(es, 0) < 0) return false;
800
+ es_next(es);
801
+ } else if (c == '`') {
802
+ es_next(es);
803
+ return true;
804
+ } else {
805
+ es_next(es);
806
+ }
807
+ }
808
+ }
809
+
810
+ // REGULAR_EXPRESSION state
811
+ static bool sub_scan_regex(EStream *es) {
812
+ bool in_charset = false;
813
+ while (true) {
814
+ int32_t c = es_peek(es, 0);
815
+ if (c < 0) return false; // EOF: INVALID_REGULAR_EXPRESSION
816
+ switch (c) {
817
+ case '\\': {
818
+ int32_t escaped = es_peek(es, 1);
819
+ if (escaped >= 0 && !is_line(escaped)) {
820
+ es_next(es);
821
+ es_next(es);
822
+ break;
823
+ }
824
+ return false; // EOL while parsing regular expression
825
+ }
826
+ case '\n':
827
+ case '\r':
828
+ return false;
829
+ case '[':
830
+ in_charset = true;
831
+ es_next(es);
832
+ break;
833
+ case ']':
834
+ in_charset = false;
835
+ es_next(es);
836
+ break;
837
+ case '/':
838
+ es_next(es);
839
+ if (!in_charset) return true;
840
+ break;
841
+ default:
842
+ es_next(es);
843
+ break;
844
+ }
845
+ }
846
+ }
847
+
848
+ // The core of EXPRESSION.parse. On success the logical position sits at the
849
+ // terminator (not consumed). Returns false on parser-error paths.
850
+ static bool scan_expr_inner(EStream *es, ExprState *e) {
851
+ Scanner *s = es->s;
852
+
853
+ while (true) {
854
+ int32_t code = es_peek(es, 0);
855
+
856
+ if (code < 0) {
857
+ // EOF
858
+ if (!e->group_len && (is_concise(s) || e->cfg.terminated_by_eol)) {
859
+ return true;
860
+ }
861
+ return false;
862
+ }
863
+
864
+ // EOL handling
865
+ if (code == '\n' || code == '\r') {
866
+ uint32_t len = (code == '\r' && es_peek(es, 1) == '\n') ? 2 : 1;
867
+ uint32_t before = s->buf_len;
868
+ int32_t after_newline = es_peek(es, len);
869
+
870
+ if (!e->group_len &&
871
+ (e->cfg.terminated_by_eol || e->cfg.terminated_by_whitespace) &&
872
+ (e->was_comment || !check_for_operators(es, e, true)) &&
873
+ !(e->cfg.consume_indented && after_newline >= 0 &&
874
+ is_indent_code(after_newline))) {
875
+ return true; // exit before the newline
876
+ }
877
+
878
+ e->was_comment = false;
879
+ if (s->buf_len == before) {
880
+ for (uint32_t i = 0; i < len; i++) es_next(es);
881
+ }
882
+ continue;
883
+ }
884
+
885
+ if (is_word_code(code)) {
886
+ es_next(es);
887
+ continue;
888
+ }
889
+
890
+ if (!e->group_len) {
891
+ if (e->cfg.terminated_by_whitespace && is_ws(code)) {
892
+ if (!check_for_operators(es, e, false)) return true;
893
+ continue;
894
+ }
895
+
896
+ if (should_terminate(es, e, code, 0)) {
897
+ bool was_expression = false;
898
+ if (e->cfg.operators) {
899
+ int64_t prev_nonws_pos =
900
+ look_behind_while_ws(es, (int64_t)s->buf_len - 1);
901
+ bool can_look = prev_nonws_pos > (int64_t)es->start;
902
+ if (!can_look && e->cfg.resume_lookbehind && es->start == 0 &&
903
+ es->before_token && !is_ws((int32_t)es->before_token)) {
904
+ // This token continues a split expression (eg a tag var type
905
+ // after its ":"); the parser's lookbehind would see that char.
906
+ can_look = true;
907
+ }
908
+ if (can_look) {
909
+ was_expression =
910
+ look_behind_for_operator(es, e, prev_nonws_pos) != -1;
911
+ }
912
+ }
913
+ if (!was_expression) return true;
914
+ }
915
+ }
916
+
917
+ switch (code) {
918
+ case '"':
919
+ es_next(es);
920
+ if (!sub_scan_string(es, '"')) return false;
921
+ break;
922
+ case '\'':
923
+ es_next(es);
924
+ if (!sub_scan_string(es, '\'')) return false;
925
+ break;
926
+ case '`':
927
+ es_next(es);
928
+ if (!sub_scan_template(es)) return false;
929
+ break;
930
+ case '?':
931
+ if (e->cfg.operators && !e->group_len) {
932
+ e->ternary_depth++;
933
+ es_next(es);
934
+ es_consume_ws(es);
935
+ continue;
936
+ }
937
+ es_next(es);
938
+ break;
939
+ case ':':
940
+ if (e->cfg.operators && !e->group_len) {
941
+ if (e->ternary_depth) {
942
+ e->ternary_depth--;
943
+ } else {
944
+ if (e->cfg.split_at_type_colon && !e->in_type) {
945
+ // Stop before the ":" that would enter type mode: the caller
946
+ // emits it as a separate token and resumes with in_type set,
947
+ // reproducing the parser's exact scan as two tokens.
948
+ return true;
949
+ }
950
+ e->in_type = true;
951
+ }
952
+ es_next(es);
953
+ es_consume_ws(es);
954
+ continue;
955
+ }
956
+ es_next(es);
957
+ break;
958
+ case '=':
959
+ if (e->cfg.operators) {
960
+ if (es_peek(es, 1) == '>') {
961
+ if (e->in_type && !e->force_type && es_prev_nonws(es) != ')') {
962
+ e->in_type = false;
963
+ }
964
+ es_next(es); // skip =; the loop handles >
965
+ } else if (!(e->force_type || e->group_len)) {
966
+ e->in_type = false;
967
+ }
968
+ es_next(es); // skip = (or the char after =>)
969
+ es_consume_ws(es);
970
+ continue;
971
+ }
972
+ es_next(es);
973
+ break;
974
+ case '/':
975
+ switch (es_peek(es, 1)) {
976
+ case '/': {
977
+ es_next(es);
978
+ es_next(es);
979
+ // JS_COMMENT_LINE: consume to EOL (the EOL itself stays).
980
+ while (true) {
981
+ int32_t c2 = es_peek(es, 0);
982
+ if (c2 < 0 || is_line(c2)) break;
983
+ es_next(es);
984
+ }
985
+ e->was_comment = true;
986
+ break;
987
+ }
988
+ case '*': {
989
+ es_next(es);
990
+ es_next(es);
991
+ // JS_COMMENT_BLOCK: consume through */
992
+ while (true) {
993
+ int32_t c2 = es_peek(es, 0);
994
+ if (c2 < 0) return false; // MALFORMED_COMMENT
995
+ if (c2 == '*' && es_peek(es, 1) == '/') {
996
+ es_next(es);
997
+ es_next(es);
998
+ break;
999
+ }
1000
+ es_next(es);
1001
+ }
1002
+ break;
1003
+ }
1004
+ default:
1005
+ if (can_follow_division(es_prev_nonws(es))) {
1006
+ es_next(es);
1007
+ es_consume_ws(es);
1008
+ continue;
1009
+ } else {
1010
+ es_next(es);
1011
+ if (!sub_scan_regex(es)) return false;
1012
+ }
1013
+ break;
1014
+ }
1015
+ break;
1016
+ case '(':
1017
+ if (e->group_len >= sizeof(e->group)) return false;
1018
+ e->group[e->group_len++] = ')';
1019
+ es_next(es);
1020
+ break;
1021
+ case '[':
1022
+ if (e->group_len >= sizeof(e->group)) return false;
1023
+ e->group[e->group_len++] = ']';
1024
+ es_next(es);
1025
+ break;
1026
+ case '{':
1027
+ if (e->in_type && !e->force_type) {
1028
+ int64_t prev_pos = look_behind_while_ws(es, (int64_t)s->buf_len - 1);
1029
+ if (look_behind_for_operator(es, e, prev_pos) == -1) {
1030
+ e->in_type = false;
1031
+ }
1032
+ }
1033
+ if (e->group_len >= sizeof(e->group)) return false;
1034
+ e->group[e->group_len++] = '}';
1035
+ es_next(es);
1036
+ break;
1037
+ case '<':
1038
+ if (e->in_type) {
1039
+ if (e->group_len >= sizeof(e->group)) return false;
1040
+ e->group[e->group_len++] = '>';
1041
+ es_next(es);
1042
+ } else if (e->cfg.operators && !e->group_len) {
1043
+ es_next(es);
1044
+ es_consume_ws(es);
1045
+ continue;
1046
+ } else {
1047
+ es_next(es);
1048
+ }
1049
+ break;
1050
+
1051
+ case ')':
1052
+ case ']':
1053
+ case '}':
1054
+ case '>': {
1055
+ if (code == '>') {
1056
+ if (!e->in_type || es_prev_char(es) == '=') {
1057
+ es_next(es);
1058
+ break;
1059
+ }
1060
+ }
1061
+
1062
+ if (!e->group_len) return false; // mismatched group
1063
+
1064
+ uint8_t expected = e->group[--e->group_len];
1065
+ if ((int32_t)expected != code) return false; // mismatched group
1066
+
1067
+ es_next(es);
1068
+ break;
1069
+ }
1070
+
1071
+ default:
1072
+ es_next(es);
1073
+ break;
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ // Scan an expression on an existing stream (which may hold pending
1079
+ // lookahead). `*empty` reports whether nothing was consumed. The committed
1080
+ // token end is maintained by automark; callers must have marked the token
1081
+ // start/end fallback before queueing any lookahead.
1082
+ static bool scan_expr_es(EStream *es, ExprCfg cfg, bool *empty) {
1083
+ bool prev_automark = es->automark;
1084
+ uint32_t start = es->s->buf_len;
1085
+ if (start == 0) es->before_token = es->s->cur.prev_char;
1086
+ es->automark = true;
1087
+ es->start = start;
1088
+
1089
+ ExprState e;
1090
+ memset(&e, 0, sizeof(e));
1091
+ e.cfg = cfg;
1092
+ e.in_type = cfg.in_type;
1093
+ e.force_type = cfg.force_type;
1094
+
1095
+ bool ok = scan_expr_inner(es, &e);
1096
+ if (empty) *empty = es->s->buf_len == start;
1097
+ es->automark = prev_automark;
1098
+ return ok;
1099
+ }
1100
+
1101
+ // Scan a whole expression token on a fresh stream.
1102
+ static bool scan_expr_token(Scanner *s, TSLexer *lexer, ExprCfg cfg,
1103
+ bool *empty) {
1104
+ EStream es;
1105
+ es_init(&es, s, lexer, true);
1106
+ mark(s, lexer); // empty expression: zero-width token
1107
+ bool ok = scan_expr_es(&es, cfg, empty);
1108
+ es_free(&es);
1109
+ return ok;
1110
+ }
1111
+
1112
+ // Consume a "//" comment's chars up to (not including) the EOL/EOF.
1113
+ static void es_consume_line_comment(EStream *es) {
1114
+ es_next_mark(es); // /
1115
+ es_next_mark(es); // /
1116
+ while (true) {
1117
+ int32_t c = es_peek(es, 0);
1118
+ if (c < 0 || is_line(c)) break;
1119
+ es_next_mark(es);
1120
+ }
1121
+ }
1122
+
1123
+ // Consume a "/*" comment through its closing "*/"; false at EOF
1124
+ // (MALFORMED_COMMENT).
1125
+ static bool es_consume_block_comment(EStream *es) {
1126
+ es_next_mark(es); // /
1127
+ es_next_mark(es); // *
1128
+ while (true) {
1129
+ int32_t c = es_peek(es, 0);
1130
+ if (c < 0) return false;
1131
+ if (c == '*' && es_peek(es, 1) == '/') {
1132
+ es_next_mark(es);
1133
+ es_next_mark(es);
1134
+ return true;
1135
+ }
1136
+ es_next_mark(es);
1137
+ }
1138
+ }
1139
+
1140
+ // --------------------------------------------------------------------------
1141
+ // Tag bookkeeping
1142
+ // --------------------------------------------------------------------------
1143
+
1144
+ // Tag types follow the tags API (and the vscode tmLanguage grammar this
1145
+ // replaces): html void elements plus openTagOnly core tags are void;
1146
+ // text:true core tags are text; statement:true core tags (and class, kept
1147
+ // for class-API compatibility) are statements.
1148
+ static uint8_t classify_tag_name(Scanner *s) {
1149
+ static const char *const VOID_TAGS[] = {
1150
+ "area", "base", "br", "col", "hr", "embed", "img",
1151
+ "input", "link", "meta", "param", "source", "track", "wbr",
1152
+ "const", "debug", "id", "let", "lifecycle",
1153
+ "log", "return", NULL,
1154
+ };
1155
+ static const char *const TEXT_TAGS[] = {
1156
+ "script", "style", "html-script", "html-style", "html-comment", NULL,
1157
+ };
1158
+ static const char *const STATEMENT_TAGS[] = {
1159
+ "import", "export", "static", "class", "server", "client", NULL,
1160
+ };
1161
+ for (int i = 0; VOID_TAGS[i]; i++) {
1162
+ if (strlen(VOID_TAGS[i]) == s->buf_len &&
1163
+ memcmp(VOID_TAGS[i], s->buf, s->buf_len) == 0)
1164
+ return TAG_VOID;
1165
+ }
1166
+ for (int i = 0; TEXT_TAGS[i]; i++) {
1167
+ if (strlen(TEXT_TAGS[i]) == s->buf_len &&
1168
+ memcmp(TEXT_TAGS[i], s->buf, s->buf_len) == 0)
1169
+ return TAG_TEXT;
1170
+ }
1171
+ for (int i = 0; STATEMENT_TAGS[i]; i++) {
1172
+ if (strlen(STATEMENT_TAGS[i]) == s->buf_len &&
1173
+ memcmp(STATEMENT_TAGS[i], s->buf, s->buf_len) == 0)
1174
+ return TAG_STATEMENT;
1175
+ }
1176
+ return TAG_HTML;
1177
+ }
1178
+
1179
+ static bool push_tag(Scanner *s, bool concise) {
1180
+ if (s->tag_len == MAX_TAGS) return false;
1181
+ TagFrame *t = &s->tags[s->tag_len++];
1182
+ memset(t, 0, sizeof(*t));
1183
+ t->name_hash = HASH_INIT;
1184
+ t->full_hash = HASH_INIT;
1185
+ t->indent_len = s->cur.cur_indent_len;
1186
+ t->indent_hash = s->cur.cur_indent_hash;
1187
+ t->type = TAG_HTML;
1188
+ if (concise) t->flags |= TF_CONCISE;
1189
+ if (s->begin_mixed_mode || s->ending_mixed_at_eol) t->flags |= TF_BEGIN_MIXED;
1190
+ s->begin_mixed_mode = 0;
1191
+ s->ending_mixed_at_eol = 0;
1192
+
1193
+ s->tag_open = 1;
1194
+ s->tag_name_done = 0;
1195
+ s->in_shorthand = 0;
1196
+ s->adjacent_lt = 0;
1197
+ s->statement_pending = 0;
1198
+ s->in_attr_group = 0;
1199
+ s->attr_active = 0;
1200
+ s->attr_stage = ATTR_UNKNOWN;
1201
+ s->attr_has_args = 0;
1202
+ s->attr_has_type_params = 0;
1203
+ s->tag_has_attrs = 0;
1204
+ s->tag_has_args = 0;
1205
+ s->tag_has_params = 0;
1206
+ s->tag_has_shorthand_id = 0;
1207
+ s->tag_pending_types = 0;
1208
+ s->expect_method_open = 0;
1209
+ s->tag_comma_continue = 0;
1210
+ return true;
1211
+ }
1212
+
1213
+ static void pop_tag(Scanner *s) {
1214
+ if (!s->tag_len) return;
1215
+ TagFrame *t = &s->tags[--s->tag_len];
1216
+ if (t->flags & TF_BEGIN_MIXED) s->ending_mixed_at_eol = 1;
1217
+ }
1218
+
1219
+ static bool push_content(Scanner *s, uint8_t content_kind, uint8_t flags,
1220
+ uint16_t delimiter_len, uint16_t indent_len,
1221
+ uint32_t indent_hash, uint8_t fresh) {
1222
+ if (s->content_len == MAX_CONTENTS) return false;
1223
+ ContentFrame *f = &s->contents[s->content_len++];
1224
+ memset(f, 0, sizeof(*f));
1225
+ f->content = content_kind;
1226
+ f->flags = flags;
1227
+ f->delimiter_len = delimiter_len;
1228
+ f->indent_len = indent_len;
1229
+ f->indent_hash = indent_hash;
1230
+ f->fresh = fresh;
1231
+ return true;
1232
+ }
1233
+
1234
+ // beginHtmlBlock: the content kind depends on the active tag's type.
1235
+ static uint8_t block_content_kind(Scanner *s) {
1236
+ TagFrame *t = top_tag(s);
1237
+ return (t && t->type == TAG_TEXT) ? CONTENT_PARSED_TEXT : CONTENT_HTML;
1238
+ }
1239
+
1240
+ // --------------------------------------------------------------------------
1241
+ // Forward declarations
1242
+ // --------------------------------------------------------------------------
1243
+
1244
+ static bool scan_content(Scanner *s, TSLexer *lexer, const bool *valid);
1245
+ static bool scan_open_tag(Scanner *s, TSLexer *lexer, const bool *valid);
1246
+ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
1247
+ EStream *es);
1248
+ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
1249
+ TSSymbol *result);
1250
+
1251
+ static inline void skip_or_peek(Scanner *s, TSLexer *lexer, bool consume) {
1252
+ if (consume) skipc(s, lexer);
1253
+ else lexer->advance(lexer, false);
1254
+ }
1255
+
1256
+ // Skip whitespace and `//`/`/* */` comments, returning true if a `,` follows.
1257
+ // `consume` skips them as trivia, otherwise it is raw lookahead beyond the
1258
+ // committed token end. Mirrors the concise-mode eol scan in OPEN_TAG.
1259
+ static bool scan_line_attr_trivia(Scanner *s, TSLexer *lexer, bool consume) {
1260
+ for (;;) {
1261
+ if (at_eof(lexer)) return false;
1262
+ int32_t c = lexer->lookahead;
1263
+ if (is_ws(c)) {
1264
+ skip_or_peek(s, lexer, consume);
1265
+ } else if (c == '/') {
1266
+ skip_or_peek(s, lexer, consume);
1267
+ if (lexer->lookahead == '/') {
1268
+ // line comment
1269
+ skip_or_peek(s, lexer, consume);
1270
+ while (!at_eof(lexer) && !is_line(lexer->lookahead))
1271
+ skip_or_peek(s, lexer, consume);
1272
+ } else if (lexer->lookahead == '*') {
1273
+ // block comment
1274
+ int32_t prev = 0;
1275
+ skip_or_peek(s, lexer, consume);
1276
+ while (!at_eof(lexer) && !(prev == '*' && lexer->lookahead == '/')) {
1277
+ prev = lexer->lookahead;
1278
+ skip_or_peek(s, lexer, consume);
1279
+ }
1280
+ if (at_eof(lexer)) return false;
1281
+ skip_or_peek(s, lexer, consume);
1282
+ } else {
1283
+ return false;
1284
+ }
1285
+ } else {
1286
+ break;
1287
+ }
1288
+ }
1289
+ return lexer->lookahead == ',';
1290
+ }
1291
+
1292
+ // After most tokens inside a concise open tag, look ahead to see whether a
1293
+ // `,` follows (over whitespace and comments): if so the open tag continues on
1294
+ // the next line with another line attribute.
1295
+ static void concise_tag_epilogue(Scanner *s, TSLexer *lexer) {
1296
+ if (!s->tag_open || !is_concise(s) || s->in_attr_group) return;
1297
+ s->tag_comma_continue = scan_line_attr_trivia(s, lexer, false);
1298
+ }
1299
+
1300
+ // --------------------------------------------------------------------------
1301
+ // Open tag end transitions
1302
+ // --------------------------------------------------------------------------
1303
+
1304
+ static void open_tag_ended(Scanner *s, bool self_closed) {
1305
+ TagFrame *t = top_tag(s);
1306
+ s->tag_open = 0;
1307
+ s->attr_active = 0;
1308
+ s->statement_pending = 0;
1309
+ s->tag_comma_continue = 0;
1310
+ if (!t) return;
1311
+
1312
+ if (self_closed || t->type == TAG_VOID || t->type == TAG_STATEMENT) {
1313
+ s->pending_element_ends++;
1314
+ } else if (t->type == TAG_TEXT && !(t->flags & TF_CONCISE)) {
1315
+ // html mode <script>/<style>/<textarea>/<html-comment>
1316
+ push_content(s, CONTENT_PARSED_TEXT, 0, 0, 0, HASH_INIT, FRESH_NONE);
1317
+ }
1318
+ }
1319
+
1320
+ // --------------------------------------------------------------------------
1321
+ // Expression token configs
1322
+ // --------------------------------------------------------------------------
1323
+
1324
+ static ExprCfg cfg_enclosed(uint8_t term) {
1325
+ ExprCfg cfg;
1326
+ memset(&cfg, 0, sizeof(cfg));
1327
+ cfg.terminator = term;
1328
+ return cfg;
1329
+ }
1330
+
1331
+ static ExprCfg cfg_type_expr(void) {
1332
+ ExprCfg cfg = cfg_enclosed(TERM_CLOSE_ANGLE);
1333
+ cfg.in_type = true;
1334
+ cfg.force_type = true;
1335
+ return cfg;
1336
+ }
1337
+
1338
+ static ExprCfg cfg_attr_name(Scanner *s) {
1339
+ ExprCfg cfg;
1340
+ memset(&cfg, 0, sizeof(cfg));
1341
+ cfg.terminated_by_whitespace = true;
1342
+ cfg.terminator = is_concise(s)
1343
+ ? (s->in_attr_group ? TERM_CONCISE_GROUPED_ATTR_NAME
1344
+ : TERM_CONCISE_ATTR_NAME)
1345
+ : TERM_HTML_ATTR_NAME;
1346
+ return cfg;
1347
+ }
1348
+
1349
+ static ExprCfg cfg_attr_value(Scanner *s) {
1350
+ ExprCfg cfg;
1351
+ memset(&cfg, 0, sizeof(cfg));
1352
+ cfg.operators = true;
1353
+ cfg.terminated_by_whitespace = true;
1354
+ cfg.terminator = is_concise(s)
1355
+ ? (s->in_attr_group ? TERM_CONCISE_GROUPED_ATTR_VALUE
1356
+ : TERM_CONCISE_ATTR_VALUE)
1357
+ : TERM_HTML_ATTR_VALUE;
1358
+ return cfg;
1359
+ }
1360
+
1361
+ static ExprCfg cfg_tag_var(Scanner *s) {
1362
+ ExprCfg cfg;
1363
+ memset(&cfg, 0, sizeof(cfg));
1364
+ cfg.operators = true;
1365
+ cfg.terminated_by_whitespace = true;
1366
+ cfg.terminator = is_concise(s) ? TERM_CONCISE_TAG_VAR : TERM_HTML_TAG_VAR;
1367
+ return cfg;
1368
+ }
1369
+
1370
+ // --------------------------------------------------------------------------
1371
+ // Close tags
1372
+ // --------------------------------------------------------------------------
1373
+
1374
+ static bool scan_close_tag_name(Scanner *s, TSLexer *lexer, TSSymbol *result) {
1375
+ TagFrame *t = top_tag(s);
1376
+ if (!t) return false; // EXTRA_CLOSING_TAG
1377
+
1378
+ EStream es;
1379
+ es_init(&es, s, lexer, false);
1380
+ mark(s, lexer); // name may be empty (</>)
1381
+
1382
+ uint32_t h = HASH_INIT;
1383
+ uint32_t len = 0;
1384
+ while (true) {
1385
+ int32_t c = es_peek(&es, 0);
1386
+ if (c < 0) {
1387
+ es_free(&es);
1388
+ return false; // MALFORMED_CLOSE_TAG (EOF)
1389
+ }
1390
+ if (c == '>') break;
1391
+ h = hash_push(h, c);
1392
+ len++;
1393
+ es_next_mark(&es);
1394
+ }
1395
+ es_free(&es);
1396
+
1397
+ if (len > 0) {
1398
+ bool match;
1399
+ if (t->name_len > 0) {
1400
+ match = (len == t->name_len && h == t->name_hash) ||
1401
+ (len == t->full_len && h == t->full_hash);
1402
+ } else {
1403
+ match = (len == 3 && h == hash_str("div")) ||
1404
+ (len == t->full_len && h == t->full_hash);
1405
+ }
1406
+ if (!match) return false; // MISMATCHED_CLOSING_TAG
1407
+ }
1408
+
1409
+ *result = CLOSE_TAG_NAME;
1410
+ return true;
1411
+ }
1412
+
1413
+ static bool scan_close_tag_end(Scanner *s, TSLexer *lexer, TSSymbol *result) {
1414
+ if (at_eof(lexer) || lexer->lookahead != '>') return false;
1415
+ cursor_advance(&s->cur, '>');
1416
+ lexer->advance(lexer, false);
1417
+ mark(s, lexer);
1418
+
1419
+ // checkForClosingTag exits PARSED_TEXT_CONTENT.
1420
+ ContentFrame *f = top_content(s);
1421
+ if (f && f->content == CONTENT_PARSED_TEXT && !(f->flags & CF_IS_BLOCK) &&
1422
+ !(f->flags & CF_SINGLE_LINE) && f->delimiter_len == 0) {
1423
+ s->content_len--;
1424
+ }
1425
+
1426
+ s->pending_element_ends++;
1427
+ *result = CLOSE_TAG_END;
1428
+ return true;
1429
+ }
1430
+
1431
+ // --------------------------------------------------------------------------
1432
+ // Placeholders & scriptlets (grammar-position driven pieces)
1433
+ // --------------------------------------------------------------------------
1434
+
1435
+ static void accumulate_tag_name_hash(Scanner *s) {
1436
+ // Tag name interpolation source feeds the open tag's name/full hashes.
1437
+ if (!s->tag_open) return;
1438
+ TagFrame *t = top_tag(s);
1439
+ if (!t) return;
1440
+ if (!s->tag_name_done || s->in_shorthand) {
1441
+ for (uint32_t i = 0; i < s->buf_len; i++) {
1442
+ int32_t c = (int32_t)(uint8_t)s->buf[i];
1443
+ t->full_hash = hash_push(t->full_hash, c);
1444
+ t->full_len++;
1445
+ if (!s->tag_name_done) {
1446
+ t->name_hash = hash_push(t->name_hash, c);
1447
+ t->name_len++;
1448
+ }
1449
+ }
1450
+ t->flags |= TF_NAME_INTERP;
1451
+ if (!s->tag_name_done) t->type = TAG_HTML;
1452
+ }
1453
+ }
1454
+
1455
+ static bool scan_placeholder_expr(Scanner *s, TSLexer *lexer,
1456
+ TSSymbol *result) {
1457
+ bool empty = false;
1458
+ if (!scan_expr_token(s, lexer, cfg_enclosed(TERM_CLOSE_CURLY), &empty)) {
1459
+ return false;
1460
+ }
1461
+ if (empty) return false; // MALFORMED_PLACEHOLDER: expression missing
1462
+ accumulate_tag_name_hash(s);
1463
+ *result = PLACEHOLDER_EXPR;
1464
+ return true;
1465
+ }
1466
+
1467
+ static bool scan_placeholder_end(Scanner *s, TSLexer *lexer,
1468
+ TSSymbol *result) {
1469
+ if (at_eof(lexer) || lexer->lookahead != '}') return false;
1470
+ s->buf_len = 0;
1471
+ buf_push(s, '}');
1472
+ cursor_advance(&s->cur, '}');
1473
+ lexer->advance(lexer, false);
1474
+ mark(s, lexer);
1475
+ accumulate_tag_name_hash(s);
1476
+ *result = PLACEHOLDER_END;
1477
+ return true;
1478
+ }
1479
+
1480
+ static bool scan_scriptlet_body(Scanner *s, TSLexer *lexer, const bool *valid,
1481
+ TSSymbol *result) {
1482
+ // INLINE_SCRIPT.parse: consumeWhitespace, then `{` block or statement.
1483
+ if (valid[SCRIPTLET_BLOCK_OPEN] || valid[SCRIPTLET_EXPR]) {
1484
+ while (!at_eof(lexer) && is_ws(lexer->lookahead)) skipc(s, lexer);
1485
+ if (!at_eof(lexer) && lexer->lookahead == '{' &&
1486
+ valid[SCRIPTLET_BLOCK_OPEN]) {
1487
+ cursor_advance(&s->cur, '{');
1488
+ lexer->advance(lexer, false);
1489
+ mark(s, lexer);
1490
+ *result = SCRIPTLET_BLOCK_OPEN;
1491
+ return true;
1492
+ }
1493
+ if (!valid[SCRIPTLET_EXPR]) return false;
1494
+ ExprCfg cfg;
1495
+ memset(&cfg, 0, sizeof(cfg));
1496
+ cfg.operators = true;
1497
+ cfg.terminated_by_eol = true;
1498
+ if (!scan_expr_token(s, lexer, cfg, NULL)) return false;
1499
+ *result = SCRIPTLET_EXPR;
1500
+ return true;
1501
+ }
1502
+
1503
+ if (valid[SCRIPTLET_BLOCK_EXPR] || valid[SCRIPTLET_BLOCK_CLOSE]) {
1504
+ if (valid[SCRIPTLET_BLOCK_EXPR]) {
1505
+ bool empty = false;
1506
+ if (!scan_expr_token(s, lexer, cfg_enclosed(TERM_CLOSE_CURLY), &empty)) {
1507
+ return false;
1508
+ }
1509
+ if (!empty) {
1510
+ *result = SCRIPTLET_BLOCK_EXPR;
1511
+ return true;
1512
+ }
1513
+ // fall through to the close token in the same call
1514
+ }
1515
+ // "}" plus optional (whitespace then) ";"
1516
+ if (at_eof(lexer) || lexer->lookahead != '}') return false;
1517
+ EStream es;
1518
+ es_init(&es, s, lexer, false);
1519
+ es_next_mark(&es); // }
1520
+ // lookAtCharCodeAhead(0) === ";" || consumeWhitespaceIfBefore(";")
1521
+ uint32_t k = 0;
1522
+ while (true) {
1523
+ int32_t c = es_peek(&es, k);
1524
+ if (c == ';') {
1525
+ for (uint32_t i = 0; i <= k; i++) es_next_mark(&es);
1526
+ break;
1527
+ }
1528
+ if (c < 0 || !is_ws(c)) break;
1529
+ k++;
1530
+ }
1531
+ es_free(&es);
1532
+ *result = SCRIPTLET_BLOCK_CLOSE;
1533
+ return true;
1534
+ }
1535
+
1536
+ return false;
1537
+ }
1538
+
1539
+ // --------------------------------------------------------------------------
1540
+ // Delimited blocks
1541
+ // --------------------------------------------------------------------------
1542
+
1543
+ // Scan the BLOCK_OPEN token: a run of 2+ hyphens. Decides whether the block
1544
+ // is single-line or delimited and pushes the content frame. Operates on the
1545
+ // dispatcher's stream.
1546
+ static bool scan_block_open(Scanner *s, EStream *es, TSSymbol *result) {
1547
+ uint16_t indent_len = s->cur.cur_indent_len;
1548
+ uint32_t indent_hash = s->cur.cur_indent_hash;
1549
+
1550
+ uint32_t hyphens = 0;
1551
+ while (es_peek(es, 0) == '-') {
1552
+ es_next_mark(es);
1553
+ hyphens++;
1554
+ }
1555
+ if (hyphens < 2) return false;
1556
+
1557
+ if (s->block_after_tag) {
1558
+ // OPEN_TAG's "--" handling: extend this.indent to the next line's indent
1559
+ // when it is deeper.
1560
+ s->block_after_tag = 0;
1561
+ uint32_t k = 0;
1562
+ while (true) {
1563
+ int32_t c = es_peek(es, k);
1564
+ if (c < 0 || c == '\n') break;
1565
+ k++;
1566
+ }
1567
+ if (es_peek(es, k) == '\n') {
1568
+ uint32_t start = ++k;
1569
+ uint32_t h = HASH_INIT;
1570
+ while (true) {
1571
+ int32_t c = es_peek(es, k);
1572
+ if (c >= 0 && is_indent_code(c)) {
1573
+ h = hash_push(h, c);
1574
+ k++;
1575
+ } else {
1576
+ break;
1577
+ }
1578
+ }
1579
+ if (k - start > indent_len) {
1580
+ indent_len = (uint16_t)(k - start);
1581
+ indent_hash = h;
1582
+ }
1583
+ }
1584
+ }
1585
+
1586
+ // Single-line vs delimited: whitespace-only to EOL => delimited.
1587
+ uint8_t fresh = FRESH_DELIM_LINE;
1588
+ uint8_t flags = CF_IS_BLOCK;
1589
+ uint32_t k = 0;
1590
+ while (true) {
1591
+ int32_t c = es_peek(es, k);
1592
+ if (c < 0 || is_line(c)) break; // delimited
1593
+ if (!is_ws(c)) {
1594
+ fresh = FRESH_SINGLE;
1595
+ flags |= CF_SINGLE_LINE;
1596
+ break;
1597
+ }
1598
+ k++;
1599
+ }
1600
+
1601
+ if (!push_content(s, block_content_kind(s), flags,
1602
+ (flags & CF_SINGLE_LINE) ? 0 : (uint16_t)hyphens,
1603
+ indent_len, indent_hash, fresh)) {
1604
+ return false;
1605
+ }
1606
+ *result = BLOCK_OPEN;
1607
+ return true;
1608
+ }
1609
+
1610
+ // --------------------------------------------------------------------------
1611
+ // Content scanning (HTML_CONTENT / PARSED_TEXT_CONTENT / blocks)
1612
+ // --------------------------------------------------------------------------
1613
+
1614
+ // checkForPlaceholder outcome
1615
+ enum PhResult {
1616
+ PH_NO = 0, // not a boundary; the caller consumes one char as text
1617
+ PH_BOUNDARY, // a token boundary; *result/handled_now describe the token
1618
+ PH_CONSUMED, // not a boundary, but chars were consumed; continue as text
1619
+ };
1620
+
1621
+ // Inspect a `$`/`\\` at the current logical position (port of
1622
+ // checkForPlaceholder).
1623
+ // - pending text (buf non-empty) before a boundary: caller emits TEXT and
1624
+ // this re-runs with an empty buffer.
1625
+ // - no pending text: emits the placeholder start, or an `escape` node
1626
+ // covering the whole backslash run (the harness splits it into its
1627
+ // text/omitted halves); odd runs set suppress_placeholder so the literal
1628
+ // ${ that follows is consumed as text.
1629
+ // PH_NO with backslashes consumed: they were plain text; the caller's loop
1630
+ // continues (buf already holds them).
1631
+ static enum PhResult check_placeholder(Scanner *s, EStream *es,
1632
+ const bool *valid, bool *handled_now,
1633
+ TSSymbol *result) {
1634
+ *handled_now = false;
1635
+ if (s->suppress_placeholder) {
1636
+ // The escaped "${" is literal text: consume it and continue as text.
1637
+ if (es_peek(es, 0) == '$') {
1638
+ s->suppress_placeholder = 0;
1639
+ es_next_mark(es); // $
1640
+ if (es_peek(es, 0) == '!') es_next_mark(es); // !
1641
+ if (es_peek(es, 0) == '{') es_next_mark(es); // {
1642
+ return PH_CONSUMED;
1643
+ }
1644
+ return PH_NO;
1645
+ }
1646
+
1647
+ if (s->buf_len > 0) {
1648
+ // Verify with peeks only; the pending text token's end is already
1649
+ // committed before the run.
1650
+ uint32_t ahead = 0;
1651
+ while (es_peek(es, ahead) == '\\') ahead++;
1652
+ int32_t c = es_peek(es, ahead);
1653
+ if (c != '$') return PH_NO;
1654
+ uint32_t after = ahead + 1;
1655
+ if (es_peek(es, after) == '!') after++;
1656
+ if (es_peek(es, after) != '{') return PH_NO;
1657
+ return PH_BOUNDARY; // end TEXT; re-detect next call with empty buffer
1658
+ }
1659
+
1660
+ // No pending text: consume the backslash run first (peek(0) never queues,
1661
+ // so the marks land), then verify what follows.
1662
+ uint32_t run = 0;
1663
+ while (es_peek(es, 0) == '\\') {
1664
+ es_next_mark(es);
1665
+ run++;
1666
+ }
1667
+
1668
+ bool escape = true;
1669
+ uint32_t after = 1;
1670
+ bool is_placeholder = es_peek(es, 0) == '$';
1671
+ if (is_placeholder && es_peek(es, after) == '!') {
1672
+ escape = false;
1673
+ after++;
1674
+ }
1675
+ if (is_placeholder && es_peek(es, after) != '{') is_placeholder = false;
1676
+
1677
+ if (!is_placeholder || run == 0) {
1678
+ if (run > 0) return PH_CONSUMED; // plain backslashes: already in buf
1679
+ if (!is_placeholder) return PH_NO;
1680
+ // A placeholder start at the current position.
1681
+ es_next_mark(es); // $
1682
+ if (!escape) es_next_mark(es); // !
1683
+ es_next_mark(es); // {
1684
+ *handled_now = true;
1685
+ *result = escape ? PLACEHOLDER_START : PLACEHOLDER_START_RAW;
1686
+ return valid[*result] ? PH_BOUNDARY : PH_NO;
1687
+ }
1688
+
1689
+ // Backslashes followed by ${ / $!{: the whole run is one escape token.
1690
+ if (run % 2) s->suppress_placeholder = 1;
1691
+ *handled_now = true;
1692
+ *result = ESCAPE;
1693
+ return valid[ESCAPE] ? PH_BOUNDARY : PH_NO;
1694
+ }
1695
+
1696
+ // Try to match the parsed-text closing tag at the current position (peeks
1697
+ // only). checkForClosingTag in CLOSE_TAG.ts.
1698
+ static bool peek_parsed_close_tag(Scanner *s, EStream *es) {
1699
+ TagFrame *t = top_tag(s);
1700
+ if (!t) return false;
1701
+ if (es_peek(es, 0) != '<' || es_peek(es, 1) != '/') return false;
1702
+ if (es_peek(es, 2) == '>') return true; // </>
1703
+ uint32_t len = t->name_len;
1704
+ uint32_t h = HASH_INIT;
1705
+ for (uint32_t i = 0; i < len; i++) {
1706
+ int32_t c = es_peek(es, 2 + i);
1707
+ if (c < 0) return false;
1708
+ h = hash_push(h, c);
1709
+ }
1710
+ return h == t->name_hash && es_peek(es, 2 + len) == '>';
1711
+ }
1712
+
1713
+ // Scan one token of html / parsed-text content (or hand off to structure).
1714
+ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
1715
+ TSSymbol *result) {
1716
+ ContentFrame *f = top_content(s);
1717
+ bool parsed = f->content == CONTENT_PARSED_TEXT;
1718
+ bool delim = f->delimiter_len > 0;
1719
+ bool single = (f->flags & CF_SINGLE_LINE) != 0;
1720
+ bool mixed = !delim && !single && (f->flags & CF_IS_BLOCK) == 0;
1721
+
1722
+ // Fresh frames: skip past the BLOCK_OPEN line remainder.
1723
+ if (f->fresh == FRESH_SINGLE) {
1724
+ f->fresh = FRESH_NONE;
1725
+ if (!at_eof(lexer) && !is_line(lexer->lookahead)) skipc(s, lexer);
1726
+ } else if (f->fresh == FRESH_DELIM_LINE) {
1727
+ f->fresh = FRESH_NONE;
1728
+ while (!at_eof(lexer) && is_ws(lexer->lookahead) &&
1729
+ !is_line(lexer->lookahead)) {
1730
+ skipc(s, lexer);
1731
+ }
1732
+ // The first newline is not content (handleDelimitedBlockEOL first=true).
1733
+ if (!at_eof(lexer) && lexer->lookahead == '\r') skipc(s, lexer);
1734
+ if (!at_eof(lexer) && lexer->lookahead == '\n') skipc(s, lexer);
1735
+ }
1736
+
1737
+ // Delimited blocks: at the start of each line, skip the block indent.
1738
+ bool at_line_start = false;
1739
+ if (delim && s->cur.line_only_ws && s->cur.cur_indent_len == 0 &&
1740
+ !at_eof(lexer)) {
1741
+ at_line_start = true;
1742
+ uint16_t n = 0;
1743
+ while (n < f->indent_len && !at_eof(lexer) &&
1744
+ is_ws(lexer->lookahead) && !is_line(lexer->lookahead)) {
1745
+ skipc(s, lexer);
1746
+ n++;
1747
+ }
1748
+ }
1749
+
1750
+ EStream es;
1751
+ es_init(&es, s, lexer, false);
1752
+ mark(s, lexer);
1753
+
1754
+ // A closing delimiter at the start of a line (reachable when the newline
1755
+ // was skipped as the first line of the block).
1756
+ if (at_line_start && !at_eof(lexer) && lexer->lookahead == '-') {
1757
+ uint32_t k = 0;
1758
+ bool close_match = true;
1759
+ for (uint16_t i = 0; i < f->delimiter_len; i++) {
1760
+ if (es_peek(&es, k) != '-') {
1761
+ close_match = false;
1762
+ break;
1763
+ }
1764
+ k++;
1765
+ }
1766
+ if (close_match) {
1767
+ for (uint32_t i = 0; i < k; i++) es_next(&es);
1768
+ while (true) {
1769
+ int32_t c2 = es_peek(&es, 0);
1770
+ if (c2 < 0 || is_line(c2)) break;
1771
+ if (!is_ws(c2)) {
1772
+ es_free(&es);
1773
+ return false; // INVALID_CHARACTER after closing delimiter
1774
+ }
1775
+ es_next(&es);
1776
+ }
1777
+ es_mark(&es);
1778
+ es_free(&es);
1779
+ s->content_len--;
1780
+ *result = BLOCK_CLOSE;
1781
+ return valid[BLOCK_CLOSE];
1782
+ }
1783
+ }
1784
+
1785
+
1786
+ #define EMIT_TEXT_OR(fallthrough_block) \
1787
+ do { \
1788
+ if (s->buf_len > 0) { \
1789
+ es_mark(&es); \
1790
+ es_free(&es); \
1791
+ *result = TEXT; \
1792
+ return valid[TEXT]; \
1793
+ } \
1794
+ fallthrough_block \
1795
+ } while (0)
1796
+
1797
+ while (true) {
1798
+ int32_t c = es_peek(&es, 0);
1799
+
1800
+ // Newlines are consumed without marking so EOF trimming works; any other
1801
+ // char commits the token end up to here (text includes interior \n).
1802
+ if (c >= 0 && !is_line(c)) es_mark(&es);
1803
+
1804
+ if (c < 0) {
1805
+ // EOF inside content. The token (if any) ends at the last mark, which
1806
+ // excludes trailing newlines (htmlEOF trimming). Structure handling
1807
+ // happens on the next call.
1808
+ if (f->pstring_quote) {
1809
+ es_free(&es);
1810
+ return false; // EOF while parsing string expression
1811
+ }
1812
+ // Only the marked part counts: trailing newlines stay unmarked and a
1813
+ // zero-width text token would loop forever.
1814
+ bool have_text = es.marked_len > 0;
1815
+ if (have_text) {
1816
+ es_free(&es);
1817
+ *result = TEXT;
1818
+ return valid[TEXT];
1819
+ }
1820
+ if (f->flags & CF_IS_BLOCK) {
1821
+ // Trailing newlines before EOF: htmlEOF trims them from the text;
1822
+ // the block close absorbs them (block tokens produce no events).
1823
+ es_mark(&es);
1824
+ es_free(&es);
1825
+ s->content_len--;
1826
+ *result = BLOCK_CLOSE;
1827
+ return valid[BLOCK_CLOSE];
1828
+ }
1829
+ es_free(&es);
1830
+ return false;
1831
+ }
1832
+
1833
+ // Parsed string state: quotes parse placeholders but nothing else.
1834
+ if (f->pstring_quote) {
1835
+ if (c == (int32_t)f->pstring_quote) {
1836
+ f->pstring_quote = 0;
1837
+ es_next_mark(&es);
1838
+ continue;
1839
+ }
1840
+ if (c == '$' || c == '\\') {
1841
+ bool handled = false;
1842
+ enum PhResult r = check_placeholder(s, &es, valid, &handled, result);
1843
+ if (r == PH_BOUNDARY) {
1844
+ es_free(&es);
1845
+ if (!handled) *result = TEXT;
1846
+ return *result != TEXT || s->buf_len > 0;
1847
+ }
1848
+ if (r == PH_NO) es_next_mark(&es);
1849
+ continue;
1850
+ }
1851
+ es_next_mark(&es);
1852
+ continue;
1853
+ }
1854
+
1855
+ if (is_line(c)) {
1856
+ uint32_t len = (c == '\r' && es_peek(&es, 1) == '\n') ? 2 : 1;
1857
+
1858
+ if (single) {
1859
+ // handleDelimitedEOL: single line block ends at EOL.
1860
+ EMIT_TEXT_OR({
1861
+ es_free(&es);
1862
+ s->content_len--; // pop the block frame
1863
+ if (!valid[BLOCK_CLOSE]) return false;
1864
+ mark(s, lexer);
1865
+ *result = BLOCK_CLOSE;
1866
+ return true;
1867
+ });
1868
+ }
1869
+
1870
+ if (mixed && !parsed && (s->begin_mixed_mode || s->ending_mixed_at_eol)) {
1871
+ // Mixed-mode html line ends at this newline.
1872
+ EMIT_TEXT_OR({
1873
+ es_free(&es);
1874
+ s->begin_mixed_mode = 0;
1875
+ s->ending_mixed_at_eol = 0;
1876
+ s->content_len--;
1877
+ if (!scan_content(s, lexer, valid)) return false;
1878
+ *result = lexer->result_symbol;
1879
+ return true;
1880
+ });
1881
+ }
1882
+
1883
+ if (delim) {
1884
+ // handleDelimitedBlockEOL (first=false). Pending text ends here (the
1885
+ // harness merges adjacent text events, so the newline may land in
1886
+ // the next text token). A buffer of pure newlines is not emitted
1887
+ // yet: if the block ends, htmlEOF/dedent trimming drops it (the
1888
+ // BLOCK_CLOSE token absorbs the run instead).
1889
+ bool buf_has_content = false;
1890
+ for (uint32_t bi = 0; bi < s->buf_len; bi++) {
1891
+ if (!is_line((int32_t)(uint8_t)s->buf[bi])) {
1892
+ buf_has_content = true;
1893
+ break;
1894
+ }
1895
+ }
1896
+ if (buf_has_content) {
1897
+ es_mark(&es);
1898
+ es_free(&es);
1899
+ *result = TEXT;
1900
+ return valid[TEXT];
1901
+ }
1902
+
1903
+ if (s->buf_len > 0) {
1904
+ // A pure-newline run: if the line after this newline closes the
1905
+ // block, everything accumulated so far is still text (the parser
1906
+ // only excludes the newline directly before the delimiter line).
1907
+ uint32_t kc = len;
1908
+ uint32_t hc = HASH_INIT;
1909
+ bool close_ahead = true;
1910
+ for (uint16_t i = 0; i < f->indent_len; i++) {
1911
+ int32_t ci = es_peek(&es, kc);
1912
+ if (ci < 0 || is_line(ci) || !is_ws(ci)) {
1913
+ close_ahead = false;
1914
+ break;
1915
+ }
1916
+ hc = hash_push(hc, ci);
1917
+ kc++;
1918
+ }
1919
+ if (close_ahead && f->indent_len > 0 && hc != f->indent_hash) {
1920
+ close_ahead = false;
1921
+ }
1922
+ if (close_ahead) {
1923
+ for (uint16_t i = 0; i < f->delimiter_len; i++) {
1924
+ if (es_peek(&es, kc) != '-') {
1925
+ close_ahead = false;
1926
+ break;
1927
+ }
1928
+ kc++;
1929
+ }
1930
+ }
1931
+ if (close_ahead) {
1932
+ es_mark(&es);
1933
+ es_free(&es);
1934
+ *result = TEXT;
1935
+ return valid[TEXT];
1936
+ }
1937
+ }
1938
+
1939
+ if (f->indent_len == 0 && f->delimiter_len > 0) {
1940
+ // With an empty indent, only the closing delimiter check applies
1941
+ // (lookAheadFor("") is falsy in the parser); peek before consuming.
1942
+ bool close0 = true;
1943
+ for (uint16_t i = 0; i < f->delimiter_len; i++) {
1944
+ if (es_peek(&es, len + i) != '-') {
1945
+ close0 = false;
1946
+ break;
1947
+ }
1948
+ }
1949
+ if (!close0) {
1950
+ // text continues through the newline
1951
+ for (uint32_t i = 0; i < len; i++) es_next(&es);
1952
+ continue;
1953
+ }
1954
+ }
1955
+
1956
+ // Consume the newline first (the token can still become text), then
1957
+ // inspect the next line.
1958
+ for (uint32_t i = 0; i < len; i++) es_next(&es);
1959
+ es_mark(&es);
1960
+
1961
+ uint32_t k = 0;
1962
+ uint32_t h = HASH_INIT;
1963
+ bool indent_match = true;
1964
+ for (uint16_t i = 0; i < f->indent_len; i++) {
1965
+ int32_t ci = es_peek(&es, k);
1966
+ if (ci < 0 || is_line(ci) || !is_ws(ci)) {
1967
+ indent_match = false;
1968
+ break;
1969
+ }
1970
+ h = hash_push(h, ci);
1971
+ k++;
1972
+ }
1973
+ if (indent_match && f->indent_len > 0 && h != f->indent_hash) {
1974
+ indent_match = false;
1975
+ }
1976
+ bool close_match = indent_match;
1977
+ if (close_match) {
1978
+ for (uint16_t i = 0; i < f->delimiter_len; i++) {
1979
+ if (es_peek(&es, k) != '-') {
1980
+ close_match = false;
1981
+ break;
1982
+ }
1983
+ k++;
1984
+ }
1985
+ }
1986
+
1987
+ if (close_match) {
1988
+ // consume indent + delimiter into the BLOCK_CLOSE token (block
1989
+ // tokens produce no events), then require whitespace to EOL.
1990
+ for (uint32_t i = 0; i < k; i++) es_next(&es);
1991
+ while (true) {
1992
+ int32_t c2 = es_peek(&es, 0);
1993
+ if (c2 < 0 || is_line(c2)) break;
1994
+ if (!is_ws(c2)) {
1995
+ es_free(&es);
1996
+ return false; // INVALID_CHARACTER after closing delimiter
1997
+ }
1998
+ es_next(&es);
1999
+ }
2000
+ es_mark(&es);
2001
+ es_free(&es);
2002
+ s->content_len--;
2003
+ *result = BLOCK_CLOSE;
2004
+ return valid[BLOCK_CLOSE];
2005
+ }
2006
+
2007
+ if (at_eof(lexer) && es_drained(&es)) {
2008
+ // Trailing newline right before EOF: htmlEOF trims it; the block
2009
+ // close absorbs it instead of a text token.
2010
+ es_free(&es);
2011
+ s->content_len--;
2012
+ *result = BLOCK_CLOSE;
2013
+ return valid[BLOCK_CLOSE];
2014
+ }
2015
+
2016
+ if (f->indent_len == 0 || indent_match) {
2017
+ // Content continues on the next line: emit the newline as its own
2018
+ // text token; the next call skips the indent.
2019
+ es_free(&es);
2020
+ *result = TEXT;
2021
+ return valid[TEXT];
2022
+ }
2023
+
2024
+ // Not enough indentation: blank lines stay in the block, anything
2025
+ // else ends it.
2026
+ bool blank = true;
2027
+ uint32_t k2 = 0;
2028
+ while (true) {
2029
+ int32_t c2 = es_peek(&es, k2);
2030
+ if (c2 < 0 || is_line(c2)) break;
2031
+ if (!is_ws(c2)) {
2032
+ blank = false;
2033
+ break;
2034
+ }
2035
+ k2++;
2036
+ }
2037
+
2038
+ if (blank) continue; // the whitespace joins the text
2039
+
2040
+ // dedent: the block ends; the newline becomes the BLOCK_CLOSE token
2041
+ // (the parser leaves it for the concise dispatcher, but block tokens
2042
+ // produce no events so consuming it is equivalent).
2043
+ es_free(&es);
2044
+ s->content_len--;
2045
+ if (!valid[BLOCK_CLOSE]) return false;
2046
+ *result = BLOCK_CLOSE;
2047
+ return true;
2048
+ }
2049
+
2050
+ // Plain content: text crosses newlines (left unmarked for EOF trim).
2051
+ for (uint32_t i = 0; i < len; i++) es_next(&es);
2052
+ continue;
2053
+ }
2054
+
2055
+ if (c == '<') {
2056
+ if (parsed) {
2057
+ if (is_concise(s)) {
2058
+ // checkForClosingTag is skipped in concise mode: "</script>" in a
2059
+ // concise delimited block is plain text.
2060
+ es_next_mark(&es);
2061
+ continue;
2062
+ }
2063
+ if (s->buf_len > 0) {
2064
+ // Pending text must end before a matching close tag; peeking past
2065
+ // the mark is safe because the token end is already committed.
2066
+ if (peek_parsed_close_tag(s, &es)) {
2067
+ es_free(&es);
2068
+ *result = TEXT;
2069
+ return valid[TEXT];
2070
+ }
2071
+ es_next_mark(&es);
2072
+ continue;
2073
+ }
2074
+ // No pending text: consume "</" first so the token end can be
2075
+ // marked, then verify; an unmatched "</" just continues as text.
2076
+ if (es_peek(&es, 1) == '/') {
2077
+ es_next_mark(&es); // <
2078
+ es_next_mark(&es); // /
2079
+ TagFrame *t = top_tag(s);
2080
+ bool match = false;
2081
+ if (t) {
2082
+ if (es_peek(&es, 0) == '>') {
2083
+ match = true; // </>
2084
+ } else {
2085
+ uint32_t len = t->name_len;
2086
+ uint32_t h = HASH_INIT;
2087
+ bool ok = true;
2088
+ for (uint32_t i = 0; i < len; i++) {
2089
+ int32_t ci = es_peek(&es, i);
2090
+ if (ci < 0) {
2091
+ ok = false;
2092
+ break;
2093
+ }
2094
+ h = hash_push(h, ci);
2095
+ }
2096
+ match = ok && h == t->name_hash &&
2097
+ es_peek(&es, len) == '>';
2098
+ }
2099
+ }
2100
+ if (match) {
2101
+ es_free(&es);
2102
+ *result = CLOSE_TAG_START;
2103
+ return valid[CLOSE_TAG_START];
2104
+ }
2105
+ continue; // "</" is plain text
2106
+ }
2107
+ es_next_mark(&es);
2108
+ continue;
2109
+ }
2110
+
2111
+ int32_t n1 = es_peek(&es, 1);
2112
+
2113
+ if (n1 == '!') {
2114
+ if (es_peek(&es, 2) == '-' && es_peek(&es, 3) == '-') {
2115
+ // <!-- ... -> (the comment ends at the first "->")
2116
+ EMIT_TEXT_OR({
2117
+ es_next_mark(&es);
2118
+ es_next_mark(&es);
2119
+ es_next_mark(&es);
2120
+ es_next_mark(&es);
2121
+ while (true) {
2122
+ int32_t c2 = es_peek(&es, 0);
2123
+ if (c2 < 0) {
2124
+ es_free(&es);
2125
+ return false; // MALFORMED_COMMENT
2126
+ }
2127
+ if (c2 == '-' && es_peek(&es, 1) == '>') {
2128
+ es_next_mark(&es);
2129
+ es_next_mark(&es);
2130
+ break;
2131
+ }
2132
+ es_next_mark(&es);
2133
+ }
2134
+ es_free(&es);
2135
+ *result = HTML_COMMENT;
2136
+ return valid[HTML_COMMENT];
2137
+ });
2138
+ }
2139
+ // <![CDATA[ ... ]]>
2140
+ static const char CDATA_OPEN[] = "<![CDATA[";
2141
+ bool cdata = true;
2142
+ for (uint32_t i = 0; i < sizeof(CDATA_OPEN) - 1; i++) {
2143
+ if (es_peek(&es, i) != (int32_t)CDATA_OPEN[i]) {
2144
+ cdata = false;
2145
+ break;
2146
+ }
2147
+ }
2148
+ if (cdata) {
2149
+ EMIT_TEXT_OR({
2150
+ for (uint32_t i = 0; i < sizeof(CDATA_OPEN) - 1; i++)
2151
+ es_next_mark(&es);
2152
+ while (true) {
2153
+ int32_t c2 = es_peek(&es, 0);
2154
+ if (c2 < 0) {
2155
+ es_free(&es);
2156
+ return false; // MALFORMED_CDATA
2157
+ }
2158
+ if (c2 == ']' && es_peek(&es, 1) == ']' &&
2159
+ es_peek(&es, 2) == '>') {
2160
+ es_next_mark(&es);
2161
+ es_next_mark(&es);
2162
+ es_next_mark(&es);
2163
+ break;
2164
+ }
2165
+ es_next_mark(&es);
2166
+ }
2167
+ es_free(&es);
2168
+ *result = CDATA;
2169
+ return valid[CDATA];
2170
+ });
2171
+ }
2172
+ // <! ... > (doctype)
2173
+ EMIT_TEXT_OR({
2174
+ es_next_mark(&es);
2175
+ es_next_mark(&es);
2176
+ while (true) {
2177
+ int32_t c2 = es_peek(&es, 0);
2178
+ if (c2 < 0) {
2179
+ es_free(&es);
2180
+ return false; // MALFORMED_DOCUMENT_TYPE
2181
+ }
2182
+ es_next_mark(&es);
2183
+ if (c2 == '>') break;
2184
+ }
2185
+ es_free(&es);
2186
+ *result = DOCTYPE;
2187
+ return valid[DOCTYPE];
2188
+ });
2189
+ }
2190
+
2191
+ if (n1 == '?') {
2192
+ // <? ... > (declaration)
2193
+ EMIT_TEXT_OR({
2194
+ es_next_mark(&es);
2195
+ es_next_mark(&es);
2196
+ while (true) {
2197
+ int32_t c2 = es_peek(&es, 0);
2198
+ if (c2 < 0) {
2199
+ es_free(&es);
2200
+ return false; // MALFORMED_DECLARATION
2201
+ }
2202
+ es_next_mark(&es);
2203
+ if (c2 == '>') break;
2204
+ }
2205
+ es_free(&es);
2206
+ *result = DECLARATION;
2207
+ return valid[DECLARATION];
2208
+ });
2209
+ }
2210
+
2211
+ if (n1 == '/') {
2212
+ EMIT_TEXT_OR({
2213
+ es_next_mark(&es);
2214
+ es_next_mark(&es);
2215
+ es_free(&es);
2216
+ *result = CLOSE_TAG_START;
2217
+ return valid[CLOSE_TAG_START];
2218
+ });
2219
+ }
2220
+
2221
+ if (n1 == '>' || n1 == '<' || n1 < 0 || is_ws(n1)) {
2222
+ if (n1 < 0) {
2223
+ // `<` at EOF: the parser starts an open tag and errors at EOF.
2224
+ EMIT_TEXT_OR({
2225
+ es_free(&es);
2226
+ return false; // MALFORMED_OPEN_TAG
2227
+ });
2228
+ }
2229
+ es_next_mark(&es);
2230
+ continue;
2231
+ }
2232
+
2233
+ // open tag
2234
+ EMIT_TEXT_OR({
2235
+ es_next_mark(&es);
2236
+ es_free(&es);
2237
+ if (!valid[OPEN_TAG_START]) return false;
2238
+ if (!push_tag(s, false)) return false;
2239
+ *result = OPEN_TAG_START;
2240
+ return true;
2241
+ });
2242
+ }
2243
+
2244
+ if (!parsed && c == '$' && s->cur.line_only_ws) {
2245
+ // "$ " at the start of a line: an inline scriptlet.
2246
+ int32_t n1 = es_peek(&es, 1);
2247
+ if (n1 >= 0 && is_ws(n1)) {
2248
+ EMIT_TEXT_OR({
2249
+ es_next_mark(&es); // $
2250
+ es_free(&es);
2251
+ *result = SCRIPTLET_START;
2252
+ return valid[SCRIPTLET_START];
2253
+ });
2254
+ }
2255
+ }
2256
+
2257
+ if (c == '/') {
2258
+ int32_t n1 = es_peek(&es, 1);
2259
+ if (parsed) {
2260
+ if (n1 == '/') {
2261
+ // JS line comment within parsed text: part of the text, but the
2262
+ // closing tag is still recognized inside it.
2263
+ es_next_mark(&es);
2264
+ es_next_mark(&es);
2265
+ while (true) {
2266
+ int32_t c2 = es_peek(&es, 0);
2267
+ if (c2 < 0 || is_line(c2)) break;
2268
+ if (c2 == '<' && !is_concise(s) && peek_parsed_close_tag(s, &es))
2269
+ break;
2270
+ es_next_mark(&es);
2271
+ }
2272
+ continue;
2273
+ }
2274
+ if (n1 == '*') {
2275
+ // JS block comment: consumed blindly (no placeholders, no close
2276
+ // tags, delimiters not considered).
2277
+ es_next_mark(&es);
2278
+ es_next_mark(&es);
2279
+ while (true) {
2280
+ int32_t c2 = es_peek(&es, 0);
2281
+ if (c2 < 0) {
2282
+ es_free(&es);
2283
+ return false; // MALFORMED_COMMENT
2284
+ }
2285
+ if (c2 == '*' && es_peek(&es, 1) == '/') {
2286
+ es_next_mark(&es);
2287
+ es_next_mark(&es);
2288
+ break;
2289
+ }
2290
+ es_next_mark(&es);
2291
+ }
2292
+ continue;
2293
+ }
2294
+ es_next_mark(&es);
2295
+ continue;
2296
+ }
2297
+
2298
+ // html content: //... and /*...*/ comments when preceded by whitespace
2299
+ int32_t prev = es_prev_char(&es);
2300
+ if (is_ws(prev) && prev != 0 && (n1 == '/' || n1 == '*')) {
2301
+ EMIT_TEXT_OR({
2302
+ if (n1 == '/') {
2303
+ es_consume_line_comment(&es);
2304
+ es_free(&es);
2305
+ *result = LINE_COMMENT;
2306
+ return valid[LINE_COMMENT];
2307
+ }
2308
+ bool comment_ok = es_consume_block_comment(&es);
2309
+ es_free(&es);
2310
+ if (!comment_ok) return false; // MALFORMED_COMMENT
2311
+ *result = BLOCK_COMMENT;
2312
+ return valid[BLOCK_COMMENT];
2313
+ });
2314
+ }
2315
+ es_next_mark(&es);
2316
+ continue;
2317
+ }
2318
+
2319
+ if (parsed && c == '`') {
2320
+ es_next_mark(&es);
2321
+ if (!sub_scan_template(&es)) {
2322
+ es_free(&es);
2323
+ return false;
2324
+ }
2325
+ es_mark(&es);
2326
+ continue;
2327
+ }
2328
+
2329
+ if (parsed && (c == '"' || c == '\'')) {
2330
+ f->pstring_quote = (uint8_t)c;
2331
+ es_next_mark(&es);
2332
+ continue;
2333
+ }
2334
+
2335
+ if (c == '$' || c == '\\') {
2336
+ bool handled = false;
2337
+ enum PhResult r = check_placeholder(s, &es, valid, &handled, result);
2338
+ if (r == PH_BOUNDARY) {
2339
+ es_free(&es);
2340
+ if (!handled) *result = TEXT;
2341
+ return *result != TEXT || s->buf_len > 0;
2342
+ }
2343
+ if (r == PH_NO) es_next_mark(&es);
2344
+ continue;
2345
+ }
2346
+
2347
+ es_next_mark(&es);
2348
+ }
2349
+ #undef EMIT_TEXT_OR
2350
+ }
2351
+
2352
+ // --------------------------------------------------------------------------
2353
+ // Concise mode content (CONCISE_HTML_CONTENT)
2354
+ // --------------------------------------------------------------------------
2355
+
2356
+ static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2357
+ TSSymbol *result) {
2358
+ // Trivia: newlines and indentation.
2359
+ while (!at_eof(lexer)) {
2360
+ int32_t c = lexer->lookahead;
2361
+ if (is_line(c)) {
2362
+ s->semicolon_line = 0;
2363
+ skipc(s, lexer);
2364
+ continue;
2365
+ }
2366
+ if (is_ws(c)) {
2367
+ skipc(s, lexer);
2368
+ continue;
2369
+ }
2370
+ break;
2371
+ }
2372
+
2373
+ if (at_eof(lexer)) {
2374
+ // htmlEOF: close remaining concise tags.
2375
+ TagFrame *t = top_tag(s);
2376
+ if (t) {
2377
+ if (!(t->flags & TF_CONCISE)) return false; // MISSING_END_TAG
2378
+ if (!valid[ELEMENT_END]) return false;
2379
+ mark(s, lexer);
2380
+ pop_tag(s);
2381
+ *result = ELEMENT_END;
2382
+ return true;
2383
+ }
2384
+ return false;
2385
+ }
2386
+
2387
+ int32_t c = lexer->lookahead;
2388
+
2389
+ if (s->semicolon_line) {
2390
+ // Only comments may follow `tag;` on the same line (// and /* via the
2391
+ // JS comment states, <!-- via HTML_COMMENT).
2392
+ if (c != '/' && c != '<') return false; // INVALID_CODE_AFTER_SEMICOLON
2393
+ EStream es;
2394
+ es_init(&es, s, lexer, false);
2395
+ mark(s, lexer);
2396
+ if (c == '<') {
2397
+ if (es_peek(&es, 1) == '!' && es_peek(&es, 2) == '-' &&
2398
+ es_peek(&es, 3) == '-') {
2399
+ for (int i = 0; i < 4; i++) es_next_mark(&es);
2400
+ while (true) {
2401
+ int32_t c2 = es_peek(&es, 0);
2402
+ if (c2 < 0) {
2403
+ es_free(&es);
2404
+ return false; // MALFORMED_COMMENT
2405
+ }
2406
+ if (c2 == '-' && es_peek(&es, 1) == '>') {
2407
+ es_next_mark(&es);
2408
+ es_next_mark(&es);
2409
+ break;
2410
+ }
2411
+ es_next_mark(&es);
2412
+ }
2413
+ es_free(&es);
2414
+ *result = HTML_COMMENT;
2415
+ return valid[HTML_COMMENT];
2416
+ }
2417
+ es_free(&es);
2418
+ return false; // INVALID_CODE_AFTER_SEMICOLON
2419
+ }
2420
+ int32_t n1 = es_peek(&es, 1);
2421
+ bool ok = false;
2422
+ if (n1 == '/') {
2423
+ es_consume_line_comment(&es);
2424
+ *result = LINE_COMMENT;
2425
+ ok = valid[LINE_COMMENT];
2426
+ } else if (n1 == '*') {
2427
+ *result = BLOCK_COMMENT;
2428
+ ok = es_consume_block_comment(&es) && valid[BLOCK_COMMENT];
2429
+ }
2430
+ es_free(&es);
2431
+ return ok;
2432
+ }
2433
+
2434
+ uint16_t cur_indent = s->cur.cur_indent_len;
2435
+
2436
+ // Close concise tags whose indent is >= the current line's indent.
2437
+ TagFrame *t = top_tag(s);
2438
+ if (t && (t->flags & TF_CONCISE) && t->indent_len >= cur_indent) {
2439
+ if (!valid[ELEMENT_END]) return false;
2440
+ mark(s, lexer);
2441
+ pop_tag(s);
2442
+ *result = ELEMENT_END;
2443
+ return true;
2444
+ }
2445
+
2446
+ if (!t && cur_indent > 0 && c != '/') {
2447
+ return false; // INVALID_INDENTATION: extra indentation at the beginning
2448
+ }
2449
+
2450
+ if (t) {
2451
+ if (t->type == TAG_TEXT && c != '-') {
2452
+ return false; // INVALID_LINE_START: text tags only allow "-" lines
2453
+ }
2454
+ if (!(t->flags & TF_HAS_NESTED_INDENT)) {
2455
+ t->flags |= TF_HAS_NESTED_INDENT;
2456
+ t->nested_indent_len = cur_indent;
2457
+ t->nested_indent_hash = s->cur.cur_indent_hash;
2458
+ } else if (t->nested_indent_len != cur_indent ||
2459
+ t->nested_indent_hash != s->cur.cur_indent_hash) {
2460
+ return false; // INVALID_INDENTATION: does not match previous line
2461
+ }
2462
+ }
2463
+
2464
+ if (c == '<') {
2465
+ // Begin a mixed-mode html block; HTML_CONTENT handles the "<".
2466
+ s->begin_mixed_mode = 1;
2467
+ if (!push_content(s, block_content_kind(s), 0, 0, cur_indent,
2468
+ s->cur.cur_indent_hash, FRESH_NONE)) {
2469
+ return false;
2470
+ }
2471
+ return scan_frame_content(s, lexer, valid, result);
2472
+ }
2473
+
2474
+ EStream es;
2475
+ es_init(&es, s, lexer, false);
2476
+ mark(s, lexer);
2477
+ bool ok = false;
2478
+
2479
+ if (c == '$' && is_ws(es_peek(&es, 1)) && es_peek(&es, 1) >= 0) {
2480
+ es_next_mark(&es); // $
2481
+ *result = SCRIPTLET_START_CONCISE;
2482
+ ok = valid[SCRIPTLET_START_CONCISE];
2483
+ es_free(&es);
2484
+ return ok;
2485
+ }
2486
+
2487
+ if (c == '-') {
2488
+ if (es_peek(&es, 1) == '-') {
2489
+ ok = valid[BLOCK_OPEN] && scan_block_open(s, &es, result);
2490
+ }
2491
+ // else: INVALID_LINE_START: single hyphen
2492
+ es_free(&es);
2493
+ return ok;
2494
+ }
2495
+
2496
+ if (c == '/') {
2497
+ int32_t n1 = es_peek(&es, 1);
2498
+ if (n1 == '/') {
2499
+ es_consume_line_comment(&es);
2500
+ *result = LINE_COMMENT;
2501
+ ok = valid[LINE_COMMENT];
2502
+ } else if (n1 == '*') {
2503
+ ok = es_consume_block_comment(&es);
2504
+ if (ok) {
2505
+ // In concise mode, only whitespace may follow a comment block; the
2506
+ // comment event still fires before the error.
2507
+ uint32_t k = 0;
2508
+ while (true) {
2509
+ int32_t c2 = es_peek(&es, k);
2510
+ if (c2 < 0 || is_line(c2)) break;
2511
+ if (!is_ws(c2)) {
2512
+ s->fail_next = 1; // INVALID_CHARACTER after this token
2513
+ break;
2514
+ }
2515
+ k++;
2516
+ }
2517
+ }
2518
+ *result = BLOCK_COMMENT;
2519
+ ok = ok && valid[BLOCK_COMMENT];
2520
+ }
2521
+ // n1 anything else: INVALID_LINE_START
2522
+ es_free(&es);
2523
+ return ok;
2524
+ }
2525
+
2526
+ // A concise open tag.
2527
+ if (!push_tag(s, true)) {
2528
+ es_free(&es);
2529
+ return false;
2530
+ }
2531
+ ok = scan_open_tag_es(s, lexer, valid, &es);
2532
+ if (ok) *result = lexer->result_symbol;
2533
+ es_free(&es);
2534
+ return ok;
2535
+ }
2536
+
2537
+ // --------------------------------------------------------------------------
2538
+ // Open tags (OPEN_TAG / TAG_NAME / ATTRIBUTE)
2539
+ // --------------------------------------------------------------------------
2540
+
2541
+ // Is this character a TAG_NAME terminator?
2542
+ static bool is_tag_name_terminator(Scanner *s, int32_t c, int32_t next) {
2543
+ if (c < 0 || is_ws(c)) return true;
2544
+ switch (c) {
2545
+ case '=': case '(': case '/': case '|': case '<': case ',':
2546
+ return true;
2547
+ case ':':
2548
+ return next == '=';
2549
+ case ';': case '[':
2550
+ return is_concise(s);
2551
+ case '>':
2552
+ return !is_concise(s);
2553
+ default:
2554
+ return false;
2555
+ }
2556
+ }
2557
+
2558
+ // Finish the current tag name segment (called when a terminator is seen).
2559
+ static void finalize_tag_name(Scanner *s, int32_t stop_char) {
2560
+ if (!s->tag_name_done) {
2561
+ s->tag_name_done = 1;
2562
+ s->adjacent_lt = stop_char == '<';
2563
+ TagFrame *t = top_tag(s);
2564
+ if (t && t->type == TAG_STATEMENT && stop_char != '.' &&
2565
+ stop_char != '#') {
2566
+ s->statement_pending = 1;
2567
+ }
2568
+ }
2569
+ s->in_shorthand = 0;
2570
+ }
2571
+
2572
+ // Scan one tag name fragment / shorthand-start / interp-start token.
2573
+ // Returns 0 on failure, 1 when a token was produced, 2 when the name section
2574
+ // ended without producing a token (continue with tag-level dispatch).
2575
+ static int scan_tag_name_token(Scanner *s, const bool *valid, EStream *es,
2576
+ TSSymbol *result) {
2577
+ TagFrame *t = top_tag(s);
2578
+
2579
+ int32_t c = es_peek(es, 0);
2580
+ int32_t n1 = es_peek(es, 1);
2581
+
2582
+ if (c == '$' && n1 == '{') {
2583
+ es_next_mark(es);
2584
+ es_next_mark(es);
2585
+ if (t) {
2586
+ t->full_hash = hash_push(hash_push(t->full_hash, '$'), '{');
2587
+ t->full_len += 2;
2588
+ if (!s->tag_name_done) {
2589
+ t->name_hash = hash_push(hash_push(t->name_hash, '$'), '{');
2590
+ t->name_len += 2;
2591
+ t->flags |= TF_NAME_INTERP;
2592
+ }
2593
+ }
2594
+ *result = INTERP_START;
2595
+ return valid[INTERP_START] ? 1 : 0;
2596
+ }
2597
+
2598
+ if (c == '.' || c == '#') {
2599
+ bool had_name_parts =
2600
+ !s->tag_name_done &&
2601
+ (t ? (t->name_len > 0 || (t->flags & TF_NAME_INTERP)) : false);
2602
+ if (!s->tag_name_done && !had_name_parts && !s->in_shorthand) {
2603
+ // Empty tag name (eg `<.foo>`): emit the zero-width marker first and
2604
+ // stay in the name section so the shorthand follows.
2605
+ s->tag_name_done = 1;
2606
+ s->in_shorthand = 1;
2607
+ *result = TAG_NAME_EMPTY;
2608
+ return valid[TAG_NAME_EMPTY] ? 1 : 0;
2609
+ }
2610
+ finalize_tag_name(s, c);
2611
+ if (c == '#') {
2612
+ if (s->tag_has_shorthand_id) {
2613
+ return 0; // INVALID_TAG_SHORTHAND: multiple IDs
2614
+ }
2615
+ s->tag_has_shorthand_id = 1;
2616
+ }
2617
+ s->in_shorthand = 1;
2618
+ es_next_mark(es);
2619
+ if (t) {
2620
+ t->full_hash = hash_push(t->full_hash, c);
2621
+ t->full_len++;
2622
+ }
2623
+ *result = c == '#' ? SHORTHAND_ID_START : SHORTHAND_CLASS_START;
2624
+ return valid[*result] ? 1 : 0;
2625
+ }
2626
+
2627
+ if (c < 0 || is_line(c) || is_tag_name_terminator(s, c, n1)) {
2628
+ if (!s->tag_name_done && (!t || t->name_len == 0) &&
2629
+ !(t && (t->flags & TF_NAME_INTERP))) {
2630
+ finalize_tag_name(s, c);
2631
+ *result = TAG_NAME_EMPTY;
2632
+ return valid[TAG_NAME_EMPTY] ? 1 : 0;
2633
+ }
2634
+ finalize_tag_name(s, c);
2635
+ return 2;
2636
+ }
2637
+
2638
+ // A fragment: consume until a terminator / `.` / `#` / `${`.
2639
+ bool first_part = !s->tag_name_done && t && t->name_len == 0 &&
2640
+ !(t->flags & TF_NAME_INTERP) && !s->in_shorthand;
2641
+ while (true) {
2642
+ c = es_peek(es, 0);
2643
+ n1 = es_peek(es, 1);
2644
+ if (c < 0 || c == '.' || c == '#' || (c == '$' && n1 == '{') ||
2645
+ is_line(c) || is_tag_name_terminator(s, c, n1)) {
2646
+ break;
2647
+ }
2648
+ if (t) {
2649
+ t->full_hash = hash_push(t->full_hash, c);
2650
+ t->full_len++;
2651
+ if (!s->tag_name_done) {
2652
+ t->name_hash = hash_push(t->name_hash, c);
2653
+ t->name_len++;
2654
+ }
2655
+ }
2656
+ es_next_mark(es);
2657
+ }
2658
+
2659
+ if (first_part && t && !(c == '$' && n1 == '{')) {
2660
+ // The whole name was this single fragment: classify it.
2661
+ t->type = classify_tag_name(s);
2662
+ }
2663
+
2664
+ if (c != '.' && c != '#' && !(c == '$' && n1 == '{')) {
2665
+ finalize_tag_name(s, c);
2666
+ }
2667
+
2668
+ *result = TAG_NAME_FRAGMENT;
2669
+ return valid[TAG_NAME_FRAGMENT] ? 1 : 0;
2670
+ }
2671
+
2672
+ // Statement tags (import/export/static/class): the expression begins right
2673
+ // after the tag name with no trivia skipping (it consumes its own
2674
+ // whitespace, EOL continuation via indentation, etc).
2675
+ static bool scan_statement_tail(Scanner *s, TSLexer *lexer, const bool *valid,
2676
+ EStream *es) {
2677
+ TagFrame *t = top_tag(s);
2678
+ s->statement_pending = 0;
2679
+ if (!t) return false;
2680
+ if (!(t->flags & TF_CONCISE)) return false; // RESERVED_TAG_NAME
2681
+ if (s->tag_len > 1) return false; // ROOT_TAG_ONLY
2682
+ if (!valid[STATEMENT_EXPR]) return false;
2683
+
2684
+ ExprCfg cfg;
2685
+ memset(&cfg, 0, sizeof(cfg));
2686
+ cfg.operators = true;
2687
+ cfg.terminated_by_eol = true;
2688
+ cfg.consume_indented = true;
2689
+
2690
+ // lookAheadFor("declare " | "interface " | "type ") from pos + 1.
2691
+ static const char *const PREFIXES[] = {"declare ", "interface ", "type ",
2692
+ NULL};
2693
+ for (int i = 0; PREFIXES[i]; i++) {
2694
+ const char *p = PREFIXES[i];
2695
+ bool match_p = true;
2696
+ for (uint32_t j = 0; p[j]; j++) {
2697
+ if (es_peek(es, 1 + j) != (int32_t)p[j]) {
2698
+ match_p = false;
2699
+ break;
2700
+ }
2701
+ }
2702
+ if (match_p) {
2703
+ cfg.in_type = true;
2704
+ cfg.force_type = true;
2705
+ break;
2706
+ }
2707
+ }
2708
+
2709
+ if (!scan_expr_es(es, cfg, NULL)) return false;
2710
+ lexer->result_symbol = STATEMENT_EXPR;
2711
+ concise_tag_epilogue(s, lexer);
2712
+ return true;
2713
+ }
2714
+
2715
+ // The real open-tag scanner. `es` may hold pending lookahead when inherited
2716
+ // from the concise dispatcher.
2717
+ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
2718
+ EStream *es) {
2719
+ TagFrame *t = top_tag(s);
2720
+ if (!t) return false;
2721
+
2722
+ // ---- statement tags: the expression starts immediately after the name
2723
+ if (s->statement_pending) {
2724
+ return scan_statement_tail(s, lexer, valid, es);
2725
+ }
2726
+
2727
+ // ---- grammar-position expressions (no trivia before them)
2728
+ if (valid[ARGS_EXPR] && !valid[ATTR_NAME]) {
2729
+ bool empty = false;
2730
+ if (!scan_expr_es(es, cfg_enclosed(TERM_CLOSE_PAREN), &empty)) {
2731
+ return false;
2732
+ }
2733
+ if (!empty) {
2734
+ lexer->result_symbol = ARGS_EXPR;
2735
+ return true;
2736
+ }
2737
+ // empty: fall through to ARGS_CLOSE below
2738
+ }
2739
+ if (valid[ARGS_CLOSE] && !valid[ATTR_NAME]) {
2740
+ if (es_peek(es, 0) != ')') return false;
2741
+ es_next_mark(es);
2742
+ if (s->attr_active) {
2743
+ s->attr_has_args = 1;
2744
+ s->attr_stage = ATTR_ARGUMENT_STAGE;
2745
+ // An attribute cannot have both type parameters and arguments unless
2746
+ // a method body follows.
2747
+ if (s->attr_has_type_params) {
2748
+ uint32_t k = 0;
2749
+ int32_t c2;
2750
+ while ((c2 = es_peek(es, k)) >= 0 && is_ws(c2)) k++;
2751
+ if (c2 != '{') return false;
2752
+ }
2753
+ } else {
2754
+ s->tag_has_args = 1;
2755
+ // Tag args followed by "{" become a default-attribute method
2756
+ // (OPEN_TAG.return: consumeWhitespaceIfBefore("{")).
2757
+ uint32_t k = 0;
2758
+ int32_t c2;
2759
+ while ((c2 = es_peek(es, k)) >= 0 && is_ws(c2)) k++;
2760
+ if (c2 == '{') {
2761
+ s->attr_active = 1;
2762
+ s->attr_has_args = 1;
2763
+ s->attr_stage = ATTR_ARGUMENT_STAGE;
2764
+ s->tag_has_attrs = 1;
2765
+ }
2766
+ }
2767
+ lexer->result_symbol = ARGS_CLOSE;
2768
+ concise_tag_epilogue(s, lexer);
2769
+ return true;
2770
+ }
2771
+ // Tag params: the parser scans one opaque expression to the closing "|";
2772
+ // the scanner splits it into pattern/type/default pieces at depth-0
2773
+ // boundaries (",", ":", "=") so each piece can highlight properly. The
2774
+ // union of the sub-scans consumes exactly the parser's chars.
2775
+ if (valid[PARAM_PATTERN] && !valid[ATTR_NAME]) {
2776
+ if (es_peek(es, 0) != '|' || !valid[PARAMS_CLOSE]) {
2777
+ bool empty = false;
2778
+ if (!scan_expr_es(es, cfg_enclosed(TERM_PARAM_PATTERN), &empty)) {
2779
+ return false;
2780
+ }
2781
+ lexer->result_symbol = PARAM_PATTERN; // may be zero-width
2782
+ return true;
2783
+ }
2784
+ // "|" right away: an empty params list; fall through to PARAMS_CLOSE.
2785
+ }
2786
+ if ((valid[PARAM_COLON] || valid[PARAM_EQ] || valid[PARAM_COMMA] ||
2787
+ valid[PARAM_TYPE] || valid[PARAM_DEFAULT] || valid[PARAMS_CLOSE]) &&
2788
+ !valid[ATTR_NAME]) {
2789
+ if (valid[PARAM_TYPE]) {
2790
+ bool empty = false;
2791
+ if (!scan_expr_es(es, cfg_enclosed(TERM_PARAM_TYPE), &empty)) {
2792
+ return false;
2793
+ }
2794
+ lexer->result_symbol = PARAM_TYPE; // may be zero-width
2795
+ return true;
2796
+ }
2797
+ if (valid[PARAM_DEFAULT]) {
2798
+ bool empty = false;
2799
+ if (!scan_expr_es(es, cfg_enclosed(TERM_PARAM_DEFAULT), &empty)) {
2800
+ return false;
2801
+ }
2802
+ lexer->result_symbol = PARAM_DEFAULT; // may be zero-width
2803
+ return true;
2804
+ }
2805
+ int32_t pc = es_peek(es, 0);
2806
+ if (pc == ':' && valid[PARAM_COLON]) {
2807
+ es_next_mark(es);
2808
+ lexer->result_symbol = PARAM_COLON;
2809
+ return true;
2810
+ }
2811
+ if (pc == '=' && valid[PARAM_EQ]) {
2812
+ es_next_mark(es);
2813
+ lexer->result_symbol = PARAM_EQ;
2814
+ return true;
2815
+ }
2816
+ if (pc == ',' && valid[PARAM_COMMA]) {
2817
+ es_next_mark(es);
2818
+ lexer->result_symbol = PARAM_COMMA;
2819
+ return true;
2820
+ }
2821
+ if (pc == '|' && valid[PARAMS_CLOSE]) {
2822
+ es_next_mark(es);
2823
+ lexer->result_symbol = PARAMS_CLOSE;
2824
+ concise_tag_epilogue(s, lexer);
2825
+ return true;
2826
+ }
2827
+ // The sub-scans only stop at the separators above, so reaching here is
2828
+ // EOF inside an unterminated params list: MALFORMED_OPEN_TAG.
2829
+ return false;
2830
+ }
2831
+ if (valid[TYPE_EXPR] && !valid[ATTR_NAME]) {
2832
+ bool empty = false;
2833
+ if (!scan_expr_es(es, cfg_type_expr(), &empty)) return false;
2834
+ if (!empty) {
2835
+ lexer->result_symbol = TYPE_EXPR;
2836
+ return true;
2837
+ }
2838
+ }
2839
+ if (valid[TYPES_CLOSE] && !valid[ATTR_NAME]) {
2840
+ if (es_peek(es, 0) != '>') return false;
2841
+ es_next_mark(es);
2842
+ if (s->types_was_args) {
2843
+ // tagTypeArgs: nothing to validate.
2844
+ s->types_was_args = 0;
2845
+ } else if (s->attr_active) {
2846
+ // Attribute type params: "(" must follow.
2847
+ s->attr_has_type_params = 1;
2848
+ s->expect_method_open = 1;
2849
+ } else if (true) {
2850
+ // Tag-level type params must be followed by | or ( (INVALID_TAG_TYPES
2851
+ // fires when the types expression returns, before anything else).
2852
+ uint32_t k = 0;
2853
+ int32_t c2;
2854
+ while ((c2 = es_peek(es, k)) >= 0 && is_ws(c2)) k++;
2855
+ if (c2 == '|') {
2856
+ if (s->tag_has_params) return false;
2857
+ } else if (c2 == '(') {
2858
+ if (s->attr_has_type_params || s->tag_has_params || s->tag_has_args) {
2859
+ return false;
2860
+ }
2861
+ } else {
2862
+ return false; // INVALID_TAG_TYPES
2863
+ }
2864
+ s->tag_pending_types = 1;
2865
+ }
2866
+ s->adjacent_lt = 0;
2867
+ lexer->result_symbol = TYPES_CLOSE;
2868
+ concise_tag_epilogue(s, lexer);
2869
+ return true;
2870
+ }
2871
+ if (valid[METHOD_BODY_EXPR] && !valid[ATTR_NAME]) {
2872
+ bool empty = false;
2873
+ if (!scan_expr_es(es, cfg_enclosed(TERM_CLOSE_CURLY), &empty)) {
2874
+ return false;
2875
+ }
2876
+ if (!empty) {
2877
+ lexer->result_symbol = METHOD_BODY_EXPR;
2878
+ return true;
2879
+ }
2880
+ }
2881
+ if (valid[METHOD_BODY_CLOSE] && !valid[ATTR_NAME]) {
2882
+ if (es_peek(es, 0) != '}') return false;
2883
+ es_next_mark(es);
2884
+ // onAttrMethod fires and the attribute exits.
2885
+ s->attr_active = 0;
2886
+ s->attr_stage = ATTR_UNKNOWN;
2887
+ s->attr_has_args = 0;
2888
+ s->attr_has_type_params = 0;
2889
+ s->tag_has_attrs = 1;
2890
+ lexer->result_symbol = METHOD_BODY_CLOSE;
2891
+ concise_tag_epilogue(s, lexer);
2892
+ return true;
2893
+ }
2894
+ if (valid[VAR_PATTERN] && !valid[ATTR_NAME]) {
2895
+ int32_t c0 = es_peek(es, 0);
2896
+ if (c0 >= 0 && is_ws(c0)) return false; // MISSING_TAG_VARIABLE
2897
+ ExprCfg cfg = cfg_tag_var(s);
2898
+ cfg.split_at_type_colon = true;
2899
+ bool empty = false;
2900
+ if (!scan_expr_es(es, cfg, &empty)) return false;
2901
+ if (empty) return false; // MISSING_TAG_VARIABLE
2902
+ lexer->result_symbol = VAR_PATTERN;
2903
+ concise_tag_epilogue(s, lexer);
2904
+ return true;
2905
+ }
2906
+ if (valid[VAR_TYPE] && !valid[ATTR_NAME]) {
2907
+ // Resume the parser's expression scan after the ":" (in type mode; the
2908
+ // ":" lookbehind keeps whitespace continuation working).
2909
+ ExprCfg cfg = cfg_tag_var(s);
2910
+ cfg.in_type = true;
2911
+ cfg.resume_lookbehind = true;
2912
+ if (!scan_expr_es(es, cfg, NULL)) return false;
2913
+ lexer->result_symbol = VAR_TYPE; // may be zero-width
2914
+ concise_tag_epilogue(s, lexer);
2915
+ return true;
2916
+ }
2917
+ if (valid[VAR_COLON] && es_drained(es)) {
2918
+ // The pattern scan stopped before the ":" type annotation, possibly
2919
+ // with whitespace in between (the parser's operator continuation covers
2920
+ // `a : T`; in concise mode it does not cross newlines). The whitespace
2921
+ // is skipped via the raw lookahead so the no-colon path stays clean for
2922
+ // the normal trivia/dispatch flow.
2923
+ bool concise_var = is_concise(s);
2924
+ while (!at_eof(lexer) && is_ws(lexer->lookahead) &&
2925
+ (!concise_var || !is_line(lexer->lookahead))) {
2926
+ skipc(s, lexer);
2927
+ }
2928
+ if (!at_eof(lexer) && lexer->lookahead == ':' && es_peek(es, 1) != '=') {
2929
+ es_next_mark(es);
2930
+ lexer->result_symbol = VAR_COLON;
2931
+ return true;
2932
+ }
2933
+ // ":=" (or anything else) continues with the regular dispatch below.
2934
+ }
2935
+ if (valid[ATTR_VALUE_EXPR] && !valid[ATTR_NAME]) {
2936
+ // After = / := the parser consumes whitespace; after ... it does not.
2937
+ if (s->attr_stage == ATTR_VALUE_STAGE) {
2938
+ while (es_drained(es) && !at_eof(lexer) && is_ws(lexer->lookahead)) {
2939
+ skipc(s, lexer);
2940
+ }
2941
+ es_mark(es);
2942
+ }
2943
+ bool empty = false;
2944
+ if (!scan_expr_es(es, cfg_attr_value(s), &empty)) return false;
2945
+ if (empty) return false; // INVALID_ATTRIBUTE_VALUE / missing value
2946
+ // The attribute exits after its value.
2947
+ s->attr_active = 0;
2948
+ s->attr_stage = ATTR_UNKNOWN;
2949
+ s->attr_has_args = 0;
2950
+ s->attr_has_type_params = 0;
2951
+ lexer->result_symbol = ATTR_VALUE_EXPR;
2952
+ concise_tag_epilogue(s, lexer);
2953
+ return true;
2954
+ }
2955
+
2956
+ bool concise = is_concise(s);
2957
+
2958
+ // ---- trivia loop (only when no lookahead is pending)
2959
+ while (es_drained(es)) {
2960
+ if (at_eof(lexer)) {
2961
+ if (!s->tag_name_done || s->in_shorthand) finalize_tag_name(s, -1);
2962
+ s->statement_pending = 0; // an empty statement expression: no events
2963
+ if (!concise) return false; // MALFORMED_OPEN_TAG: EOF
2964
+ if (s->in_attr_group) return false; // EOF within attribute group
2965
+ if (!valid[CONCISE_OPEN_TAG_END]) return false;
2966
+ mark(s, lexer);
2967
+ open_tag_ended(s, false);
2968
+ lexer->result_symbol = CONCISE_OPEN_TAG_END;
2969
+ return true;
2970
+ }
2971
+
2972
+ int32_t cc = lexer->lookahead;
2973
+
2974
+ if ((!s->tag_name_done || s->in_shorthand) &&
2975
+ (is_ws(cc) || cc == ',')) {
2976
+ // TAG_NAME terminates at whitespace/comma before any trivia skipping.
2977
+ finalize_tag_name(s, cc);
2978
+ if (s->statement_pending) {
2979
+ return scan_statement_tail(s, lexer, valid, es);
2980
+ }
2981
+ continue;
2982
+ }
2983
+
2984
+ if (is_line(cc)) {
2985
+ s->attr_active = 0;
2986
+ s->attr_stage = ATTR_UNKNOWN;
2987
+ s->attr_has_args = 0;
2988
+ s->attr_has_type_params = 0;
2989
+ if (!concise || s->in_attr_group) {
2990
+ skipc(s, lexer);
2991
+ continue;
2992
+ }
2993
+ if (s->tag_comma_continue) {
2994
+ // comma continues the open tag with another line attribute
2995
+ if (scan_line_attr_trivia(s, lexer, true)) skipc(s, lexer);
2996
+ while (!at_eof(lexer) && is_ws(lexer->lookahead)) skipc(s, lexer);
2997
+ s->tag_comma_continue = 0;
2998
+ continue;
2999
+ }
3000
+ if (!valid[CONCISE_OPEN_TAG_END]) return false;
3001
+ mark(s, lexer); // zero-width at the newline
3002
+ open_tag_ended(s, false);
3003
+ lexer->result_symbol = CONCISE_OPEN_TAG_END;
3004
+ return true;
3005
+ }
3006
+
3007
+ if (is_ws(cc)) {
3008
+ skipc(s, lexer);
3009
+ continue;
3010
+ }
3011
+
3012
+ if (cc == ',') {
3013
+ skipc(s, lexer);
3014
+ while (!at_eof(lexer) && is_ws(lexer->lookahead)) skipc(s, lexer);
3015
+ continue;
3016
+ }
3017
+
3018
+ break;
3019
+ }
3020
+
3021
+ es_mark(es); // the token starts here
3022
+ int32_t c = es_peek(es, 0);
3023
+ #ifdef TSMARKO_DEBUG
3024
+ fprintf(stderr,
3025
+ "[open-tag dispatch c=%c active=%d stage=%d args=%d expect=%d nd=%d ish=%d]\n",
3026
+ c < 0 ? '?' : (char)c, s->attr_active, s->attr_stage,
3027
+ s->attr_has_args, s->expect_method_open, s->tag_name_done,
3028
+ s->in_shorthand);
3029
+ #endif
3030
+ if (c < 0) return false;
3031
+
3032
+ // ---- tag name section (TAG_NAME handles its chars before OPEN_TAG does)
3033
+ if (!s->tag_name_done || s->in_shorthand) {
3034
+ TSSymbol name_sym = 0;
3035
+ int r = scan_tag_name_token(s, valid, es, &name_sym);
3036
+ if (r == 0) return false;
3037
+ if (r == 1) {
3038
+ lexer->result_symbol = name_sym;
3039
+ concise_tag_epilogue(s, lexer);
3040
+ return true;
3041
+ }
3042
+ // r == 2: the name section ended without a token; redispatch.
3043
+ return scan_open_tag_es(s, lexer, valid, es);
3044
+ }
3045
+
3046
+ // ---- concise structure
3047
+ if (concise) {
3048
+ if (c == ';' && !s->in_attr_group) {
3049
+ if (!valid[CONCISE_OPEN_TAG_END]) return false;
3050
+ es_next_mark(es);
3051
+ s->semicolon_line = 1;
3052
+ open_tag_ended(s, false);
3053
+ lexer->result_symbol = CONCISE_OPEN_TAG_END;
3054
+ return true;
3055
+ }
3056
+ if (c == '-' && s->tag_name_done) {
3057
+ // OPEN_TAG sees "-" whenever an attribute is not mid-expression:
3058
+ // "--" ends the tag and begins a delimited block; "-" alone is an
3059
+ // error inside an open tag.
3060
+ if (es_peek(es, 1) != '-') {
3061
+ return false; // '"-" not allowed as first character of attr name'
3062
+ }
3063
+ s->attr_active = 0;
3064
+ s->attr_stage = ATTR_UNKNOWN;
3065
+ s->attr_has_args = 0;
3066
+ s->attr_has_type_params = 0;
3067
+ if (s->in_attr_group) return false; // group was not properly ended
3068
+ if (!valid[CONCISE_OPEN_TAG_END]) return false;
3069
+ // zero-width at the first hyphen (the mark from the trivia loop).
3070
+ s->block_after_tag = 1;
3071
+ open_tag_ended(s, false);
3072
+ lexer->result_symbol = CONCISE_OPEN_TAG_END;
3073
+ return true;
3074
+ }
3075
+ if (c == '[' && s->tag_name_done) {
3076
+ // OPEN_TAG handles "[" before attribute dispatch in concise mode.
3077
+ if (s->in_attr_group) return false; // nested group: MALFORMED_OPEN_TAG
3078
+ if (!valid[ATTR_GROUP_OPEN]) return false;
3079
+ s->attr_active = 0;
3080
+ s->attr_stage = ATTR_UNKNOWN;
3081
+ s->attr_has_args = 0;
3082
+ s->attr_has_type_params = 0;
3083
+ es_next_mark(es);
3084
+ s->in_attr_group = 1;
3085
+ lexer->result_symbol = ATTR_GROUP_OPEN;
3086
+ return true;
3087
+ }
3088
+ if (c == ']') {
3089
+ if (!s->in_attr_group) return false;
3090
+ if (!valid[ATTR_GROUP_CLOSE]) return false;
3091
+ s->attr_active = 0;
3092
+ s->attr_stage = ATTR_UNKNOWN;
3093
+ s->attr_has_args = 0;
3094
+ s->attr_has_type_params = 0;
3095
+ es_next_mark(es);
3096
+ s->in_attr_group = 0;
3097
+ lexer->result_symbol = ATTR_GROUP_CLOSE;
3098
+ concise_tag_epilogue(s, lexer);
3099
+ return true;
3100
+ }
3101
+ } else {
3102
+ if (c == '>') {
3103
+ if (!valid[OPEN_TAG_END]) return false;
3104
+ es_next_mark(es);
3105
+ open_tag_ended(s, false);
3106
+ lexer->result_symbol = OPEN_TAG_END;
3107
+ return true;
3108
+ }
3109
+ }
3110
+
3111
+ // ---- "/" disambiguation: />, //, /*, tag var, attr content
3112
+ if (c == '/') {
3113
+ int32_t n1 = es_peek(es, 1);
3114
+
3115
+ if (!concise && n1 == '>') {
3116
+ if (!valid[OPEN_TAG_END_SELF]) return false;
3117
+ es_next_mark(es);
3118
+ es_next_mark(es);
3119
+ open_tag_ended(s, true);
3120
+ lexer->result_symbol = OPEN_TAG_END_SELF;
3121
+ return true;
3122
+ }
3123
+
3124
+ if (n1 == '/' || n1 == '*') {
3125
+ // Comments inside open tags produce no events: a hidden token.
3126
+ if (!valid[TAG_COMMENT]) return false;
3127
+ if (n1 == '/') {
3128
+ es_consume_line_comment(es);
3129
+ } else if (!es_consume_block_comment(es)) {
3130
+ return false; // MALFORMED_COMMENT
3131
+ }
3132
+ lexer->result_symbol = TAG_COMMENT;
3133
+ concise_tag_epilogue(s, lexer);
3134
+ return true;
3135
+ }
3136
+ }
3137
+
3138
+ if (s->expect_method_open && c != '(') {
3139
+ #ifdef TSMARKO_DEBUG
3140
+ fprintf(stderr, "[expect_method_open fail c=%c]\n", (char)c);
3141
+ #endif
3142
+ return false; // INVALID_ATTR_TYPE_PARAMS
3143
+ }
3144
+ if (s->tag_pending_types) {
3145
+ if (c == '|') {
3146
+ if (s->tag_has_params) return false; // INVALID_TAG_TYPES
3147
+ s->tag_pending_types = 0;
3148
+ } else if (c == '(') {
3149
+ if (s->attr_has_type_params || s->tag_has_params || s->tag_has_args) {
3150
+ return false; // INVALID_TAG_TYPES
3151
+ }
3152
+ s->tag_pending_types = 0;
3153
+ s->attr_active = 1;
3154
+ s->attr_has_type_params = 1;
3155
+ s->attr_stage = ATTR_UNKNOWN;
3156
+ } else {
3157
+ return false; // INVALID_TAG_TYPES
3158
+ }
3159
+ }
3160
+
3161
+ // ---- attribute / tag-part dispatch
3162
+ bool attr_context = s->tag_has_attrs || s->attr_active;
3163
+
3164
+ if (!attr_context) {
3165
+ switch (c) {
3166
+ case '/': {
3167
+ s->adjacent_lt = 0;
3168
+ if (!valid[TAG_VAR_START]) return false;
3169
+ es_next_mark(es);
3170
+ lexer->result_symbol = TAG_VAR_START;
3171
+ return true;
3172
+ }
3173
+ case '(': {
3174
+ if (s->tag_has_args) {
3175
+ return false; // INVALID_TAG_ARGUMENT: only one argument
3176
+ }
3177
+ s->adjacent_lt = 0;
3178
+ if (!valid[ARGS_OPEN]) return false;
3179
+ es_next_mark(es);
3180
+ lexer->result_symbol = ARGS_OPEN;
3181
+ return true;
3182
+ }
3183
+ case '|': {
3184
+ if (s->tag_has_params) return false; // INVALID_TAG_PARAMS
3185
+ s->adjacent_lt = 0;
3186
+ s->tag_has_params = 1;
3187
+ if (!valid[PARAMS_OPEN]) return false;
3188
+ es_next_mark(es);
3189
+ lexer->result_symbol = PARAMS_OPEN;
3190
+ return true;
3191
+ }
3192
+ case '<': {
3193
+ bool adjacent = s->adjacent_lt != 0;
3194
+ s->adjacent_lt = 0;
3195
+ s->types_was_args = adjacent;
3196
+ TSSymbol open_sym = adjacent ? TYPE_ARGS_OPEN : TYPE_PARAMS_OPEN;
3197
+ if (!valid[open_sym]) return false;
3198
+ es_next_mark(es);
3199
+ lexer->result_symbol = open_sym;
3200
+ return true;
3201
+ }
3202
+ default:
3203
+ s->tag_has_attrs = 1;
3204
+ s->attr_active = 1;
3205
+ s->attr_stage = ATTR_UNKNOWN;
3206
+ s->attr_has_args = 0;
3207
+ s->attr_has_type_params = 0;
3208
+ break;
3209
+ }
3210
+ } else if (!s->attr_active) {
3211
+ s->attr_active = 1;
3212
+ s->attr_stage = ATTR_UNKNOWN;
3213
+ s->attr_has_args = 0;
3214
+ s->attr_has_type_params = 0;
3215
+ }
3216
+
3217
+ s->adjacent_lt = 0;
3218
+
3219
+ // ---- ATTRIBUTE.parse dispatch
3220
+ if (c == '=') {
3221
+ if (!valid[ATTR_EQ]) return false;
3222
+ es_next_mark(es);
3223
+ s->attr_stage = ATTR_VALUE_STAGE;
3224
+ lexer->result_symbol = ATTR_EQ;
3225
+ return true;
3226
+ }
3227
+ if (c == ':' && es_peek(es, 1) == '=') {
3228
+ if (!valid[ATTR_BOUND_EQ]) return false;
3229
+ es_next_mark(es);
3230
+ es_next_mark(es);
3231
+ s->attr_stage = ATTR_VALUE_STAGE;
3232
+ lexer->result_symbol = ATTR_BOUND_EQ;
3233
+ return true;
3234
+ }
3235
+ if (c == '.' && es_peek(es, 1) == '.' && es_peek(es, 2) == '.') {
3236
+ if (!valid[ATTR_SPREAD_START]) return false;
3237
+ es_next_mark(es);
3238
+ es_next_mark(es);
3239
+ es_next_mark(es);
3240
+ s->attr_stage = ATTR_BLOCK_STAGE; // marker: no ws-skip before the value
3241
+ lexer->result_symbol = ATTR_SPREAD_START;
3242
+ return true;
3243
+ }
3244
+ if (c == '(') {
3245
+ if (s->attr_has_args) return false; // one set of arguments only
3246
+ if (!valid[ARGS_OPEN]) return false;
3247
+ s->expect_method_open = 0;
3248
+ es_next_mark(es);
3249
+ lexer->result_symbol = ARGS_OPEN;
3250
+ return true;
3251
+ }
3252
+ if (c == '<' && s->attr_stage == ATTR_NAME_STAGE) {
3253
+ if (!valid[TYPE_PARAMS_OPEN]) return false;
3254
+ s->types_was_args = 0;
3255
+ es_next_mark(es);
3256
+ lexer->result_symbol = TYPE_PARAMS_OPEN;
3257
+ return true;
3258
+ }
3259
+ if (c == '{' && s->attr_has_args) {
3260
+ if (!valid[METHOD_BODY_OPEN]) return false;
3261
+ s->expect_method_open = 0;
3262
+ es_next_mark(es);
3263
+ lexer->result_symbol = METHOD_BODY_OPEN;
3264
+ return true;
3265
+ }
3266
+
3267
+ if (s->attr_stage != ATTR_UNKNOWN) {
3268
+ // The attribute ends; what follows starts a fresh attribute.
3269
+ s->attr_active = 1;
3270
+ s->attr_stage = ATTR_UNKNOWN;
3271
+ s->attr_has_args = 0;
3272
+ s->attr_has_type_params = 0;
3273
+ s->tag_has_attrs = 1;
3274
+ }
3275
+
3276
+ if (c == '<') return false; // INVALID_ATTRIBUTE_NAME
3277
+ {
3278
+ bool empty = false;
3279
+ if (!scan_expr_es(es, cfg_attr_name(s), &empty)) return false;
3280
+ if (empty) return false;
3281
+ if (!valid[ATTR_NAME]) return false;
3282
+ s->attr_stage = ATTR_NAME_STAGE;
3283
+ lexer->result_symbol = ATTR_NAME;
3284
+ concise_tag_epilogue(s, lexer);
3285
+ return true;
3286
+ }
3287
+ }
3288
+
3289
+ static bool scan_open_tag(Scanner *s, TSLexer *lexer, const bool *valid) {
3290
+ EStream es;
3291
+ es_init(&es, s, lexer, false);
3292
+ mark(s, lexer);
3293
+ bool ok = scan_open_tag_es(s, lexer, valid, &es);
3294
+ es_free(&es);
3295
+ return ok;
3296
+ }
3297
+
3298
+ // --------------------------------------------------------------------------
3299
+ // Content dispatch
3300
+ // --------------------------------------------------------------------------
3301
+
3302
+ static bool scan_content(Scanner *s, TSLexer *lexer, const bool *valid) {
3303
+ TSSymbol result = 0;
3304
+
3305
+ if (s->block_after_tag) {
3306
+ // A concise open tag ended at "--": BEGIN_DELIMITED_HTML_BLOCK follows
3307
+ // immediately (the concise dispatcher never sees these hyphens).
3308
+ if (!valid[BLOCK_OPEN]) return false;
3309
+ EStream es;
3310
+ es_init(&es, s, lexer, false);
3311
+ mark(s, lexer);
3312
+ bool ok = scan_block_open(s, &es, &result);
3313
+ es_free(&es);
3314
+ if (!ok) return false;
3315
+ lexer->result_symbol = result;
3316
+ return true;
3317
+ }
3318
+
3319
+ ContentFrame *f = top_content(s);
3320
+
3321
+ if (!f) {
3322
+ if (!scan_concise(s, lexer, valid, &result)) return false;
3323
+ lexer->result_symbol = result;
3324
+ return true;
3325
+ }
3326
+
3327
+ // EOF handling for frames.
3328
+ if (at_eof(lexer) && f->fresh == FRESH_NONE) {
3329
+ if (f->flags & CF_IS_BLOCK) {
3330
+ if (!valid[BLOCK_CLOSE]) return false;
3331
+ mark(s, lexer);
3332
+ s->content_len--;
3333
+ lexer->result_symbol = BLOCK_CLOSE;
3334
+ return true;
3335
+ }
3336
+ if (f->content == CONTENT_PARSED_TEXT) {
3337
+ // <script> etc reached EOF: MISSING_END_TAG
3338
+ return false;
3339
+ }
3340
+ // mixed html line at EOF: pop silently and continue.
3341
+ s->begin_mixed_mode = 0;
3342
+ s->ending_mixed_at_eol = 0;
3343
+ s->content_len--;
3344
+ return scan_content(s, lexer, valid);
3345
+ }
3346
+
3347
+ if (!scan_frame_content(s, lexer, valid, &result)) return false;
3348
+ lexer->result_symbol = result;
3349
+ return true;
3350
+ }
3351
+
3352
+ // --------------------------------------------------------------------------
3353
+ // Entry points
3354
+ // --------------------------------------------------------------------------
3355
+
3356
+ static bool scan_main(Scanner *s, TSLexer *lexer, const bool *valid) {
3357
+ if (valid[ERROR_SENTINEL]) return false; // error recovery: give up
3358
+ if (s->fail_next) return false; // deferred error
3359
+
3360
+ if (!s->started) {
3361
+ if (!at_eof(lexer) && lexer->lookahead == 0xFEFF) skipc(s, lexer);
3362
+ s->started = 1;
3363
+ s->cur.line_only_ws = 1;
3364
+ s->cur.cur_indent_hash = HASH_INIT;
3365
+ }
3366
+
3367
+ // Structural pendings first.
3368
+ if (s->pending_element_ends) {
3369
+ if (!valid[ELEMENT_END]) return false;
3370
+ mark(s, lexer);
3371
+ s->pending_element_ends--;
3372
+ pop_tag(s);
3373
+ lexer->result_symbol = ELEMENT_END;
3374
+ return true;
3375
+ }
3376
+
3377
+ // Grammar-position-driven pieces (TEXT is never valid at these positions).
3378
+ if (valid[CLOSE_TAG_NAME] && !valid[TEXT] && !valid[ATTR_NAME]) {
3379
+ TSSymbol result = 0;
3380
+ if (!scan_close_tag_name(s, lexer, &result)) return false;
3381
+ lexer->result_symbol = result;
3382
+ return true;
3383
+ }
3384
+ if (valid[CLOSE_TAG_END] && !valid[TEXT] && !valid[ATTR_NAME]) {
3385
+ TSSymbol result = 0;
3386
+ if (!scan_close_tag_end(s, lexer, &result)) return false;
3387
+ lexer->result_symbol = result;
3388
+ return true;
3389
+ }
3390
+ if (valid[PLACEHOLDER_EXPR] && !valid[TEXT] && !valid[ATTR_NAME]) {
3391
+ TSSymbol result = 0;
3392
+ if (!scan_placeholder_expr(s, lexer, &result)) return false;
3393
+ lexer->result_symbol = result;
3394
+ return true;
3395
+ }
3396
+ if (valid[PLACEHOLDER_END] && !valid[TEXT] && !valid[ATTR_NAME]) {
3397
+ TSSymbol result = 0;
3398
+ if (!scan_placeholder_end(s, lexer, &result)) return false;
3399
+ lexer->result_symbol = result;
3400
+ return true;
3401
+ }
3402
+ if ((valid[SCRIPTLET_BLOCK_OPEN] || valid[SCRIPTLET_EXPR] ||
3403
+ valid[SCRIPTLET_BLOCK_EXPR] || valid[SCRIPTLET_BLOCK_CLOSE]) &&
3404
+ !valid[TEXT] && !valid[ATTR_NAME]) {
3405
+ TSSymbol result = 0;
3406
+ if (!scan_scriptlet_body(s, lexer, valid, &result)) return false;
3407
+ lexer->result_symbol = result;
3408
+ return true;
3409
+ }
3410
+
3411
+ if (s->tag_open) return scan_open_tag(s, lexer, valid);
3412
+
3413
+ return scan_content(s, lexer, valid);
3414
+ }
3415
+
3416
+ // --------------------------------------------------------------------------
3417
+ // Serialization
3418
+ // --------------------------------------------------------------------------
3419
+
3420
+ #define WRITE(field) \
3421
+ do { \
3422
+ memcpy(buffer + n, &(field), sizeof(field)); \
3423
+ n += sizeof(field); \
3424
+ } while (0)
3425
+ #define READ(field) \
3426
+ do { \
3427
+ memcpy(&(field), buffer + n, sizeof(field)); \
3428
+ n += sizeof(field); \
3429
+ } while (0)
3430
+
3431
+ void *tree_sitter_marko_external_scanner_create(void) {
3432
+ Scanner *s = (Scanner *)calloc(1, sizeof(Scanner));
3433
+ s->cur.line_only_ws = 1;
3434
+ s->cur.cur_indent_hash = HASH_INIT;
3435
+ s->saved = s->cur;
3436
+ return s;
3437
+ }
3438
+
3439
+ void tree_sitter_marko_external_scanner_destroy(void *payload) {
3440
+ Scanner *s = (Scanner *)payload;
3441
+ free(s->buf);
3442
+ free(s);
3443
+ }
3444
+
3445
+ unsigned tree_sitter_marko_external_scanner_serialize(void *payload,
3446
+ char *buffer) {
3447
+ Scanner *s = (Scanner *)payload;
3448
+ unsigned n = 0;
3449
+ WRITE(s->saved);
3450
+ WRITE(s->started);
3451
+ WRITE(s->begin_mixed_mode);
3452
+ WRITE(s->ending_mixed_at_eol);
3453
+ WRITE(s->semicolon_line);
3454
+ WRITE(s->pending_element_ends);
3455
+ WRITE(s->suppress_placeholder);
3456
+ WRITE(s->fail_next);
3457
+ WRITE(s->tag_comma_continue);
3458
+ WRITE(s->tag_open);
3459
+ WRITE(s->tag_name_done);
3460
+ WRITE(s->in_shorthand);
3461
+ WRITE(s->adjacent_lt);
3462
+ WRITE(s->statement_pending);
3463
+ WRITE(s->in_attr_group);
3464
+ WRITE(s->attr_active);
3465
+ WRITE(s->attr_stage);
3466
+ WRITE(s->attr_has_args);
3467
+ WRITE(s->attr_has_type_params);
3468
+ WRITE(s->tag_has_attrs);
3469
+ WRITE(s->tag_has_args);
3470
+ WRITE(s->tag_has_params);
3471
+ WRITE(s->tag_has_shorthand_id);
3472
+ WRITE(s->tag_pending_types);
3473
+ WRITE(s->types_was_args);
3474
+ WRITE(s->expect_method_open);
3475
+ WRITE(s->block_after_tag);
3476
+ WRITE(s->tag_len);
3477
+ WRITE(s->content_len);
3478
+ for (uint8_t i = 0; i < s->tag_len; i++) WRITE(s->tags[i]);
3479
+ for (uint8_t i = 0; i < s->content_len; i++) WRITE(s->contents[i]);
3480
+ return n;
3481
+ }
3482
+
3483
+ void tree_sitter_marko_external_scanner_deserialize(void *payload,
3484
+ const char *buffer,
3485
+ unsigned length) {
3486
+ Scanner *s = (Scanner *)payload;
3487
+ char *buf = s->buf;
3488
+ uint32_t buf_cap = s->buf_cap;
3489
+ memset(s, 0, sizeof(Scanner));
3490
+ s->buf = buf;
3491
+ s->buf_cap = buf_cap;
3492
+ s->cur.line_only_ws = 1;
3493
+ s->cur.cur_indent_hash = HASH_INIT;
3494
+
3495
+ if (length == 0) {
3496
+ s->saved = s->cur;
3497
+ return;
3498
+ }
3499
+
3500
+ unsigned n = 0;
3501
+ const char *bufferc = buffer;
3502
+ {
3503
+ const char *buffer = bufferc;
3504
+ READ(s->saved);
3505
+ READ(s->started);
3506
+ READ(s->begin_mixed_mode);
3507
+ READ(s->ending_mixed_at_eol);
3508
+ READ(s->semicolon_line);
3509
+ READ(s->pending_element_ends);
3510
+ READ(s->suppress_placeholder);
3511
+ READ(s->fail_next);
3512
+ READ(s->tag_comma_continue);
3513
+ READ(s->tag_open);
3514
+ READ(s->tag_name_done);
3515
+ READ(s->in_shorthand);
3516
+ READ(s->adjacent_lt);
3517
+ READ(s->statement_pending);
3518
+ READ(s->in_attr_group);
3519
+ READ(s->attr_active);
3520
+ READ(s->attr_stage);
3521
+ READ(s->attr_has_args);
3522
+ READ(s->attr_has_type_params);
3523
+ READ(s->tag_has_attrs);
3524
+ READ(s->tag_has_args);
3525
+ READ(s->tag_has_params);
3526
+ READ(s->tag_has_shorthand_id);
3527
+ READ(s->tag_pending_types);
3528
+ READ(s->types_was_args);
3529
+ READ(s->expect_method_open);
3530
+ READ(s->block_after_tag);
3531
+ READ(s->tag_len);
3532
+ READ(s->content_len);
3533
+ for (uint8_t i = 0; i < s->tag_len; i++) READ(s->tags[i]);
3534
+ for (uint8_t i = 0; i < s->content_len; i++) READ(s->contents[i]);
3535
+ }
3536
+ s->cur = s->saved;
3537
+ }
3538
+
3539
+ #ifdef TSMARKO_DEBUG
3540
+ static const char *const TOKEN_NAMES[] = {
3541
+ "TEXT", "PLACEHOLDER_START", "PLACEHOLDER_START_RAW", "INTERP_START",
3542
+ "PLACEHOLDER_EXPR", "PLACEHOLDER_END", "HTML_COMMENT", "LINE_COMMENT",
3543
+ "BLOCK_COMMENT", "CDATA", "DOCTYPE", "DECLARATION", "SCRIPTLET_START",
3544
+ "SCRIPTLET_START_CONCISE", "SCRIPTLET_BLOCK_OPEN", "SCRIPTLET_BLOCK_EXPR",
3545
+ "SCRIPTLET_BLOCK_CLOSE", "SCRIPTLET_EXPR", "BLOCK_OPEN", "BLOCK_CLOSE",
3546
+ "OPEN_TAG_START", "TAG_NAME_FRAGMENT", "TAG_NAME_EMPTY",
3547
+ "SHORTHAND_ID_START", "SHORTHAND_CLASS_START", "STATEMENT_EXPR",
3548
+ "TAG_VAR_START", "VAR_PATTERN", "VAR_COLON", "VAR_TYPE", "ARGS_OPEN",
3549
+ "ARGS_EXPR", "ARGS_CLOSE", "PARAMS_OPEN", "PARAM_PATTERN", "PARAM_COLON",
3550
+ "PARAM_TYPE", "PARAM_EQ", "PARAM_DEFAULT", "PARAM_COMMA",
3551
+ "PARAMS_CLOSE", "TYPE_ARGS_OPEN",
3552
+ "TYPE_PARAMS_OPEN", "TYPE_EXPR", "TYPES_CLOSE", "ATTR_GROUP_OPEN",
3553
+ "ATTR_GROUP_CLOSE", "ATTR_NAME", "ATTR_EQ", "ATTR_BOUND_EQ",
3554
+ "ATTR_SPREAD_START", "ATTR_VALUE_EXPR", "METHOD_BODY_OPEN",
3555
+ "METHOD_BODY_EXPR", "METHOD_BODY_CLOSE", "OPEN_TAG_END",
3556
+ "OPEN_TAG_END_SELF", "CONCISE_OPEN_TAG_END", "CLOSE_TAG_START",
3557
+ "CLOSE_TAG_NAME", "CLOSE_TAG_END", "ELEMENT_END", "TAG_COMMENT",
3558
+ "ESCAPE", "ERROR_SENTINEL",
3559
+ };
3560
+ #endif
3561
+
3562
+ bool tree_sitter_marko_external_scanner_scan(void *payload, TSLexer *lexer,
3563
+ const bool *valid_symbols) {
3564
+ Scanner *s = (Scanner *)payload;
3565
+ s->buf_len = 0;
3566
+ #ifdef TSMARKO_DEBUG
3567
+ uint32_t col = lexer->get_column(lexer);
3568
+ bool ok = scan_main(s, lexer, valid_symbols);
3569
+ fprintf(stderr, "[scan col=%u eof=%d err=%d] -> %s (buf=%.*s)\n", col,
3570
+ at_eof(lexer), valid_symbols[ERROR_SENTINEL],
3571
+ ok ? TOKEN_NAMES[lexer->result_symbol] : "(none)",
3572
+ (int)(s->buf_len > 40 ? 40 : s->buf_len), s->buf ? s->buf : "");
3573
+ if (!ok) {
3574
+ fprintf(stderr, " valid:");
3575
+ for (int i = 0; i <= ERROR_SENTINEL; i++) {
3576
+ if (valid_symbols[i]) fprintf(stderr, " %s", TOKEN_NAMES[i]);
3577
+ }
3578
+ fprintf(stderr, "\n");
3579
+ }
3580
+ return ok;
3581
+ #else
3582
+ return scan_main(s, lexer, valid_symbols);
3583
+ #endif
3584
+ }