tree-sitter-aspx 0.1.3 → 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 CHANGED
@@ -1,22 +1,26 @@
1
1
  // External scanner for tree-sitter-aspx.
2
2
  //
3
- // Three jobs the ordinary lexer cannot do:
3
+ // Jobs the ordinary lexer cannot do:
4
4
  //
5
5
  // * Code, comments, style, and handler bodies run until a terminator
6
- // (`%>`, `--%>`, `</script>`, `</style>`, or EOF). Tree-sitter's
7
- // regex dialect has no lookahead, so a regex cannot stop before that
8
- // terminator without eating it.
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.
9
10
  // * The page `Language` value is recorded here. Later code tokens are
10
11
  // emitted as C# or VB so the injection query can name the grammar
11
12
  // without finding the directive again. The byte is serialized because
12
13
  // an incremental reparse may resume after the directive.
13
14
  // * A `Language` value that is neither C# nor VB is still consumed, and
14
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"
15
21
  #include "tree_sitter/parser.h"
16
22
 
17
- #include <stdlib.h>
18
23
  #include <string.h>
19
- #include <wctype.h>
20
24
 
21
25
  enum TokenType {
22
26
  LANG_CSHARP,
@@ -29,7 +33,14 @@ enum TokenType {
29
33
  SCRIPT_VB,
30
34
  STYLE_TEXT,
31
35
  EXPRESSION_TEXT,
32
- HANDLER_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,
33
44
  };
34
45
 
35
46
  enum Language {
@@ -37,14 +48,44 @@ enum Language {
37
48
  LANGUAGE_VB = 1,
38
49
  };
39
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
+
40
75
  typedef struct {
41
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];
42
83
  } Scanner;
43
84
 
44
85
  // ASCII-only lowercase. The Wasm parser target provides no libc ctype
45
- // functions, and language names here are ASCII by construction.
46
- static char ascii_lower(char c) {
47
- return (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : c;
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;
48
89
  }
49
90
 
50
91
  static bool is_space(int32_t c) {
@@ -52,26 +93,100 @@ static bool is_space(int32_t c) {
52
93
  }
53
94
 
54
95
  void *tree_sitter_aspx_external_scanner_create(void) {
55
- // malloc plus an explicit field set, not calloc: the compiler can lower
56
- // calloc to memset, which the Wasm parser target does not provide.
57
- Scanner *scanner = malloc(sizeof(Scanner));
58
- scanner->language = LANGUAGE_CSHARP;
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
+ }
59
111
  return scanner;
60
112
  }
61
113
 
62
114
  void tree_sitter_aspx_external_scanner_destroy(void *payload) {
63
- free(payload);
115
+ ts_free(payload);
64
116
  }
65
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.
66
122
  unsigned tree_sitter_aspx_external_scanner_serialize(void *payload, char *buffer) {
67
123
  Scanner *scanner = payload;
68
- buffer[0] = (char)scanner->language;
69
- return 1;
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;
70
149
  }
71
150
 
72
151
  void tree_sitter_aspx_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) {
73
152
  Scanner *scanner = payload;
74
- scanner->language = length > 0 ? (uint8_t)buffer[0] : LANGUAGE_CSHARP;
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);
75
190
  }
76
191
 
77
192
  static bool scan_until_percent_gt(TSLexer *lexer) {
@@ -94,19 +209,22 @@ static bool scan_until_percent_gt(TSLexer *lexer) {
94
209
  return has_content;
95
210
  }
96
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.
97
214
  static bool scan_until_server_comment_end(TSLexer *lexer) {
98
215
  bool has_content = false;
99
216
  while (lexer->lookahead != 0) {
100
217
  if (lexer->lookahead == '-') {
101
218
  lexer->mark_end(lexer);
102
- lexer->advance(lexer, false);
103
- if (lexer->lookahead == '-') {
219
+ unsigned dashes = 0;
220
+ while (lexer->lookahead == '-') {
104
221
  lexer->advance(lexer, false);
105
- if (lexer->lookahead == '%') {
106
- lexer->advance(lexer, false);
107
- if (lexer->lookahead == '>') {
108
- return has_content;
109
- }
222
+ dashes++;
223
+ }
224
+ if (dashes >= 2 && lexer->lookahead == '%') {
225
+ lexer->advance(lexer, false);
226
+ if (lexer->lookahead == '>') {
227
+ return has_content;
110
228
  }
111
229
  }
112
230
  lexer->mark_end(lexer);
@@ -123,7 +241,7 @@ static bool scan_until_server_comment_end(TSLexer *lexer) {
123
241
  static bool match_ci(TSLexer *lexer, const char *lower) {
124
242
  for (unsigned i = 0; lower[i] != '\0'; i++) {
125
243
  int32_t c = lexer->lookahead;
126
- if (c == 0 || towlower(c) != (wint_t)lower[i]) {
244
+ if (c == 0 || ascii_lower(c) != lower[i]) {
127
245
  return false;
128
246
  }
129
247
  lexer->advance(lexer, false);
@@ -131,24 +249,75 @@ static bool match_ci(TSLexer *lexer, const char *lower) {
131
249
  return true;
132
250
  }
133
251
 
134
- // `</name` with optional whitespace before `>`. On a match the token end
135
- // stays where the caller marked it, before the `<`. Otherwise the consumed
136
- // characters become content.
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.
137
303
  static bool scan_until_end_tag(TSLexer *lexer, const char *name) {
138
304
  bool has_content = false;
139
305
  while (lexer->lookahead != 0) {
140
306
  if (lexer->lookahead == '<') {
141
307
  lexer->mark_end(lexer);
142
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
+ }
143
317
  if (lexer->lookahead == '/') {
144
318
  lexer->advance(lexer, false);
145
- if (match_ci(lexer, name)) {
146
- while (is_space(lexer->lookahead)) {
147
- lexer->advance(lexer, false);
148
- }
149
- if (lexer->lookahead == '>') {
150
- return has_content;
151
- }
319
+ if (at_end_tag_name(lexer, name)) {
320
+ return has_content;
152
321
  }
153
322
  }
154
323
  lexer->mark_end(lexer);
@@ -162,44 +331,226 @@ static bool scan_until_end_tag(TSLexer *lexer, const char *name) {
162
331
  return has_content;
163
332
  }
164
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
+
165
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.
166
490
  char buf[32];
167
491
  unsigned n = 0;
168
- while (n + 1 < sizeof(buf)) {
492
+ bool truncated = false;
493
+ for (;;) {
169
494
  int32_t c = lexer->lookahead;
170
- if (c == 0 || c == '"' || c == '\'' || is_space(c) || c > 127) {
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)) {
171
502
  break;
503
+ } else {
504
+ lexer->advance(lexer, false);
172
505
  }
173
- buf[n++] = ascii_lower((char)c);
174
- lexer->advance(lexer, false);
506
+ if (n + 1 < sizeof(buf)) {
507
+ buf[n++] = (char)ascii_lower(c);
508
+ } else {
509
+ truncated = true;
510
+ }
511
+ lexer->mark_end(lexer);
175
512
  }
176
513
  buf[n] = '\0';
177
514
  if (n == 0) {
178
515
  return false;
179
516
  }
180
- lexer->mark_end(lexer);
181
517
 
182
- enum TokenType symbol = LANG_OTHER;
183
- if (strcmp(buf, "c#") == 0 || strcmp(buf, "csharp") == 0) {
184
- symbol = LANG_CSHARP;
185
- scanner->language = LANGUAGE_CSHARP;
186
- } else if (strcmp(buf, "vb") == 0 || strcmp(buf, "visualbasic") == 0) {
187
- symbol = LANG_VB;
188
- scanner->language = LANGUAGE_VB;
189
- }
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;
190
523
  if (!valid_symbols[symbol]) {
191
524
  return false;
192
525
  }
526
+ if (language != SCRIPT_UNSET) {
527
+ scanner->language = language;
528
+ scanner->language_set = true;
529
+ }
193
530
  lexer->result_symbol = symbol;
194
531
  return true;
195
532
  }
196
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
+
197
544
  static bool emit_code(TSLexer *lexer, Scanner *scanner, const bool *valid_symbols, enum TokenType csharp, enum TokenType vb) {
198
- enum TokenType symbol = scanner->language == LANGUAGE_VB ? vb : csharp;
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;
199
551
  if (!valid_symbols[symbol]) {
200
552
  return false;
201
553
  }
202
- bool is_script = csharp == SCRIPT_CSHARP;
203
554
  if (is_script) {
204
555
  if (!scan_until_end_tag(lexer, "script")) {
205
556
  return false;
@@ -211,8 +562,462 @@ static bool emit_code(TSLexer *lexer, Scanner *scanner, const bool *valid_symbol
211
562
  return true;
212
563
  }
213
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
+
214
988
  bool tree_sitter_aspx_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) {
215
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
+ }
216
1021
 
217
1022
  if (valid_symbols[LANG_CSHARP] || valid_symbols[LANG_VB] || valid_symbols[LANG_OTHER]) {
218
1023
  return scan_language(lexer, scanner, valid_symbols);
@@ -250,15 +1055,12 @@ bool tree_sitter_aspx_external_scanner_scan(void *payload, TSLexer *lexer, const
250
1055
  return true;
251
1056
  }
252
1057
 
253
- if (valid_symbols[HANDLER_TEXT]) {
254
- if (lexer->lookahead == 0) {
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)) {
255
1061
  return false;
256
1062
  }
257
- while (lexer->lookahead != 0) {
258
- lexer->advance(lexer, false);
259
- }
260
- lexer->mark_end(lexer);
261
- lexer->result_symbol = HANDLER_TEXT;
1063
+ lexer->result_symbol = symbol;
262
1064
  return true;
263
1065
  }
264
1066