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