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/README.md +1 -1
- package/grammar.js +137 -24
- package/package.json +3 -3
- package/prebuilds/linux-x64/tree-sitter-aspx.node +0 -0
- package/prebuilds/win32-x64/tree-sitter-aspx.node +0 -0
- package/queries/highlights.scm +2 -0
- package/queries/injections.scm +37 -8
- package/src/grammar.json +498 -122
- package/src/node-types.json +246 -25
- package/src/parser.c +8385 -3824
- package/src/scanner.c +860 -58
- package/tree-sitter.json +2 -2
package/src/scanner.c
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
// External scanner for tree-sitter-aspx.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
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).
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
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
|
|
46
|
-
static
|
|
47
|
-
return (c >= 'A' && c <= 'Z') ?
|
|
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
|
-
//
|
|
56
|
-
// calloc to memset, which the Wasm parser target does not provide.
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
219
|
+
unsigned dashes = 0;
|
|
220
|
+
while (lexer->lookahead == '-') {
|
|
104
221
|
lexer->advance(lexer, false);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 ||
|
|
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
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
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 (
|
|
146
|
-
|
|
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
|
-
|
|
492
|
+
bool truncated = false;
|
|
493
|
+
for (;;) {
|
|
169
494
|
int32_t c = lexer->lookahead;
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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[
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|