tree-sitter-aspx 0.0.0-stage → 1.0.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,1068 @@
1
+ // External scanner for tree-sitter-aspx.
2
+ //
3
+ // Jobs the ordinary lexer cannot do:
4
+ //
5
+ // * Code, comments, style, and handler bodies run until a terminator
6
+ // (`%>`, `--%>`, `</script>`, `</style>`, or EOF). Script and style
7
+ // bodies also stop at `<%`, so nuggets inside them stay real nodes.
8
+ // Tree-sitter's regex dialect has no lookahead, so a regex cannot stop
9
+ // before that terminator without eating it.
10
+ // * The page `Language` value is recorded here. Later code tokens are
11
+ // emitted as C# or VB so the injection query can name the grammar
12
+ // without finding the directive again. The byte is serialized because
13
+ // an incremental reparse may resume after the directive.
14
+ // * A `Language` value that is neither C# nor VB is still consumed, and
15
+ // the recorded language stays unchanged.
16
+ // * Start and end tag names pass through here so the scanner can keep a
17
+ // stack of open elements and emit a zero-width implied end tag where
18
+ // HTML lets a page leave one out. The stack is serialized with the
19
+ // language.
20
+ #include "tree_sitter/alloc.h"
21
+ #include "tree_sitter/parser.h"
22
+
23
+ #include <string.h>
24
+
25
+ enum TokenType {
26
+ LANG_CSHARP,
27
+ LANG_VB,
28
+ LANG_OTHER,
29
+ CODE_CSHARP,
30
+ CODE_VB,
31
+ SERVER_COMMENT_TEXT,
32
+ SCRIPT_CSHARP,
33
+ SCRIPT_VB,
34
+ STYLE_TEXT,
35
+ EXPRESSION_TEXT,
36
+ HANDLER_CSHARP,
37
+ HANDLER_VB,
38
+ START_TAG_NAME,
39
+ END_TAG_NAME,
40
+ IMPLICIT_END_TAG,
41
+ SELF_CLOSING_DELIMITER,
42
+ STRAY_END_TAG_NAME,
43
+ SCRIPT_TAG_PROBE,
44
+ };
45
+
46
+ enum Language {
47
+ LANGUAGE_CSHARP = 0,
48
+ LANGUAGE_VB = 1,
49
+ };
50
+
51
+ // Open elements as lowercase names. Only the innermost TAG_STACK_MAX names
52
+ // are kept, in a ring indexed by depth; positions below `known_from` (and
53
+ // any name the serialization buffer could not hold) are unknown. Names
54
+ // longer than TAG_NAME_MAX are cut to that length. An unknown name never
55
+ // matches, and no end tag is implied while the innermost name is unknown,
56
+ // so very deep pages fall back to pairing end tags by position.
57
+ #define TAG_STACK_MAX 64
58
+ #define TAG_NAME_MAX 31
59
+
60
+ // `language` is the language a directive named (C# when none did), and
61
+ // `language_set` records that one did. `script_language` is the language
62
+ // the current server script's own `language` attribute gives its body, and
63
+ // `page_script_language` the one the first such script gave; a page whose
64
+ // directives name no language takes it from there. Both are SCRIPT_UNSET
65
+ // when no recognized language was given.
66
+ #define SCRIPT_UNSET 2
67
+
68
+ // The serialized flags byte.
69
+ #define FLAG_LANGUAGE_VB 0x01
70
+ #define FLAG_LANGUAGE_SET 0x02
71
+ #define FLAG_SCRIPT_SHIFT 2
72
+ #define FLAG_PAGE_SCRIPT_SHIFT 4
73
+ #define FLAG_SCRIPT_MASK 0x03
74
+
75
+ typedef struct {
76
+ uint8_t language;
77
+ bool language_set;
78
+ uint8_t script_language;
79
+ uint8_t page_script_language;
80
+ uint16_t depth;
81
+ uint16_t known_from;
82
+ char names[TAG_STACK_MAX][TAG_NAME_MAX + 1];
83
+ } Scanner;
84
+
85
+ // ASCII-only lowercase. The Wasm parser target provides no libc ctype
86
+ // functions, and every name compared here is ASCII by construction.
87
+ static int32_t ascii_lower(int32_t c) {
88
+ return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
89
+ }
90
+
91
+ static bool is_space(int32_t c) {
92
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
93
+ }
94
+
95
+ void *tree_sitter_aspx_external_scanner_create(void) {
96
+ // ts_malloc plus an explicit field set, not ts_calloc: the compiler can
97
+ // lower calloc to memset, which the Wasm parser target does not provide.
98
+ // ts_malloc is plain malloc unless the host build opts into tree-sitter's
99
+ // allocator, and the Wasm target has no abort(), so a failed allocation
100
+ // returns NULL. The other entry points then do nothing: the parse goes on
101
+ // without external tokens instead of writing through a null pointer.
102
+ Scanner *scanner = ts_malloc(sizeof(Scanner));
103
+ if (scanner != NULL) {
104
+ scanner->language = LANGUAGE_CSHARP;
105
+ scanner->language_set = false;
106
+ scanner->script_language = SCRIPT_UNSET;
107
+ scanner->page_script_language = SCRIPT_UNSET;
108
+ scanner->depth = 0;
109
+ scanner->known_from = 1;
110
+ }
111
+ return scanner;
112
+ }
113
+
114
+ void tree_sitter_aspx_external_scanner_destroy(void *payload) {
115
+ ts_free(payload);
116
+ }
117
+
118
+ // Layout: a flags byte (the FLAG_ values above), depth (two bytes,
119
+ // little-endian), the number of names
120
+ // stored, then each stored name as a length byte and its characters,
121
+ // innermost first. Names that do not fit are restored as unknown.
122
+ unsigned tree_sitter_aspx_external_scanner_serialize(void *payload, char *buffer) {
123
+ Scanner *scanner = payload;
124
+ if (scanner == NULL) {
125
+ return 0;
126
+ }
127
+ buffer[0] = (char)((scanner->language == LANGUAGE_VB ? FLAG_LANGUAGE_VB : 0) |
128
+ (scanner->language_set ? FLAG_LANGUAGE_SET : 0) |
129
+ (scanner->script_language << FLAG_SCRIPT_SHIFT) |
130
+ (scanner->page_script_language << FLAG_PAGE_SCRIPT_SHIFT));
131
+ buffer[1] = (char)(scanner->depth & 0xff);
132
+ buffer[2] = (char)(scanner->depth >> 8);
133
+ unsigned size = 4;
134
+ unsigned stored = 0;
135
+ for (unsigned d = scanner->depth; d >= scanner->known_from && d > 0 && stored < 255; d--) {
136
+ const char *name = scanner->names[(d - 1) % TAG_STACK_MAX];
137
+ unsigned length = (unsigned)strlen(name);
138
+ if (size + 1 + length > TREE_SITTER_SERIALIZATION_BUFFER_SIZE) {
139
+ break;
140
+ }
141
+ buffer[size++] = (char)length;
142
+ for (unsigned i = 0; i < length; i++) {
143
+ buffer[size++] = name[i];
144
+ }
145
+ stored++;
146
+ }
147
+ buffer[3] = (char)stored;
148
+ return size;
149
+ }
150
+
151
+ void tree_sitter_aspx_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) {
152
+ Scanner *scanner = payload;
153
+ if (scanner == NULL) {
154
+ return;
155
+ }
156
+ scanner->language = LANGUAGE_CSHARP;
157
+ scanner->language_set = false;
158
+ scanner->script_language = SCRIPT_UNSET;
159
+ scanner->page_script_language = SCRIPT_UNSET;
160
+ scanner->depth = 0;
161
+ scanner->known_from = 1;
162
+ if (length == 0) {
163
+ return;
164
+ }
165
+ uint8_t flags = (uint8_t)buffer[0];
166
+ scanner->language = (flags & FLAG_LANGUAGE_VB) != 0 ? LANGUAGE_VB : LANGUAGE_CSHARP;
167
+ scanner->language_set = (flags & FLAG_LANGUAGE_SET) != 0;
168
+ scanner->script_language = (flags >> FLAG_SCRIPT_SHIFT) & FLAG_SCRIPT_MASK;
169
+ scanner->page_script_language = (flags >> FLAG_PAGE_SCRIPT_SHIFT) & FLAG_SCRIPT_MASK;
170
+ if (length < 4) {
171
+ return;
172
+ }
173
+ scanner->depth = (uint16_t)((uint8_t)buffer[1] | ((uint8_t)buffer[2] << 8));
174
+ unsigned stored = (uint8_t)buffer[3];
175
+ unsigned offset = 4;
176
+ unsigned restored = 0;
177
+ for (unsigned d = scanner->depth; restored < stored && d > 0 && offset < length; d--) {
178
+ unsigned name_length = (uint8_t)buffer[offset++];
179
+ if (name_length > TAG_NAME_MAX || offset + name_length > length) {
180
+ break;
181
+ }
182
+ char *slot = scanner->names[(d - 1) % TAG_STACK_MAX];
183
+ for (unsigned i = 0; i < name_length; i++) {
184
+ slot[i] = buffer[offset++];
185
+ }
186
+ slot[name_length] = '\0';
187
+ restored++;
188
+ }
189
+ scanner->known_from = (uint16_t)(scanner->depth - restored + 1);
190
+ }
191
+
192
+ static bool scan_until_percent_gt(TSLexer *lexer) {
193
+ bool has_content = false;
194
+ while (lexer->lookahead != 0) {
195
+ if (lexer->lookahead == '%') {
196
+ lexer->mark_end(lexer);
197
+ lexer->advance(lexer, false);
198
+ if (lexer->lookahead == '>') {
199
+ return has_content;
200
+ }
201
+ lexer->mark_end(lexer);
202
+ has_content = true;
203
+ continue;
204
+ }
205
+ lexer->advance(lexer, false);
206
+ lexer->mark_end(lexer);
207
+ has_content = true;
208
+ }
209
+ return has_content;
210
+ }
211
+
212
+ // Content runs until a run of two or more dashes followed by `%>`. The
213
+ // whole run belongs to the closing token, so `---%>` closes the comment.
214
+ static bool scan_until_server_comment_end(TSLexer *lexer) {
215
+ bool has_content = false;
216
+ while (lexer->lookahead != 0) {
217
+ if (lexer->lookahead == '-') {
218
+ lexer->mark_end(lexer);
219
+ unsigned dashes = 0;
220
+ while (lexer->lookahead == '-') {
221
+ lexer->advance(lexer, false);
222
+ dashes++;
223
+ }
224
+ if (dashes >= 2 && lexer->lookahead == '%') {
225
+ lexer->advance(lexer, false);
226
+ if (lexer->lookahead == '>') {
227
+ return has_content;
228
+ }
229
+ }
230
+ lexer->mark_end(lexer);
231
+ has_content = true;
232
+ continue;
233
+ }
234
+ lexer->advance(lexer, false);
235
+ lexer->mark_end(lexer);
236
+ has_content = true;
237
+ }
238
+ return has_content;
239
+ }
240
+
241
+ static bool match_ci(TSLexer *lexer, const char *lower) {
242
+ for (unsigned i = 0; lower[i] != '\0'; i++) {
243
+ int32_t c = lexer->lookahead;
244
+ if (c == 0 || ascii_lower(c) != lower[i]) {
245
+ return false;
246
+ }
247
+ lexer->advance(lexer, false);
248
+ }
249
+ return true;
250
+ }
251
+
252
+ // After `</`, matches `name` with optional whitespace before `>`.
253
+ static bool at_end_tag_name(TSLexer *lexer, const char *name) {
254
+ if (!match_ci(lexer, name)) {
255
+ return false;
256
+ }
257
+ while (is_space(lexer->lookahead)) {
258
+ lexer->advance(lexer, false);
259
+ }
260
+ return lexer->lookahead == '>';
261
+ }
262
+
263
+ // Called with the lexer just past the `<` of a `<%` at the start of a body
264
+ // chunk. Reports whether a `%>` closes it before the body's end tag. When it
265
+ // does not, the `<%` is literal text (for example inside a string in a server
266
+ // script), so the token end is moved to the end tag or EOF as the scan goes.
267
+ static bool nugget_closes_in_body(TSLexer *lexer, const char *name) {
268
+ lexer->advance(lexer, false);
269
+ // The `<%` itself is content when nothing closes it, even at the end of
270
+ // the file; a zero-width chunk would repeat forever.
271
+ lexer->mark_end(lexer);
272
+ while (lexer->lookahead != 0) {
273
+ if (lexer->lookahead == '%') {
274
+ lexer->advance(lexer, false);
275
+ if (lexer->lookahead == '>') {
276
+ return true;
277
+ }
278
+ lexer->mark_end(lexer);
279
+ continue;
280
+ }
281
+ if (lexer->lookahead == '<') {
282
+ lexer->mark_end(lexer);
283
+ lexer->advance(lexer, false);
284
+ if (lexer->lookahead == '/') {
285
+ lexer->advance(lexer, false);
286
+ if (at_end_tag_name(lexer, name)) {
287
+ return false;
288
+ }
289
+ }
290
+ lexer->mark_end(lexer);
291
+ continue;
292
+ }
293
+ lexer->advance(lexer, false);
294
+ lexer->mark_end(lexer);
295
+ }
296
+ return false;
297
+ }
298
+
299
+ // Content runs until `</name` with optional whitespace before `>`, or until
300
+ // a `<%` that opens a nugget or server comment inside the body. On a stop the
301
+ // token end stays where the caller marked it, before the `<`. A `<%` with no
302
+ // `%>` before the end tag is not a nugget and stays in the content.
303
+ static bool scan_until_end_tag(TSLexer *lexer, const char *name) {
304
+ bool has_content = false;
305
+ while (lexer->lookahead != 0) {
306
+ if (lexer->lookahead == '<') {
307
+ lexer->mark_end(lexer);
308
+ lexer->advance(lexer, false);
309
+ if (lexer->lookahead == '%') {
310
+ // Stop before the `<%`; the next scan starts there and decides
311
+ // whether it opens a nugget.
312
+ if (has_content) {
313
+ return true;
314
+ }
315
+ return !nugget_closes_in_body(lexer, name);
316
+ }
317
+ if (lexer->lookahead == '/') {
318
+ lexer->advance(lexer, false);
319
+ if (at_end_tag_name(lexer, name)) {
320
+ return has_content;
321
+ }
322
+ }
323
+ lexer->mark_end(lexer);
324
+ has_content = true;
325
+ continue;
326
+ }
327
+ lexer->advance(lexer, false);
328
+ lexer->mark_end(lexer);
329
+ has_content = true;
330
+ }
331
+ return has_content;
332
+ }
333
+
334
+ // The CodeDom names ASP.NET accepts for each compiler. Matching ignores
335
+ // case; anything else is LANG_OTHER and leaves the page language alone.
336
+ static const char *const CSHARP_NAMES[] = {"c#", "cs", "csharp"};
337
+ static const char *const VB_NAMES[] = {"vb", "vbs", "visualbasic", "vbscript"};
338
+
339
+ static bool name_in(const char *name, const char *const *names, unsigned count) {
340
+ for (unsigned i = 0; i < count; i++) {
341
+ if (strcmp(name, names[i]) == 0) {
342
+ return true;
343
+ }
344
+ }
345
+ return false;
346
+ }
347
+
348
+ // LANGUAGE_CSHARP or LANGUAGE_VB for a recognized lowercase name, otherwise
349
+ // SCRIPT_UNSET.
350
+ static uint8_t classify_language(const char *name) {
351
+ if (name_in(name, CSHARP_NAMES, sizeof(CSHARP_NAMES) / sizeof(CSHARP_NAMES[0]))) {
352
+ return LANGUAGE_CSHARP;
353
+ }
354
+ if (name_in(name, VB_NAMES, sizeof(VB_NAMES) / sizeof(VB_NAMES[0]))) {
355
+ return LANGUAGE_VB;
356
+ }
357
+ return SCRIPT_UNSET;
358
+ }
359
+
360
+ // A quoted value stops at the quote; an unquoted one also stops at the `>`
361
+ // that ends the tag. scan_language handles `%>` itself.
362
+ static bool ends_language_value(int32_t c) {
363
+ return c == 0 || c == '"' || c == '\'' || c == '>' || is_space(c) || c > 127;
364
+ }
365
+
366
+ // Case-insensitive search for `word` ahead of the lexer, stopping at `stop`
367
+ // (or EOF). Consumes what it reads.
368
+ static bool find_ci_before(TSLexer *lexer, const char *word, int32_t stop) {
369
+ while (lexer->lookahead != 0 && lexer->lookahead != stop) {
370
+ if (ascii_lower(lexer->lookahead) == word[0]) {
371
+ lexer->advance(lexer, false);
372
+ if (match_ci(lexer, word + 1)) {
373
+ return true;
374
+ }
375
+ continue;
376
+ }
377
+ lexer->advance(lexer, false);
378
+ }
379
+ return false;
380
+ }
381
+
382
+ // Consumes up to and including `--%>`, reporting whether it was found.
383
+ static bool find_comment_end(TSLexer *lexer) {
384
+ unsigned dashes = 0;
385
+ while (lexer->lookahead != 0) {
386
+ int32_t c = lexer->lookahead;
387
+ lexer->advance(lexer, false);
388
+ if (c == '-') {
389
+ dashes++;
390
+ } else if (c == '%' && dashes >= 2 && lexer->lookahead == '>') {
391
+ return true;
392
+ } else {
393
+ dashes = 0;
394
+ }
395
+ }
396
+ return false;
397
+ }
398
+
399
+ // With the lexer just past a line-start `<`, reports whether what follows is
400
+ // markup the handler grammar can finish, so the code should stop before it:
401
+ // * `<%@ ... %>`, a directive such as `<%@ Assembly Name="..." %>`;
402
+ // * `<%-- ... --%>`, a complete server comment;
403
+ // * `<script ... runat ... >` that is not self-closing and has a later
404
+ // `</script>`. Only server scripts mean anything in a handler file; a
405
+ // client `<script>` line is almost always HTML inside a C# or VB string.
406
+ // Consumes what it reads; on false the caller treats all of it as code.
407
+ static bool at_handler_markup(TSLexer *lexer) {
408
+ if (lexer->lookahead == '%') {
409
+ lexer->advance(lexer, false);
410
+ if (lexer->lookahead == '@') {
411
+ return find_ci_before(lexer, "%>", 0);
412
+ }
413
+ if (lexer->lookahead != '-') {
414
+ return false;
415
+ }
416
+ lexer->advance(lexer, false);
417
+ if (lexer->lookahead != '-') {
418
+ return false;
419
+ }
420
+ lexer->advance(lexer, false);
421
+ return find_comment_end(lexer);
422
+ }
423
+ if (!match_ci(lexer, "script") ||
424
+ !(is_space(lexer->lookahead) || lexer->lookahead == '>')) {
425
+ return false;
426
+ }
427
+ bool runat = false;
428
+ int32_t previous = 0;
429
+ while (lexer->lookahead != 0 && lexer->lookahead != '>') {
430
+ if (ascii_lower(lexer->lookahead) == 'r') {
431
+ lexer->advance(lexer, false);
432
+ if (match_ci(lexer, "unat")) {
433
+ runat = true;
434
+ }
435
+ previous = 0;
436
+ continue;
437
+ }
438
+ previous = lexer->lookahead;
439
+ lexer->advance(lexer, false);
440
+ }
441
+ if (!runat || previous == '/' || lexer->lookahead != '>') {
442
+ return false;
443
+ }
444
+ return find_ci_before(lexer, "</script", 0);
445
+ }
446
+
447
+ // The body of a WebHandler or WebService file is code up to EOF, except that
448
+ // a line starting (after optional indentation) with a directive, a server
449
+ // comment, or a server script begins that construct instead. Leading
450
+ // whitespace is skipped, so the code starts at its first non-space character.
451
+ static bool scan_handler_code(TSLexer *lexer) {
452
+ while (is_space(lexer->lookahead)) {
453
+ lexer->advance(lexer, true);
454
+ }
455
+ bool has_content = false;
456
+ bool line_start = true;
457
+ while (lexer->lookahead != 0) {
458
+ int32_t c = lexer->lookahead;
459
+ if (c == '<' && line_start) {
460
+ lexer->mark_end(lexer);
461
+ lexer->advance(lexer, false);
462
+ if (at_handler_markup(lexer)) {
463
+ return has_content;
464
+ }
465
+ // Not markup: everything just read is code. It may have crossed
466
+ // newlines, so the next `<` is not treated as a line start.
467
+ lexer->mark_end(lexer);
468
+ has_content = true;
469
+ line_start = false;
470
+ continue;
471
+ }
472
+ if (c == '\n') {
473
+ line_start = true;
474
+ } else if (c != ' ' && c != '\t' && c != '\r') {
475
+ line_start = false;
476
+ }
477
+ lexer->advance(lexer, false);
478
+ lexer->mark_end(lexer);
479
+ has_content = true;
480
+ }
481
+ return has_content;
482
+ }
483
+
484
+ static bool scan_language(TSLexer *lexer, Scanner *scanner, const bool *valid_symbols) {
485
+ while (is_space(lexer->lookahead)) {
486
+ lexer->advance(lexer, true);
487
+ }
488
+ // Names longer than the buffer are still consumed whole; none of them is
489
+ // C# or VB, so they become LANG_OTHER.
490
+ char buf[32];
491
+ unsigned n = 0;
492
+ bool truncated = false;
493
+ for (;;) {
494
+ int32_t c = lexer->lookahead;
495
+ // A `%` ends an unquoted value only when it starts the closing `%>`.
496
+ if (c == '%') {
497
+ lexer->advance(lexer, false);
498
+ if (lexer->lookahead == '>') {
499
+ break;
500
+ }
501
+ } else if (ends_language_value(c)) {
502
+ break;
503
+ } else {
504
+ lexer->advance(lexer, false);
505
+ }
506
+ if (n + 1 < sizeof(buf)) {
507
+ buf[n++] = (char)ascii_lower(c);
508
+ } else {
509
+ truncated = true;
510
+ }
511
+ lexer->mark_end(lexer);
512
+ }
513
+ buf[n] = '\0';
514
+ if (n == 0) {
515
+ return false;
516
+ }
517
+
518
+ // A truncated name is too long to be any recognized one.
519
+ uint8_t language = truncated ? SCRIPT_UNSET : classify_language(buf);
520
+ enum TokenType symbol = language == LANGUAGE_CSHARP ? LANG_CSHARP
521
+ : language == LANGUAGE_VB ? LANG_VB
522
+ : LANG_OTHER;
523
+ if (!valid_symbols[symbol]) {
524
+ return false;
525
+ }
526
+ if (language != SCRIPT_UNSET) {
527
+ scanner->language = language;
528
+ scanner->language_set = true;
529
+ }
530
+ lexer->result_symbol = symbol;
531
+ return true;
532
+ }
533
+
534
+ // The language of nuggets and server script bodies: a directive's, else the
535
+ // first server script's, else C#. Handler code follows only the directive,
536
+ // since a handler file's code is compiled as a whole in that language.
537
+ static uint8_t page_language(const Scanner *scanner) {
538
+ if (!scanner->language_set && scanner->page_script_language != SCRIPT_UNSET) {
539
+ return scanner->page_script_language;
540
+ }
541
+ return scanner->language;
542
+ }
543
+
544
+ static bool emit_code(TSLexer *lexer, Scanner *scanner, const bool *valid_symbols, enum TokenType csharp, enum TokenType vb) {
545
+ bool is_script = csharp == SCRIPT_CSHARP;
546
+ uint8_t language = page_language(scanner);
547
+ if (is_script && scanner->script_language != SCRIPT_UNSET) {
548
+ language = scanner->script_language;
549
+ }
550
+ enum TokenType symbol = language == LANGUAGE_VB ? vb : csharp;
551
+ if (!valid_symbols[symbol]) {
552
+ return false;
553
+ }
554
+ if (is_script) {
555
+ if (!scan_until_end_tag(lexer, "script")) {
556
+ return false;
557
+ }
558
+ } else if (!scan_until_percent_gt(lexer)) {
559
+ return false;
560
+ }
561
+ lexer->result_symbol = symbol;
562
+ return true;
563
+ }
564
+
565
+ // ---- Open-element stack -------------------------------------------------
566
+
567
+ static bool is_name_start(int32_t c) {
568
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
569
+ }
570
+
571
+ static bool is_name_char(int32_t c) {
572
+ return is_name_start(c) || (c >= '0' && c <= '9') || c == '.' || c == ':' || c == '-';
573
+ }
574
+
575
+ // Reads a tag name (the grammar's `_name` pattern) as lowercase into `out`,
576
+ // keeping the first TAG_NAME_MAX characters. Returns false if none is there.
577
+ static bool read_tag_name(TSLexer *lexer, char *out) {
578
+ if (!is_name_start(lexer->lookahead)) {
579
+ return false;
580
+ }
581
+ unsigned n = 0;
582
+ while (is_name_char(lexer->lookahead)) {
583
+ if (n < TAG_NAME_MAX) {
584
+ out[n++] = (char)ascii_lower(lexer->lookahead);
585
+ }
586
+ lexer->advance(lexer, false);
587
+ }
588
+ out[n] = '\0';
589
+ return true;
590
+ }
591
+
592
+ #define COUNT(array) (sizeof(array) / sizeof((array)[0]))
593
+
594
+ // Names the grammar lexes itself as reserved words: void elements never hold
595
+ // content, and script and style bodies are raw text with their own end tags.
596
+ // Keep in step with VOID_TAGS in grammar.js; tools/scanner-tags.test.mjs
597
+ // compares the two.
598
+ static const char *const RESERVED_TAGS[] = {
599
+ "area", "base", "br", "col", "embed", "hr", "img", "input",
600
+ "link", "meta", "param", "source", "track", "wbr", "script", "style",
601
+ };
602
+
603
+ // Start tags that close an open `p` in button scope, per the HTML parsing
604
+ // rules (li, dt, and dd close one too, after their own check).
605
+ static const char *const CLOSES_P[] = {
606
+ "address", "article", "aside", "blockquote", "details", "dialog", "div",
607
+ "dl", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2",
608
+ "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "main", "menu", "nav",
609
+ "ol", "p", "pre", "section", "table", "ul", "li", "dt", "dd",
610
+ };
611
+
612
+ // Elements that stop the search for an open `p` (HTML's button scope).
613
+ static const char *const BUTTON_SCOPE[] = {
614
+ "applet", "button", "caption", "html", "marquee", "object", "table",
615
+ "td", "template", "th",
616
+ };
617
+
618
+ // HTML's "special" elements, which stop the search for an open `li`, `dt`,
619
+ // or `dd`, except for address, div, and p.
620
+ static const char *const SPECIAL[] = {
621
+ "applet", "article", "aside", "blockquote", "body", "button", "caption",
622
+ "center", "colgroup", "details", "dir", "dl", "fieldset", "figcaption",
623
+ "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head",
624
+ "header", "hgroup", "html", "iframe", "listing", "main", "marquee",
625
+ "menu", "nav", "noscript", "object", "ol", "pre", "section", "select",
626
+ "summary", "table", "tbody", "td", "template", "textarea", "tfoot", "th",
627
+ "thead", "tr", "ul",
628
+ };
629
+
630
+ static const char *const LIST_ITEMS[] = {"li"};
631
+ static const char *const DEFINITIONS[] = {"dt", "dd"};
632
+ static const char *const CELLS[] = {"td", "th"};
633
+ static const char *const ROWS[] = {"tr"};
634
+ static const char *const SECTIONS[] = {"tbody", "thead", "tfoot"};
635
+ static const char *const OPTIONS[] = {"option"};
636
+ static const char *const OPTION_GROUPS[] = {"option", "optgroup"};
637
+ static const char *const RUBY_TEXT[] = {"rt", "rp"};
638
+ static const char *const TABLE_SCOPE[] = {"html", "table", "template"};
639
+ static const char *const SELECT_SCOPE[] = {"datalist", "select"};
640
+ static const char *const RUBY_SCOPE[] = {"ruby"};
641
+ static const char *const PARAGRAPH[] = {"p"};
642
+ static const char *const NOT_SPECIAL_FOR_ITEMS[] = {"address", "div", "p"};
643
+
644
+ // How a start tag finds the open element it closes: the names it closes,
645
+ // and the names that end the search further out. With `stop_at_special`,
646
+ // the search instead ends at any special element not in `stops`.
647
+ typedef struct {
648
+ const char *const *closes;
649
+ unsigned closes_count;
650
+ const char *const *stops;
651
+ unsigned stops_count;
652
+ bool stop_at_special;
653
+ } CloseRule;
654
+
655
+ #define RULE(closes, stops, special) {closes, COUNT(closes), stops, COUNT(stops), special}
656
+
657
+ static bool find_rule(const char *next, CloseRule *rule) {
658
+ static const struct {
659
+ const char *next;
660
+ CloseRule rule;
661
+ } RULES[] = {
662
+ {"li", RULE(LIST_ITEMS, NOT_SPECIAL_FOR_ITEMS, true)},
663
+ {"dt", RULE(DEFINITIONS, NOT_SPECIAL_FOR_ITEMS, true)},
664
+ {"dd", RULE(DEFINITIONS, NOT_SPECIAL_FOR_ITEMS, true)},
665
+ {"td", RULE(CELLS, TABLE_SCOPE, false)},
666
+ {"th", RULE(CELLS, TABLE_SCOPE, false)},
667
+ {"tr", RULE(ROWS, TABLE_SCOPE, false)},
668
+ {"tbody", RULE(SECTIONS, TABLE_SCOPE, false)},
669
+ {"thead", RULE(SECTIONS, TABLE_SCOPE, false)},
670
+ {"tfoot", RULE(SECTIONS, TABLE_SCOPE, false)},
671
+ {"option", RULE(OPTIONS, SELECT_SCOPE, false)},
672
+ {"optgroup", RULE(OPTION_GROUPS, SELECT_SCOPE, false)},
673
+ {"rt", RULE(RUBY_TEXT, RUBY_SCOPE, false)},
674
+ {"rp", RULE(RUBY_TEXT, RUBY_SCOPE, false)},
675
+ };
676
+ for (unsigned i = 0; i < COUNT(RULES); i++) {
677
+ if (strcmp(next, RULES[i].next) == 0) {
678
+ *rule = RULES[i].rule;
679
+ return true;
680
+ }
681
+ }
682
+ return false;
683
+ }
684
+
685
+ // The name at 1-based stack position `d`, or NULL when it is not known.
686
+ static const char *name_at(Scanner *scanner, unsigned d) {
687
+ if (d == 0 || d > scanner->depth || d < scanner->known_from) {
688
+ return NULL;
689
+ }
690
+ return scanner->names[(d - 1) % TAG_STACK_MAX];
691
+ }
692
+
693
+ static void push_tag(Scanner *scanner, const char *name) {
694
+ if (scanner->depth == UINT16_MAX) {
695
+ return;
696
+ }
697
+ scanner->depth++;
698
+ char *slot = scanner->names[(scanner->depth - 1) % TAG_STACK_MAX];
699
+ unsigned i = 0;
700
+ for (; name[i] != '\0'; i++) {
701
+ slot[i] = name[i];
702
+ }
703
+ slot[i] = '\0';
704
+ if (scanner->depth - scanner->known_from + 1 > TAG_STACK_MAX) {
705
+ scanner->known_from = (uint16_t)(scanner->depth - TAG_STACK_MAX + 1);
706
+ }
707
+ }
708
+
709
+ static void pop_tag(Scanner *scanner) {
710
+ if (scanner->depth > 0) {
711
+ scanner->depth--;
712
+ }
713
+ if (scanner->known_from > scanner->depth + 1) {
714
+ scanner->known_from = (uint16_t)(scanner->depth + 1);
715
+ }
716
+ }
717
+
718
+ // Walks out from the innermost element for one `rule` closes, stopping at
719
+ // the rule's scope boundary or at an unknown name. Returns its position.
720
+ static unsigned find_closed(Scanner *scanner, const CloseRule *rule) {
721
+ for (unsigned d = scanner->depth; d > 0; d--) {
722
+ const char *name = name_at(scanner, d);
723
+ if (name == NULL) {
724
+ return 0;
725
+ }
726
+ if (name_in(name, rule->closes, rule->closes_count)) {
727
+ return d;
728
+ }
729
+ bool listed = name_in(name, rule->stops, rule->stops_count);
730
+ if (rule->stop_at_special ? name_in(name, SPECIAL, COUNT(SPECIAL)) && !listed : listed) {
731
+ return 0;
732
+ }
733
+ }
734
+ return 0;
735
+ }
736
+
737
+ // Whether a start tag named `next` implies the end of some open element, so
738
+ // the innermost one should end now. Each implied end pops one element and
739
+ // the scanner runs again, so this repeats until the closed element is gone.
740
+ static bool start_tag_implies_end(Scanner *scanner, const char *next) {
741
+ CloseRule rule;
742
+ if (find_rule(next, &rule) && find_closed(scanner, &rule) > 0) {
743
+ return true;
744
+ }
745
+ if (name_in(next, CLOSES_P, COUNT(CLOSES_P))) {
746
+ static const CloseRule P_RULE = RULE(PARAGRAPH, BUTTON_SCOPE, false);
747
+ return find_closed(scanner, &P_RULE) > 0;
748
+ }
749
+ return false;
750
+ }
751
+
752
+ // The position of the innermost open element named `name`, or 0. The search
753
+ // stops at the first unknown name.
754
+ static unsigned find_open(Scanner *scanner, const char *name) {
755
+ for (unsigned d = scanner->depth; d > 0; d--) {
756
+ const char *open = name_at(scanner, d);
757
+ if (open == NULL) {
758
+ return 0;
759
+ }
760
+ if (strcmp(open, name) == 0) {
761
+ return d;
762
+ }
763
+ }
764
+ return 0;
765
+ }
766
+
767
+ // A zero-width token that ends the innermost open element before the next
768
+ // start tag, end tag, or the end of the file, when HTML implies that end.
769
+ static bool scan_implicit_end_tag(TSLexer *lexer, Scanner *scanner) {
770
+ while (is_space(lexer->lookahead)) {
771
+ lexer->advance(lexer, true);
772
+ }
773
+ lexer->mark_end(lexer);
774
+ if (scanner->depth == 0) {
775
+ return false;
776
+ }
777
+ bool implied = false;
778
+ if (lexer->eof(lexer)) {
779
+ implied = true;
780
+ } else if (lexer->lookahead == '<' && name_at(scanner, scanner->depth) != NULL) {
781
+ lexer->advance(lexer, false);
782
+ char name[TAG_NAME_MAX + 1];
783
+ if (lexer->lookahead == '/') {
784
+ lexer->advance(lexer, false);
785
+ while (is_space(lexer->lookahead)) {
786
+ lexer->advance(lexer, false);
787
+ }
788
+ // An end tag for an element further out closes this one first.
789
+ if (read_tag_name(lexer, name)) {
790
+ unsigned d = find_open(scanner, name);
791
+ implied = d > 0 && d < scanner->depth;
792
+ }
793
+ } else {
794
+ // A `<` followed by whitespace is text, not a start tag.
795
+ implied = read_tag_name(lexer, name) && start_tag_implies_end(scanner, name);
796
+ }
797
+ }
798
+ if (!implied) {
799
+ return false;
800
+ }
801
+ pop_tag(scanner);
802
+ lexer->result_symbol = IMPLICIT_END_TAG;
803
+ return true;
804
+ }
805
+
806
+ static bool scan_start_tag_name(TSLexer *lexer, Scanner *scanner) {
807
+ char name[TAG_NAME_MAX + 1];
808
+ if (!read_tag_name(lexer, name) || name_in(name, RESERVED_TAGS, COUNT(RESERVED_TAGS))) {
809
+ return false;
810
+ }
811
+ lexer->mark_end(lexer);
812
+ push_tag(scanner, name);
813
+ lexer->result_symbol = START_TAG_NAME;
814
+ return true;
815
+ }
816
+
817
+ // An end tag closes the innermost element when its name matches it, or when
818
+ // that name is unknown. An end tag that names no open element is stray, as
819
+ // a browser treats it: it closes nothing. Implied end tags have already
820
+ // closed anything between it and a matching element further out.
821
+ static bool scan_end_tag_name(TSLexer *lexer, Scanner *scanner, const bool *valid_symbols) {
822
+ while (is_space(lexer->lookahead)) {
823
+ lexer->advance(lexer, true);
824
+ }
825
+ char name[TAG_NAME_MAX + 1];
826
+ if (!read_tag_name(lexer, name)) {
827
+ return false;
828
+ }
829
+ lexer->mark_end(lexer);
830
+ const char *top = name_at(scanner, scanner->depth);
831
+ bool closes = false;
832
+ if (scanner->depth > 0) {
833
+ if (top == NULL || strcmp(top, name) == 0) {
834
+ closes = true;
835
+ } else if (find_open(scanner, name) == 0) {
836
+ // No known open element has this name. When some names further out
837
+ // are unknown, it may still be one of them, so pair it by position.
838
+ closes = scanner->known_from > 1;
839
+ } else {
840
+ closes = true;
841
+ }
842
+ }
843
+ if (closes && valid_symbols[END_TAG_NAME]) {
844
+ pop_tag(scanner);
845
+ lexer->result_symbol = END_TAG_NAME;
846
+ return true;
847
+ }
848
+ if (valid_symbols[STRAY_END_TAG_NAME]) {
849
+ lexer->result_symbol = STRAY_END_TAG_NAME;
850
+ return true;
851
+ }
852
+ if (valid_symbols[END_TAG_NAME]) {
853
+ pop_tag(scanner);
854
+ lexer->result_symbol = END_TAG_NAME;
855
+ return true;
856
+ }
857
+ return false;
858
+ }
859
+
860
+ static bool scan_self_closing_delimiter(TSLexer *lexer, Scanner *scanner) {
861
+ while (is_space(lexer->lookahead)) {
862
+ lexer->advance(lexer, true);
863
+ }
864
+ if (lexer->lookahead != '/') {
865
+ return false;
866
+ }
867
+ lexer->advance(lexer, false);
868
+ if (lexer->lookahead != '>') {
869
+ return false;
870
+ }
871
+ lexer->advance(lexer, false);
872
+ lexer->mark_end(lexer);
873
+ pop_tag(scanner);
874
+ lexer->result_symbol = SELF_CLOSING_DELIMITER;
875
+ return true;
876
+ }
877
+
878
+ // ---- Server script language --------------------------------------------
879
+
880
+ // Called just past a `<` with `%` at the lexer: skips the rest of a nugget
881
+ // (through `%>`) or a server comment (through a run of two or more dashes
882
+ // and `%>`), or to EOF when it never closes. Never marks the token end.
883
+ static void skip_server_block(TSLexer *lexer) {
884
+ lexer->advance(lexer, false);
885
+ bool comment = false;
886
+ if (lexer->lookahead == '-') {
887
+ lexer->advance(lexer, false);
888
+ if (lexer->lookahead == '-') {
889
+ lexer->advance(lexer, false);
890
+ comment = true;
891
+ }
892
+ }
893
+ unsigned dashes = 0;
894
+ while (lexer->lookahead != 0) {
895
+ int32_t c = lexer->lookahead;
896
+ lexer->advance(lexer, false);
897
+ if (c == '%' && lexer->lookahead == '>' && (!comment || dashes >= 2)) {
898
+ lexer->advance(lexer, false);
899
+ return;
900
+ }
901
+ dashes = c == '-' ? dashes + 1 : 0;
902
+ }
903
+ }
904
+
905
+ // Reads an attribute value at the lexer, quoted or unquoted, keeping the
906
+ // first characters, lowercased, in `out`. Nuggets and server comments in it
907
+ // are skipped; a value holding one is computed at run time, so `out` comes
908
+ // back empty. An unquoted value runs to whitespace or `>`, as the grammar's
909
+ // unquoted_attribute_value does.
910
+ static void read_attribute_value(TSLexer *lexer, char *out, unsigned size) {
911
+ unsigned n = 0;
912
+ bool dynamic = false;
913
+ int32_t quote = 0;
914
+ if (lexer->lookahead == '"' || lexer->lookahead == '\'') {
915
+ quote = lexer->lookahead;
916
+ lexer->advance(lexer, false);
917
+ }
918
+ while (lexer->lookahead != 0) {
919
+ int32_t c = lexer->lookahead;
920
+ if (quote != 0 ? c == quote : (is_space(c) || c == '>')) {
921
+ break;
922
+ }
923
+ lexer->advance(lexer, false);
924
+ if (c == '<' && lexer->lookahead == '%') {
925
+ skip_server_block(lexer);
926
+ dynamic = true;
927
+ continue;
928
+ }
929
+ if (n + 1 < size) {
930
+ out[n++] = (char)ascii_lower(c);
931
+ }
932
+ }
933
+ if (quote != 0 && lexer->lookahead == quote) {
934
+ lexer->advance(lexer, false);
935
+ }
936
+ out[dynamic ? 0 : n] = '\0';
937
+ }
938
+
939
+ // A zero-width token at the start of a script start tag's attributes. It
940
+ // reads ahead through the tag for `runat` and `language`: a server script
941
+ // with a recognized language labels its own body with that language, and
942
+ // when no directive names the page language, ASP.NET takes it from the
943
+ // first such script, so nuggets follow it too. A client script's `language`
944
+ // (often VBScript or JavaScript) changes nothing.
945
+ static bool scan_script_tag_probe(TSLexer *lexer, Scanner *scanner) {
946
+ lexer->mark_end(lexer);
947
+ bool server = false;
948
+ uint8_t language = SCRIPT_UNSET;
949
+ while (lexer->lookahead != 0 && lexer->lookahead != '>') {
950
+ if (lexer->lookahead == '<') {
951
+ lexer->advance(lexer, false);
952
+ if (lexer->lookahead == '%') {
953
+ skip_server_block(lexer);
954
+ }
955
+ continue;
956
+ }
957
+ char name[TAG_NAME_MAX + 1];
958
+ if (!read_tag_name(lexer, name)) {
959
+ lexer->advance(lexer, false);
960
+ continue;
961
+ }
962
+ while (is_space(lexer->lookahead)) {
963
+ lexer->advance(lexer, false);
964
+ }
965
+ if (lexer->lookahead != '=') {
966
+ continue;
967
+ }
968
+ lexer->advance(lexer, false);
969
+ while (is_space(lexer->lookahead)) {
970
+ lexer->advance(lexer, false);
971
+ }
972
+ char value[32];
973
+ read_attribute_value(lexer, value, sizeof(value));
974
+ if (strcmp(name, "runat") == 0) {
975
+ server = strcmp(value, "server") == 0;
976
+ } else if (strcmp(name, "language") == 0) {
977
+ language = classify_language(value);
978
+ }
979
+ }
980
+ scanner->script_language = server ? language : SCRIPT_UNSET;
981
+ if (scanner->page_script_language == SCRIPT_UNSET) {
982
+ scanner->page_script_language = scanner->script_language;
983
+ }
984
+ lexer->result_symbol = SCRIPT_TAG_PROBE;
985
+ return true;
986
+ }
987
+
988
+ bool tree_sitter_aspx_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) {
989
+ Scanner *scanner = payload;
990
+ if (scanner == NULL) {
991
+ return false;
992
+ }
993
+
994
+ // During error recovery every external token is valid at once. No real
995
+ // parse state allows both a Language value and handler code, so that pair
996
+ // identifies recovery. Scanning then would turn arbitrary text into a
997
+ // language value and could switch the language for the rest of the page.
998
+ if (valid_symbols[LANG_CSHARP] && valid_symbols[HANDLER_CSHARP]) {
999
+ return false;
1000
+ }
1001
+
1002
+ if (valid_symbols[SCRIPT_TAG_PROBE]) {
1003
+ return scan_script_tag_probe(lexer, scanner);
1004
+ }
1005
+
1006
+ if (valid_symbols[IMPLICIT_END_TAG]) {
1007
+ return scan_implicit_end_tag(lexer, scanner);
1008
+ }
1009
+
1010
+ if (valid_symbols[START_TAG_NAME]) {
1011
+ return scan_start_tag_name(lexer, scanner);
1012
+ }
1013
+
1014
+ if (valid_symbols[END_TAG_NAME] || valid_symbols[STRAY_END_TAG_NAME]) {
1015
+ return scan_end_tag_name(lexer, scanner, valid_symbols);
1016
+ }
1017
+
1018
+ if (valid_symbols[SELF_CLOSING_DELIMITER]) {
1019
+ return scan_self_closing_delimiter(lexer, scanner);
1020
+ }
1021
+
1022
+ if (valid_symbols[LANG_CSHARP] || valid_symbols[LANG_VB] || valid_symbols[LANG_OTHER]) {
1023
+ return scan_language(lexer, scanner, valid_symbols);
1024
+ }
1025
+
1026
+ if (valid_symbols[SERVER_COMMENT_TEXT]) {
1027
+ if (!scan_until_server_comment_end(lexer)) {
1028
+ return false;
1029
+ }
1030
+ lexer->result_symbol = SERVER_COMMENT_TEXT;
1031
+ return true;
1032
+ }
1033
+
1034
+ if (valid_symbols[CODE_CSHARP] || valid_symbols[CODE_VB]) {
1035
+ return emit_code(lexer, scanner, valid_symbols, CODE_CSHARP, CODE_VB);
1036
+ }
1037
+
1038
+ if (valid_symbols[SCRIPT_CSHARP] || valid_symbols[SCRIPT_VB]) {
1039
+ return emit_code(lexer, scanner, valid_symbols, SCRIPT_CSHARP, SCRIPT_VB);
1040
+ }
1041
+
1042
+ if (valid_symbols[STYLE_TEXT]) {
1043
+ if (!scan_until_end_tag(lexer, "style")) {
1044
+ return false;
1045
+ }
1046
+ lexer->result_symbol = STYLE_TEXT;
1047
+ return true;
1048
+ }
1049
+
1050
+ if (valid_symbols[EXPRESSION_TEXT]) {
1051
+ if (!scan_until_percent_gt(lexer)) {
1052
+ return false;
1053
+ }
1054
+ lexer->result_symbol = EXPRESSION_TEXT;
1055
+ return true;
1056
+ }
1057
+
1058
+ if (valid_symbols[HANDLER_CSHARP] || valid_symbols[HANDLER_VB]) {
1059
+ enum TokenType symbol = scanner->language == LANGUAGE_VB ? HANDLER_VB : HANDLER_CSHARP;
1060
+ if (!valid_symbols[symbol] || !scan_handler_code(lexer)) {
1061
+ return false;
1062
+ }
1063
+ lexer->result_symbol = symbol;
1064
+ return true;
1065
+ }
1066
+
1067
+ return false;
1068
+ }