@rohal12/spindle 0.51.3 → 0.52.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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +4833 -1603
- package/dist/pkg/macro-registry.json +7 -7
- package/dist/pkg/story-variables.js +1658 -189
- package/package.json +5 -2
- package/src/automation/runner.ts +2 -1
- package/src/class-registry.ts +277 -90
- package/src/components/Passage.tsx +2 -2
- package/src/components/PassageDialog.tsx +2 -5
- package/src/components/StoryInterface.tsx +2 -4
- package/src/components/macros/Button.tsx +9 -32
- package/src/components/macros/Checkbox.tsx +10 -4
- package/src/components/macros/Computed.tsx +19 -13
- package/src/components/macros/Dialog.tsx +4 -1
- package/src/components/macros/For.tsx +33 -4
- package/src/components/macros/If.tsx +8 -0
- package/src/components/macros/Include.tsx +15 -24
- package/src/components/macros/MacroError.tsx +2 -1
- package/src/components/macros/MacroLink.tsx +14 -46
- package/src/components/macros/Meter.tsx +26 -40
- package/src/components/macros/Nobr.tsx +1 -0
- package/src/components/macros/PassageDisplay.tsx +3 -0
- package/src/components/macros/Print.tsx +4 -0
- package/src/components/macros/Radiobutton.tsx +27 -2
- package/src/components/macros/SaveManager.tsx +39 -14
- package/src/components/macros/Span.tsx +1 -0
- package/src/components/macros/StoryTitle.tsx +1 -0
- package/src/components/macros/Switch.tsx +13 -0
- package/src/components/macros/Unset.tsx +30 -10
- package/src/components/macros/VarDisplay.tsx +21 -4
- package/src/components/macros/Watch.tsx +88 -40
- package/src/components/macros/Widget.tsx +20 -1
- package/src/components/macros/WidgetInvocation.tsx +20 -158
- package/src/components/macros/arg-utils.ts +226 -0
- package/src/components/macros/detached-body.tsx +68 -0
- package/src/components/macros/option-utils.ts +10 -5
- package/src/define-macro.ts +44 -38
- package/src/execute-mutation.ts +499 -28
- package/src/expression.ts +88 -272
- package/src/hooks/use-action.ts +18 -3
- package/src/hooks/use-interpolate.ts +36 -5
- package/src/index.tsx +10 -1
- package/src/interpolation.ts +394 -96
- package/src/js-lexer.ts +1460 -0
- package/src/markup/code-attributes.ts +64 -0
- package/src/markup/markdown.ts +188 -9
- package/src/markup/render.tsx +552 -113
- package/src/markup/tokenizer.ts +601 -119
- package/src/prng.ts +8 -8
- package/src/registry.ts +35 -0
- package/src/saves/save-manager.ts +368 -153
- package/src/saves/storage.ts +24 -7
- package/src/saves/types.ts +20 -6
- package/src/store.ts +549 -137
- package/src/story-api.ts +46 -81
- package/src/story-init.ts +1 -1
- package/src/story-variables.ts +98 -102
- package/src/triggers.ts +6 -2
- package/src/utils/error-message.ts +12 -0
- package/src/utils/live-locals.ts +10 -3
- package/src/utils/namespace.ts +71 -0
- package/src/utils/object-path.ts +194 -0
- package/src/utils/stable-key.ts +82 -0
- package/src/widgets/widget-registry.ts +9 -0
- package/types/index.d.ts +43 -7
- package/types/tooling.d.ts +1 -0
package/src/js-lexer.ts
ADDED
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical scanner for the JavaScript in expressions and macro arguments.
|
|
3
|
+
*
|
|
4
|
+
* It is the one place that knows where string, template and regex literals
|
|
5
|
+
* and comments begin and end. The expression transformer (`expression.ts`)
|
|
6
|
+
* and the macro argument splitters (`components/macros/arg-utils.ts`) both
|
|
7
|
+
* walk source text through `lexJs` and differ only in what they do with the
|
|
8
|
+
* pieces it reports. The passage tokenizer (`markup/tokenizer.ts`) and
|
|
9
|
+
* attribute interpolation (`interpolation.ts`) find where the code in a
|
|
10
|
+
* `{…}` ends with `findCodeEnd`.
|
|
11
|
+
*
|
|
12
|
+
* It also finds the sigil variable references in code: `$name`, `_name` and
|
|
13
|
+
* `@name` where an identifier starts — not inside `a$b` or `ñ_x`, and not
|
|
14
|
+
* where a property name stands: after `.`, as an object literal key or a
|
|
15
|
+
* class member name — and `%name`.
|
|
16
|
+
*
|
|
17
|
+
* Besides literals, the scanner tracks whether the next token is an operand
|
|
18
|
+
* or an operator, which decides two ambiguities: `/` opens a regex in operand
|
|
19
|
+
* position and divides otherwise, and `%name` is a transient reference in
|
|
20
|
+
* operand position while `%` after an operand — `($n)%3`, `$a[i] %2`,
|
|
21
|
+
* `_i++ %n` — is the modulo operator. For that it tells blocks from object
|
|
22
|
+
* literals: `}` closing a block may be followed by a statement, `}` closing
|
|
23
|
+
* an object literal by an operator.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The sigil of a variable reference: story, temporary, local, transient. */
|
|
27
|
+
export type Sigil = '$' | '_' | '@' | '%';
|
|
28
|
+
|
|
29
|
+
export interface JsLexHandlers {
|
|
30
|
+
/**
|
|
31
|
+
* One character of code, outside literals and comments. `nesting` is the
|
|
32
|
+
* number of template-literal `${…}` interpolations around it (0 at top
|
|
33
|
+
* level).
|
|
34
|
+
*/
|
|
35
|
+
code?(ch: string, index: number, nesting: number): void;
|
|
36
|
+
/**
|
|
37
|
+
* Literal text passed through verbatim: a string or regex literal, a
|
|
38
|
+
* comment, or a piece of a template literal (its backticks, text, escapes
|
|
39
|
+
* and the `${` / `}` delimiters around interpolations, whose code is
|
|
40
|
+
* reported through `code`).
|
|
41
|
+
*/
|
|
42
|
+
literal?(text: string, index: number, nesting: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* A sigil variable reference — `$name`, `_name`, `@name` or `%name` —
|
|
45
|
+
* covering the sigil and `name`.
|
|
46
|
+
*/
|
|
47
|
+
variable?(sigil: Sigil, name: string, index: number, nesting: number): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What the source is: one expression (a macro argument, an `evaluate`d
|
|
52
|
+
* expression) or a list of statements (the body of `execute`).
|
|
53
|
+
*/
|
|
54
|
+
export type JsGoal = 'expression' | 'statements';
|
|
55
|
+
|
|
56
|
+
/** Transient name after `%`: an identifier, so `%3` is never a reference. */
|
|
57
|
+
const TRANS_NAME_RE = /[A-Za-z_]\w*/y;
|
|
58
|
+
/** An assignment operator: `=`, `+=`, `??=`, … but not `==` or `=>`. */
|
|
59
|
+
const ASSIGN_OP_RE = /(?:\*\*|<<|>>>?|&&|\|\||\?\?|[-+*/%&|^])?=(?![=>])/y;
|
|
60
|
+
/** Flags after the closing `/` of a regex literal. */
|
|
61
|
+
const REGEX_FLAGS_RE = /\w*/y;
|
|
62
|
+
/**
|
|
63
|
+
* Characters of identifiers (with the `\u…` escapes they may contain) and
|
|
64
|
+
* numbers. Surrogates stand for the astral identifier characters they encode.
|
|
65
|
+
*/
|
|
66
|
+
const WORD_CHAR_RE = /[\p{ID_Continue}$\u200c\u200d\\\ud800-\udfff]/u;
|
|
67
|
+
/** An identifier, or the rest of one after a `$`, `_` or `@` sigil. */
|
|
68
|
+
const IDENT_RE = /[\p{ID_Continue}$\u200c\u200d]*/uy;
|
|
69
|
+
/** A sigil variable name: the whole identifier after the sigil. */
|
|
70
|
+
const VAR_NAME_RE = /^\w+$/;
|
|
71
|
+
/** What may start a property name after a `get`, `set`, … modifier. */
|
|
72
|
+
const KEY_START_RE = /[\p{ID_Continue}$\\"'[*#]/u;
|
|
73
|
+
const SPACE_RE = /\s/;
|
|
74
|
+
const LINE_TERMINATOR_RE = /[\n\r\u2028\u2029]/;
|
|
75
|
+
const LINE_TERMINATOR_G = /[\n\r\u2028\u2029]/g;
|
|
76
|
+
/**
|
|
77
|
+
* Keywords followed by an operand rather than an operator (a declaration's
|
|
78
|
+
* binding counts as one: `const of of xs`, `let { _a: x } = o`).
|
|
79
|
+
*/
|
|
80
|
+
const OPERAND_KEYWORDS = new Set([
|
|
81
|
+
'await',
|
|
82
|
+
'case',
|
|
83
|
+
'const',
|
|
84
|
+
'delete',
|
|
85
|
+
'do',
|
|
86
|
+
'else',
|
|
87
|
+
'in',
|
|
88
|
+
'instanceof',
|
|
89
|
+
'let',
|
|
90
|
+
'new',
|
|
91
|
+
'return',
|
|
92
|
+
'throw',
|
|
93
|
+
'typeof',
|
|
94
|
+
'var',
|
|
95
|
+
'void',
|
|
96
|
+
'yield',
|
|
97
|
+
]);
|
|
98
|
+
/** Keywords whose parenthesised header is followed by a statement. */
|
|
99
|
+
const HEADER_KEYWORDS = new Set(['for', 'if', 'while', 'with']);
|
|
100
|
+
/** Keywords that a line break ends the statement after. */
|
|
101
|
+
const RESTRICTED_KEYWORDS = new Set(['break', 'continue', 'return']);
|
|
102
|
+
/** Words that may precede a property name in an object literal or class. */
|
|
103
|
+
const MODIFIERS = new Set(['async', 'get', 'set', 'static']);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Scan the `"…"` or `'…'` string literal opening at `start`. `end` is the
|
|
107
|
+
* index just past its closing quote, or `src.length` when it is unterminated
|
|
108
|
+
* (`closed` false). A backslash escapes the character after it, so a quote
|
|
109
|
+
* after an even run of backslashes closes the string and one after an odd
|
|
110
|
+
* run does not.
|
|
111
|
+
*/
|
|
112
|
+
export function scanStringLiteral(
|
|
113
|
+
src: string,
|
|
114
|
+
start: number,
|
|
115
|
+
): { end: number; closed: boolean } {
|
|
116
|
+
const quote = src.charAt(start);
|
|
117
|
+
let i = start + 1;
|
|
118
|
+
while (i < src.length) {
|
|
119
|
+
const c = src.charAt(i);
|
|
120
|
+
if (c === '\\') i += 2;
|
|
121
|
+
else if (c === quote) return { end: i + 1, closed: true };
|
|
122
|
+
else i++;
|
|
123
|
+
}
|
|
124
|
+
return { end: src.length, closed: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scan the regex literal (with flags) opening at `start`. `end` is the index
|
|
129
|
+
* just past it; an unterminated one (`closed` false) ends at the line break
|
|
130
|
+
* (escaped or not) or the end of the source.
|
|
131
|
+
*/
|
|
132
|
+
function scanRegex(
|
|
133
|
+
src: string,
|
|
134
|
+
start: number,
|
|
135
|
+
cache?: JsScanCache,
|
|
136
|
+
): { end: number; closed: boolean } {
|
|
137
|
+
// The rest of a regex reads the same from just past a class, however the
|
|
138
|
+
// scan got there: record how it ends there, and use what is recorded
|
|
139
|
+
const pending: number[] = [];
|
|
140
|
+
const done = (end: number, closed: boolean) => {
|
|
141
|
+
if (cache) {
|
|
142
|
+
for (const at of pending) cache.regexes.set(at, end * 2 + +closed);
|
|
143
|
+
}
|
|
144
|
+
return { end, closed };
|
|
145
|
+
};
|
|
146
|
+
let i = start + 1;
|
|
147
|
+
while (i < src.length) {
|
|
148
|
+
const c = src.charAt(i);
|
|
149
|
+
if (c === '\\') {
|
|
150
|
+
// An escaped line break ends the line, and the regex, too
|
|
151
|
+
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return done(i + 1, false);
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Unterminated at the end of the line: leave the rest to the parser
|
|
156
|
+
if (LINE_TERMINATOR_RE.test(c)) return done(i, false);
|
|
157
|
+
if (c === '[') {
|
|
158
|
+
// A class, where `/` does not close
|
|
159
|
+
i = scanRegexClass(src, i, cache);
|
|
160
|
+
if (src.charAt(i) !== ']') return done(i, false);
|
|
161
|
+
i++;
|
|
162
|
+
const known = cache?.regexes.get(i);
|
|
163
|
+
if (known !== undefined)
|
|
164
|
+
return done(Math.floor(known / 2), known % 2 === 1);
|
|
165
|
+
pending.push(i);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (c === '/') {
|
|
169
|
+
REGEX_FLAGS_RE.lastIndex = i + 1;
|
|
170
|
+
const flags = REGEX_FLAGS_RE.exec(src)?.[0].length ?? 0;
|
|
171
|
+
return done(i + 1 + flags, true);
|
|
172
|
+
}
|
|
173
|
+
i++;
|
|
174
|
+
}
|
|
175
|
+
return done(src.length, false);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Index of the `]` closing the regex character class opening at `open`, or
|
|
180
|
+
* of the line break (or the end of the source) that leaves it unterminated.
|
|
181
|
+
* Every `[` in a class reads the rest of it the same way, so the result is
|
|
182
|
+
* recorded for them too: scans of `/[/[/[…` from each `/` share one pass.
|
|
183
|
+
*/
|
|
184
|
+
function scanRegexClass(
|
|
185
|
+
src: string,
|
|
186
|
+
open: number,
|
|
187
|
+
cache?: JsScanCache,
|
|
188
|
+
): number {
|
|
189
|
+
const known = cache?.classes.get(open);
|
|
190
|
+
if (known !== undefined) return known;
|
|
191
|
+
const opens = [open];
|
|
192
|
+
let i = open + 1;
|
|
193
|
+
while (i < src.length) {
|
|
194
|
+
const c = src.charAt(i);
|
|
195
|
+
if (c === '\\') {
|
|
196
|
+
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) {
|
|
197
|
+
i++;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
i += 2;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (c === ']' || LINE_TERMINATOR_RE.test(c)) break;
|
|
204
|
+
if (c === '[' && cache) opens.push(i);
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
207
|
+
i = Math.min(i, src.length);
|
|
208
|
+
if (cache) for (const o of opens) cache.classes.set(o, i);
|
|
209
|
+
return i;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The next index from `from` on where `find` matches (-1 for none), for
|
|
214
|
+
* searches from increasing positions: a search from within the stretch the
|
|
215
|
+
* last one covered has the same answer.
|
|
216
|
+
*/
|
|
217
|
+
function nextMatch(
|
|
218
|
+
memo: NextMatch | undefined,
|
|
219
|
+
from: number,
|
|
220
|
+
find: (from: number) => number,
|
|
221
|
+
): number {
|
|
222
|
+
if (memo && from >= memo.from && (memo.at < 0 || from <= memo.at)) {
|
|
223
|
+
return memo.at;
|
|
224
|
+
}
|
|
225
|
+
const at = find(from);
|
|
226
|
+
if (memo) {
|
|
227
|
+
memo.from = from;
|
|
228
|
+
memo.at = at;
|
|
229
|
+
}
|
|
230
|
+
return at;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A search memo for `nextMatch`. */
|
|
234
|
+
interface NextMatch {
|
|
235
|
+
from: number;
|
|
236
|
+
at: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Index just past the comment opening at `start` (`//` or `/*`), or -1 for
|
|
241
|
+
* an unterminated `/*` comment.
|
|
242
|
+
*/
|
|
243
|
+
function findCommentEnd(
|
|
244
|
+
src: string,
|
|
245
|
+
start: number,
|
|
246
|
+
cache?: JsScanCache,
|
|
247
|
+
): number {
|
|
248
|
+
if (src.charAt(start + 1) === '/') {
|
|
249
|
+
const end = nextLineBreak(src, start, cache);
|
|
250
|
+
return end < 0 ? src.length : end;
|
|
251
|
+
}
|
|
252
|
+
const end = nextMatch(cache?.commentClose, start + 2, (from) =>
|
|
253
|
+
src.indexOf('*/', from),
|
|
254
|
+
);
|
|
255
|
+
return end < 0 ? -1 : end + 2;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Next line break from `from` on (-1 for none). */
|
|
259
|
+
function nextLineBreak(src: string, from: number, cache?: JsScanCache): number {
|
|
260
|
+
return nextMatch(cache?.lineEnd, from, (at) => {
|
|
261
|
+
LINE_TERMINATOR_G.lastIndex = at;
|
|
262
|
+
return LINE_TERMINATOR_G.exec(src)?.index ?? -1;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Is there a line break between `from` and `to`? */
|
|
267
|
+
function lineBreakIn(
|
|
268
|
+
src: string,
|
|
269
|
+
from: number,
|
|
270
|
+
to: number,
|
|
271
|
+
cache?: JsScanCache,
|
|
272
|
+
): boolean {
|
|
273
|
+
if (!cache) return LINE_TERMINATOR_RE.test(src.slice(from, to));
|
|
274
|
+
const at = nextLineBreak(src, from, cache);
|
|
275
|
+
return at >= 0 && at < to;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Index just past the comment opening at `start` (`//` or `/*`). */
|
|
279
|
+
function skipComment(src: string, start: number): number {
|
|
280
|
+
const end = findCommentEnd(src, start);
|
|
281
|
+
return end < 0 ? src.length : end;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Index of the first character from `i` on that is no space or comment. */
|
|
285
|
+
function skipTrivia(src: string, i: number): number {
|
|
286
|
+
while (i < src.length) {
|
|
287
|
+
const c = src.charAt(i);
|
|
288
|
+
if (SPACE_RE.test(c)) i++;
|
|
289
|
+
else if (c === '/' && '/*'.includes(src.charAt(i + 1)))
|
|
290
|
+
i = skipComment(src, i);
|
|
291
|
+
else break;
|
|
292
|
+
}
|
|
293
|
+
return i;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Lex the template literal opening at `start` (a backtick): its backticks,
|
|
298
|
+
* text and escapes are reported as literal text and the code of its `${…}`
|
|
299
|
+
* interpolations as `lexJs` does, one nesting level deeper. Returns the index
|
|
300
|
+
* just past the closing backtick, or `src.length` if it is unterminated.
|
|
301
|
+
*/
|
|
302
|
+
export function lexTemplate(
|
|
303
|
+
src: string,
|
|
304
|
+
start: number,
|
|
305
|
+
handlers: JsLexHandlers = {},
|
|
306
|
+
nesting = 0,
|
|
307
|
+
): number {
|
|
308
|
+
handlers.literal?.('`', start, nesting);
|
|
309
|
+
const outer = frame('template', '`', start);
|
|
310
|
+
return scan(src, handlers, start + 1, nesting, outer, newContext());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* State shared by the scans of one source: the main scan and the look-ahead
|
|
315
|
+
* scans that find where a bracketed assignment target ends.
|
|
316
|
+
*/
|
|
317
|
+
interface ScanContext {
|
|
318
|
+
/**
|
|
319
|
+
* Look ahead after a `%name` starting a line for an assignment. Off in
|
|
320
|
+
* look-ahead scans, which only need to match brackets — and whose own
|
|
321
|
+
* look-ahead could rescan the same text over and over.
|
|
322
|
+
*/
|
|
323
|
+
lookahead: boolean;
|
|
324
|
+
/**
|
|
325
|
+
* Index of the `]` matching the `[` at an index (`src.length` if there is
|
|
326
|
+
* none), as found by look-ahead scans: each text is scanned ahead once.
|
|
327
|
+
*/
|
|
328
|
+
brackets: Map<number, number>;
|
|
329
|
+
/** Set for `findCodeEnd`: the scan stops at the first lexical error. */
|
|
330
|
+
strict?: StrictScan;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface StrictScan {
|
|
334
|
+
/** Stop at a `{` in code for which this holds. */
|
|
335
|
+
stop?: (index: number) => boolean;
|
|
336
|
+
/** The scan ran into a lexical error. */
|
|
337
|
+
malformed: boolean;
|
|
338
|
+
/** The scan stopped where `stop` held. */
|
|
339
|
+
stopped: boolean;
|
|
340
|
+
/** Results shared with other scans of the source. */
|
|
341
|
+
cache?: JsScanCache;
|
|
342
|
+
/** `cache.braces`, unless the scan has a `stop`. */
|
|
343
|
+
braces?: Map<number, number>;
|
|
344
|
+
/** `cache.checkpoints` for this kind of scan. */
|
|
345
|
+
checkpoints?: Map<CheckpointKey, number>;
|
|
346
|
+
/** `cache.parens` for this kind of scan. */
|
|
347
|
+
parens?: Map<number, number>;
|
|
348
|
+
/** Checkpoints this scan passed, to record its result at. */
|
|
349
|
+
passed: CheckpointKey[];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Characters that a scan checkpoint follows (`checkpointKey`). */
|
|
353
|
+
const CHECKPOINT_AFTER = new Set([
|
|
354
|
+
'}',
|
|
355
|
+
'"',
|
|
356
|
+
"'",
|
|
357
|
+
'`',
|
|
358
|
+
'/',
|
|
359
|
+
'\n',
|
|
360
|
+
'\r',
|
|
361
|
+
'\u2028',
|
|
362
|
+
'\u2029',
|
|
363
|
+
' ',
|
|
364
|
+
'\t',
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* How far into a scan its results start to be shared within brackets
|
|
369
|
+
* (`checkpointKey`, `knownParen`). Most scans end sooner, so they don't pay
|
|
370
|
+
* for recording results no other scan will use; a long one pays this much
|
|
371
|
+
* before it can use what earlier scans recorded, which keeps scans from
|
|
372
|
+
* many starts about linear.
|
|
373
|
+
*/
|
|
374
|
+
const SHARE_AFTER = 256;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* A scan checkpoint: a position and the scan state there (`checkpointKey`).
|
|
378
|
+
* A number at the top level, a string within brackets.
|
|
379
|
+
*/
|
|
380
|
+
type CheckpointKey = number | string;
|
|
381
|
+
|
|
382
|
+
/** A frame result: it is still open at the end of the source. */
|
|
383
|
+
const UNCLOSED = -1;
|
|
384
|
+
/** A frame result: a lexical error inside it ends the scan. */
|
|
385
|
+
const MALFORMED = -2;
|
|
386
|
+
/** A frame result: the scan stops at index `s` inside it (`STOPPED - s`). */
|
|
387
|
+
const STOPPED = -3;
|
|
388
|
+
|
|
389
|
+
/** The closers whose search for their frame a frame result may depend on. */
|
|
390
|
+
const CLOSERS = [')', ']', '}'] as const;
|
|
391
|
+
const FRAME_KINDS: readonly Frame['kind'][] = [
|
|
392
|
+
'block',
|
|
393
|
+
'object',
|
|
394
|
+
'class',
|
|
395
|
+
'template',
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Key of a frame result, for the frames whose code lexes the same whatever
|
|
400
|
+
* surrounds them: braces (a block, an object literal, a class body), a
|
|
401
|
+
* template literal and its interpolations. Undefined for parentheses and
|
|
402
|
+
* square brackets, which a stray closer inside may close.
|
|
403
|
+
*/
|
|
404
|
+
function frameKey(f: Frame): number | undefined {
|
|
405
|
+
const kind = f.interpolation ? 4 : FRAME_KINDS.indexOf(f.kind);
|
|
406
|
+
return kind < 0 ? undefined : f.open * 5 + kind;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Results that `findCodeEnd` scans of one source share, so that scanning it
|
|
411
|
+
* from many starts doesn't lex the same code over and over.
|
|
412
|
+
*/
|
|
413
|
+
export interface JsScanCache {
|
|
414
|
+
/**
|
|
415
|
+
* Where each `{…}` frame, template literal or `${…}` interpolation closes:
|
|
416
|
+
* the index of its `}` or closing backtick, `UNCLOSED` or `MALFORMED`. The
|
|
417
|
+
* code inside such a frame lexes the same whatever surrounds it, given
|
|
418
|
+
* where it opens and its kind — a stray `)` or `]` inside never closes it
|
|
419
|
+
* — so a later scan entering the same frame skips to its end.
|
|
420
|
+
*/
|
|
421
|
+
braces: Map<number, number>;
|
|
422
|
+
/** Look-ahead bracket matches (`ScanContext.brackets`). */
|
|
423
|
+
brackets: Map<number, number>;
|
|
424
|
+
/** Regex character class ends (`scanRegexClass`). */
|
|
425
|
+
classes: Map<number, number>;
|
|
426
|
+
/** How a regex goes on from just past a class: `end * 2 + closed`. */
|
|
427
|
+
regexes: Map<number, number>;
|
|
428
|
+
/** The last search for a line break ending a `//` comment. */
|
|
429
|
+
lineEnd: NextMatch;
|
|
430
|
+
/** The last search for the end of a block comment. */
|
|
431
|
+
commentClose: NextMatch;
|
|
432
|
+
/**
|
|
433
|
+
* How scans go on from points, by the kind of scan (its goal and how it
|
|
434
|
+
* ends) and then by the point and the scan state there, the brackets open
|
|
435
|
+
* around it included: the index the scan ends at, `UNCLOSED` or
|
|
436
|
+
* `MALFORMED`. The points are where no word is being read (see
|
|
437
|
+
* `checkpointKey`). Scans from different starts soon pass such points in
|
|
438
|
+
* the same state, and from there on go the same way.
|
|
439
|
+
*/
|
|
440
|
+
checkpoints: Map<string, Map<CheckpointKey, number>>;
|
|
441
|
+
/**
|
|
442
|
+
* Ids of the stacks of open brackets that checkpoints have seen (see
|
|
443
|
+
* `scan`), by the id of the stack below the innermost bracket, the state
|
|
444
|
+
* of that one and the innermost bracket.
|
|
445
|
+
*/
|
|
446
|
+
stacks: Map<string, number>;
|
|
447
|
+
/**
|
|
448
|
+
* How `(…)` and `[…]` frames end, by the kind of scan and then by the frame
|
|
449
|
+
* (`parenKey`): `end * 2 + 1` if it closes at `end` and the code inside
|
|
450
|
+
* started a function or class body (which replaced the one to come, and
|
|
451
|
+
* can't follow once the frame is closed), `end * 2` otherwise, or
|
|
452
|
+
* `UNCLOSED`, `MALFORMED` or `STOPPED - s`. Unlike braces, a stray closer inside may close a frame
|
|
453
|
+
* around them, so the code inside lexes the same only around frames for
|
|
454
|
+
* which the closers it met find nothing to close; the key says which
|
|
455
|
+
* closers met none.
|
|
456
|
+
*/
|
|
457
|
+
parens: Map<string, Map<number, number>>;
|
|
458
|
+
/**
|
|
459
|
+
* How far into a scan its results start to be shared within brackets
|
|
460
|
+
* (`SHARE_AFTER`; tests set 0 to share them all).
|
|
461
|
+
*/
|
|
462
|
+
shareAfter: number;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function createJsScanCache(): JsScanCache {
|
|
466
|
+
return {
|
|
467
|
+
checkpoints: new Map(),
|
|
468
|
+
stacks: new Map(),
|
|
469
|
+
parens: new Map(),
|
|
470
|
+
shareAfter: SHARE_AFTER,
|
|
471
|
+
braces: new Map(),
|
|
472
|
+
brackets: new Map(),
|
|
473
|
+
classes: new Map(),
|
|
474
|
+
regexes: new Map(),
|
|
475
|
+
lineEnd: { from: Infinity, at: -1 },
|
|
476
|
+
commentClose: { from: Infinity, at: -1 },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const newContext = (): ScanContext => ({
|
|
481
|
+
lookahead: true,
|
|
482
|
+
brackets: new Map(),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
/** Index of the `]` matching the `[` at `open`, or `src.length`. */
|
|
486
|
+
function matchBracket(src: string, open: number, ctx: ScanContext): number {
|
|
487
|
+
let close = ctx.brackets.get(open);
|
|
488
|
+
if (close === undefined) {
|
|
489
|
+
const ahead = { lookahead: false, brackets: ctx.brackets };
|
|
490
|
+
close = scan(src, {}, open + 1, 0, frame('expr', ']', open), ahead);
|
|
491
|
+
ctx.brackets.set(open, close);
|
|
492
|
+
}
|
|
493
|
+
return close;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Is the code at `i`, just past a `%name`, the rest of an assignment target
|
|
498
|
+
* and its operator: ` = 1`, `.a[b] += 2`, but not `== 1`, `=> 1` or `% 2`?
|
|
499
|
+
*/
|
|
500
|
+
function assignmentFollows(src: string, i: number, ctx: ScanContext): boolean {
|
|
501
|
+
for (;;) {
|
|
502
|
+
i = skipTrivia(src, i);
|
|
503
|
+
const c = src.charAt(i);
|
|
504
|
+
if (c === '.' && src.charAt(i + 1) !== '.') {
|
|
505
|
+
IDENT_RE.lastIndex = skipTrivia(src, i + 1);
|
|
506
|
+
const name = IDENT_RE.exec(src)?.[0];
|
|
507
|
+
if (!name) return false;
|
|
508
|
+
i = IDENT_RE.lastIndex;
|
|
509
|
+
} else if (c === '[') {
|
|
510
|
+
i = matchBracket(src, i, ctx);
|
|
511
|
+
if (i >= src.length) return false;
|
|
512
|
+
i++;
|
|
513
|
+
} else {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
ASSIGN_OP_RE.lastIndex = i;
|
|
518
|
+
return ASSIGN_OP_RE.test(src);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The code between a pair of brackets, or the whole source.
|
|
523
|
+
*
|
|
524
|
+
* - `block`: statements (a block, a function body, the whole source as
|
|
525
|
+
* statements),
|
|
526
|
+
* - `object`: the property definitions of an object literal,
|
|
527
|
+
* - `class`: the member definitions of a class body,
|
|
528
|
+
* - `expr`: an expression (parentheses, square brackets, a template literal
|
|
529
|
+
* interpolation, the whole source as an expression),
|
|
530
|
+
* - `template`: the text of a template literal.
|
|
531
|
+
*/
|
|
532
|
+
interface Frame {
|
|
533
|
+
kind: 'block' | 'object' | 'class' | 'expr' | 'template';
|
|
534
|
+
/** The character closing it, or '' for the whole source. */
|
|
535
|
+
closer: string;
|
|
536
|
+
/** Index of its opening bracket. */
|
|
537
|
+
open: number;
|
|
538
|
+
/** A parenthesised `if`/`for`/`while`/`with` header: a statement follows. */
|
|
539
|
+
header: boolean;
|
|
540
|
+
/** Its closing `}` ends an operand (object literal, function expression). */
|
|
541
|
+
operand: boolean;
|
|
542
|
+
/** Conditional-expression `?`s awaiting their `:`. */
|
|
543
|
+
ternary: number;
|
|
544
|
+
/** A template literal's `${…}` interpolation. */
|
|
545
|
+
interpolation: boolean;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function frame(kind: Frame['kind'], closer: string, open = -1): Frame {
|
|
549
|
+
return {
|
|
550
|
+
kind,
|
|
551
|
+
closer,
|
|
552
|
+
open,
|
|
553
|
+
header: false,
|
|
554
|
+
operand: false,
|
|
555
|
+
ternary: 0,
|
|
556
|
+
interpolation: false,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Walk `src`, reporting code characters, literal text and variable
|
|
562
|
+
* references to `handlers` in source order. Every character is reported
|
|
563
|
+
* exactly once (a variable reference covers its sigil and name). Returns
|
|
564
|
+
* `src.length`.
|
|
565
|
+
*/
|
|
566
|
+
export function lexJs(
|
|
567
|
+
src: string,
|
|
568
|
+
handlers: JsLexHandlers,
|
|
569
|
+
goal: JsGoal = 'expression',
|
|
570
|
+
): number {
|
|
571
|
+
const outer = frame(goal === 'statements' ? 'block' : 'expr', '');
|
|
572
|
+
return scan(src, handlers, 0, 0, outer, newContext());
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export interface FindCodeEndOptions {
|
|
576
|
+
/** What the code is (default `expression`). */
|
|
577
|
+
goal?: JsGoal;
|
|
578
|
+
/** End the code at a `{` in code, at any depth, for which this holds. */
|
|
579
|
+
stop?: (index: number) => boolean;
|
|
580
|
+
/**
|
|
581
|
+
* Names `stop` for `cache`: scans with the same key share results (scans
|
|
582
|
+
* with a `stop` but no key don't use `cache.checkpoints`).
|
|
583
|
+
*/
|
|
584
|
+
stopKey?: string;
|
|
585
|
+
/**
|
|
586
|
+
* Results to share with other scans of the same source. Scanning a source
|
|
587
|
+
* from n starts then takes about linear time instead of n scans of the
|
|
588
|
+
* rest of it.
|
|
589
|
+
*/
|
|
590
|
+
cache?: JsScanCache;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Find where the code starting at `start` ends, lexing it as JavaScript:
|
|
595
|
+
* braces, quotes and backticks inside string, template and regex literals and
|
|
596
|
+
* comments don't count.
|
|
597
|
+
*
|
|
598
|
+
* Without `stop`, the code ends at the first `}` in code outside the
|
|
599
|
+
* brackets it opened (the `}` closing a `{…}` around it); with `stop`, at
|
|
600
|
+
* the first `{` in code, at any depth, for which `stop` holds. Returns the
|
|
601
|
+
* index of that `}` or `{`.
|
|
602
|
+
*
|
|
603
|
+
* Returns -1 when there is no such end, or when the code before it is not
|
|
604
|
+
* well-formed JavaScript as far as a lexer can tell: an unterminated string,
|
|
605
|
+
* regex literal or block comment, or a quote directly after an identifier or
|
|
606
|
+
* number (`don't`, but not `typeof'x'`). Callers fall back to a more lenient
|
|
607
|
+
* reading there, so text that only looks like code is not swallowed by an
|
|
608
|
+
* apostrophe or a stray quote.
|
|
609
|
+
*/
|
|
610
|
+
export function findCodeEnd(
|
|
611
|
+
src: string,
|
|
612
|
+
start: number,
|
|
613
|
+
{ goal = 'expression', stop, stopKey, cache }: FindCodeEndOptions = {},
|
|
614
|
+
): number {
|
|
615
|
+
const strict: StrictScan = {
|
|
616
|
+
stop,
|
|
617
|
+
malformed: false,
|
|
618
|
+
stopped: false,
|
|
619
|
+
passed: [],
|
|
620
|
+
};
|
|
621
|
+
strict.cache = cache;
|
|
622
|
+
// Where a `{…}` frame ends depends on `stop`
|
|
623
|
+
if (cache && !stop) strict.braces = cache.braces;
|
|
624
|
+
if (cache && (!stop || stopKey !== undefined)) {
|
|
625
|
+
const kindKey = `${goal} ${stop ? `stop ${stopKey}` : '}'}`;
|
|
626
|
+
let checkpoints = cache.checkpoints.get(kindKey);
|
|
627
|
+
if (!checkpoints) cache.checkpoints.set(kindKey, (checkpoints = new Map()));
|
|
628
|
+
strict.checkpoints = checkpoints;
|
|
629
|
+
let parens = cache.parens.get(kindKey);
|
|
630
|
+
if (!parens) cache.parens.set(kindKey, (parens = new Map()));
|
|
631
|
+
strict.parens = parens;
|
|
632
|
+
}
|
|
633
|
+
const ctx: ScanContext = {
|
|
634
|
+
lookahead: true,
|
|
635
|
+
brackets: cache?.brackets ?? new Map(),
|
|
636
|
+
strict,
|
|
637
|
+
};
|
|
638
|
+
const kind = goal === 'statements' ? 'block' : 'expr';
|
|
639
|
+
const outer = frame(kind, stop ? '' : '}', start - 1);
|
|
640
|
+
const end = scan(src, {}, start, 0, outer, ctx);
|
|
641
|
+
if (strict.checkpoints) {
|
|
642
|
+
const ended = stop ? strict.stopped : end < src.length;
|
|
643
|
+
const result = strict.malformed ? MALFORMED : ended ? end : UNCLOSED;
|
|
644
|
+
for (const at of strict.passed) strict.checkpoints.set(at, result);
|
|
645
|
+
}
|
|
646
|
+
if (strict.malformed) return -1;
|
|
647
|
+
if (stop) return strict.stopped ? end : -1;
|
|
648
|
+
return end < src.length ? end : -1;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Scan from `start` within `outer`. Returns `src.length`, or the index of
|
|
653
|
+
* the `]` closing an `outer` square bracket, or the index just past the
|
|
654
|
+
* backtick closing an `outer` template literal.
|
|
655
|
+
*
|
|
656
|
+
* Nested brackets, template literals and their interpolations are frames
|
|
657
|
+
* on a stack, not recursive calls: however deep the nesting, the scan
|
|
658
|
+
* cannot overflow the call stack.
|
|
659
|
+
*/
|
|
660
|
+
function scan(
|
|
661
|
+
src: string,
|
|
662
|
+
handlers: JsLexHandlers,
|
|
663
|
+
start: number,
|
|
664
|
+
nesting: number,
|
|
665
|
+
outer: Frame,
|
|
666
|
+
ctx: ScanContext,
|
|
667
|
+
): number {
|
|
668
|
+
const frames: Frame[] = [outer];
|
|
669
|
+
const top = () => frames[frames.length - 1]!;
|
|
670
|
+
/**
|
|
671
|
+
* Indices of the open frames closed by `)`, `]` and `}` (above `outer`),
|
|
672
|
+
* innermost last: the frame a closer closes is found without walking the
|
|
673
|
+
* stack.
|
|
674
|
+
*/
|
|
675
|
+
const closers: Record<string, number[]> = { ')': [], ']': [], '}': [] };
|
|
676
|
+
const innermost = (c: string) => {
|
|
677
|
+
const list = closers[c]!;
|
|
678
|
+
return list.length ? list[list.length - 1]! : 0;
|
|
679
|
+
};
|
|
680
|
+
let i = start;
|
|
681
|
+
|
|
682
|
+
let operandNext = true; // an operand (not an operator) comes next
|
|
683
|
+
let stmtNext = outer.kind === 'block'; // a statement starts here
|
|
684
|
+
let arrowBody = false; // an arrow function body starts here
|
|
685
|
+
let keyNext = false; // a property name may come next
|
|
686
|
+
let word = ''; // identifier/number currently being read
|
|
687
|
+
let afterDot = false; // the next word is a property name, never a keyword
|
|
688
|
+
let dots = 0; // length of the current run of `.` tokens
|
|
689
|
+
let afterHeaderKeyword = false; // last token was if/while/for/with
|
|
690
|
+
let lastPunct = '';
|
|
691
|
+
let lastKeyword = ''; // the last token, if it was a keyword
|
|
692
|
+
let lineBreak = false; // a line break since the last token
|
|
693
|
+
/** The next `{` at this depth opens a function or class body. */
|
|
694
|
+
let pendingBody: { depth: number; frame: Frame } | undefined;
|
|
695
|
+
|
|
696
|
+
const strict = ctx.strict;
|
|
697
|
+
const braces = strict?.braces;
|
|
698
|
+
const stacks = strict?.checkpoints && strict.cache?.stacks;
|
|
699
|
+
/**
|
|
700
|
+
* With checkpoints, per open frame the id of the stack up to it, found
|
|
701
|
+
* when a checkpoint needs it: the kind and flags of each frame, and the
|
|
702
|
+
* conditional-expression count of each but the innermost (which changes
|
|
703
|
+
* only while it is innermost, and is part of the checkpoint state). How a
|
|
704
|
+
* scan goes on depends on these, not on where the frames opened.
|
|
705
|
+
*/
|
|
706
|
+
const stackIds: (number | undefined)[] | undefined = stacks ? [0] : undefined;
|
|
707
|
+
|
|
708
|
+
/** The id of the stack of open frames (`stackIds`). */
|
|
709
|
+
function stackId(): number {
|
|
710
|
+
let k = frames.length - 1;
|
|
711
|
+
while (stackIds![k] === undefined) k--;
|
|
712
|
+
for (k++; k < frames.length; k++) {
|
|
713
|
+
const f = frames[k]!;
|
|
714
|
+
const key =
|
|
715
|
+
`${stackIds![k - 1]} ${frames[k - 1]!.ternary} ${f.kind} ${f.closer}` +
|
|
716
|
+
` ${+f.header}${+f.operand}${+f.interpolation}`;
|
|
717
|
+
let id = stacks!.get(key);
|
|
718
|
+
if (id === undefined) stacks!.set(key, (id = stacks!.size + 1));
|
|
719
|
+
stackIds![k] = id;
|
|
720
|
+
}
|
|
721
|
+
return stackIds![frames.length - 1]!;
|
|
722
|
+
}
|
|
723
|
+
/** A `{…}` frame result found in `braces`, to skip to. */
|
|
724
|
+
let skipTo: { frame: Frame; end: number } | undefined;
|
|
725
|
+
|
|
726
|
+
const parens = strict?.parens;
|
|
727
|
+
const shareAfter = strict?.cache?.shareAfter ?? SHARE_AFTER;
|
|
728
|
+
/**
|
|
729
|
+
* With `parens`, per open frame and per closer in `CLOSERS`: the lowest
|
|
730
|
+
* frame index the closer looked for its frame at, while this frame or one
|
|
731
|
+
* opened in it was innermost. Below a frame's own index, the code in it
|
|
732
|
+
* depended on what is around it. A frame's entry takes in those of the
|
|
733
|
+
* frames opened in it as they close.
|
|
734
|
+
*/
|
|
735
|
+
const lowest: number[][] | undefined = parens
|
|
736
|
+
? [[Infinity, Infinity, Infinity]]
|
|
737
|
+
: undefined;
|
|
738
|
+
/**
|
|
739
|
+
* How many function or class bodies to come the scan has started, and with
|
|
740
|
+
* `parens`, that count when each open frame opened. Code in a `(…)` or
|
|
741
|
+
* `[…]` that starts one replaces the one to come, and the new one can't
|
|
742
|
+
* follow once the frame is closed: none is to come then.
|
|
743
|
+
*/
|
|
744
|
+
let bodyStarts = 0;
|
|
745
|
+
const startsAt: number[] | undefined = parens ? [0] : undefined;
|
|
746
|
+
/** A `(…)` or `[…]` frame result found in `parens`, to skip to. */
|
|
747
|
+
let parenTo: { frame: Frame; result: number } | undefined;
|
|
748
|
+
|
|
749
|
+
/** The closer `c` looked for its frame and found index `k`. */
|
|
750
|
+
function lookedFor(c: (typeof CLOSERS)[number], k: number) {
|
|
751
|
+
if (!lowest) return;
|
|
752
|
+
const entry = lowest[lowest.length - 1]!;
|
|
753
|
+
const n = CLOSERS.indexOf(c);
|
|
754
|
+
if (k < entry[n]!) entry[n] = k;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Take the entry of the frame at index `k` into the one below it. */
|
|
758
|
+
function foldLowest(k: number) {
|
|
759
|
+
const from = lowest![k]!;
|
|
760
|
+
const into = lowest![k - 1]!;
|
|
761
|
+
for (let n = 0; n < 3; n++) if (from[n]! < into[n]!) into[n] = from[n]!;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Key of a `(…)` or `[…]` frame result, but for the closers it met. */
|
|
765
|
+
const parenKey = (f: Frame) =>
|
|
766
|
+
(f.open * 3 + (f.closer === ']' ? 2 : +f.header)) * 8;
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Record how the frame at index `k` ends, if it is a `(…)` or `[…]`, its
|
|
770
|
+
* `lowest` entry complete: under the closers that looked below it, which
|
|
771
|
+
* found nothing to close (else the frame was closed with them).
|
|
772
|
+
*/
|
|
773
|
+
function recordParen(k: number, result: number) {
|
|
774
|
+
const f = frames[k]!;
|
|
775
|
+
if (k === 0 || (f.closer !== ')' && f.closer !== ']')) return;
|
|
776
|
+
if (f.open - start < shareAfter) return;
|
|
777
|
+
let met = 0;
|
|
778
|
+
for (let n = 0; n < 3; n++) if (lowest![k]![n]! < k) met |= 1 << n;
|
|
779
|
+
parens!.set(parenKey(f) + met, result);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Record how the open `(…)` and `[…]` frames end: the scan ends in them. */
|
|
783
|
+
function recordOpenParens(result: number) {
|
|
784
|
+
if (!lowest) return;
|
|
785
|
+
for (let k = frames.length - 1; k > 0; k--) {
|
|
786
|
+
recordParen(k, result);
|
|
787
|
+
foldLowest(k);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* How an earlier scan found a `(…)` or `[…]` frame opening here to end,
|
|
793
|
+
* if it did: a result recorded under closers that find nothing to close
|
|
794
|
+
* around the frame here too. The frame is not open yet.
|
|
795
|
+
*/
|
|
796
|
+
function knownParen(f: Frame): number | undefined {
|
|
797
|
+
if (!parens || f.open - start < shareAfter) return undefined;
|
|
798
|
+
// Where each closer would look for its frame, and whether it would find
|
|
799
|
+
// none to close (it is stray)
|
|
800
|
+
const at = [
|
|
801
|
+
Math.max(innermost(')'), innermost('}')),
|
|
802
|
+
Math.max(innermost(']'), innermost('}')),
|
|
803
|
+
innermost('}'),
|
|
804
|
+
];
|
|
805
|
+
let stray = 0;
|
|
806
|
+
for (let n = 0; n < 3; n++) {
|
|
807
|
+
const k = at[n]!;
|
|
808
|
+
if (k === 0 || (n < 2 && frames[k]!.closer !== CLOSERS[n])) {
|
|
809
|
+
stray |= 1 << n;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const key = parenKey(f);
|
|
813
|
+
for (let met = stray; ; met = (met - 1) & stray) {
|
|
814
|
+
const result = parens.get(key + met);
|
|
815
|
+
if (result !== undefined) {
|
|
816
|
+
// The closers it met look here too
|
|
817
|
+
for (let n = 0; n < 3; n++) {
|
|
818
|
+
if (met & (1 << n)) lookedFor(CLOSERS[n]!, at[n]!);
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
if (met === 0) return undefined;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** Record how the open frames end, when the scan ends in them. */
|
|
827
|
+
function recordOpen(result: number) {
|
|
828
|
+
if (!braces) return;
|
|
829
|
+
for (let k = 1; k < frames.length; k++) record(frames[k]!, result);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Record how a frame ends. */
|
|
833
|
+
function record(f: Frame, result: number) {
|
|
834
|
+
const key = braces && frameKey(f);
|
|
835
|
+
if (key !== undefined) braces!.set(key, result);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** How an earlier scan found the frame `f` to end, if it did. */
|
|
839
|
+
const known = (f: Frame) => {
|
|
840
|
+
const key = braces && frameKey(f);
|
|
841
|
+
return key === undefined ? undefined : braces!.get(key);
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
/** End a strict scan at a lexical error. */
|
|
845
|
+
const malformed = () => {
|
|
846
|
+
recordOpen(MALFORMED);
|
|
847
|
+
recordOpenParens(MALFORMED);
|
|
848
|
+
strict!.malformed = true;
|
|
849
|
+
return src.length;
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
function push(f: Frame) {
|
|
853
|
+
stackIds?.push(undefined);
|
|
854
|
+
closers[f.closer]?.push(frames.length);
|
|
855
|
+
frames.push(f);
|
|
856
|
+
lowest?.push([Infinity, Infinity, Infinity]);
|
|
857
|
+
startsAt?.push(bodyStarts);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Just past a token or a space, line break or comment, but not within a
|
|
862
|
+
* word: the key of the position and the whole scan state there, the open
|
|
863
|
+
* frames included, which decide how the scan goes on (or undefined
|
|
864
|
+
* elsewhere). No word is being read there, and the character before is no
|
|
865
|
+
* word character, so a quote after it starts a string in any scan.
|
|
866
|
+
*
|
|
867
|
+
* Within brackets too, once the scan is long (`SHARE_AFTER`). Scans that
|
|
868
|
+
* never passed such a point in the same state each ran on to the end of
|
|
869
|
+
* the source, so many of them took quadratic time: inside an unclosed `(`
|
|
870
|
+
* in `{(}{(}{(…` (there was no point within brackets), or after the regex
|
|
871
|
+
* literals of `{a'</a </p>…` (there was no point after a space).
|
|
872
|
+
*/
|
|
873
|
+
function checkpointKey(): CheckpointKey | undefined {
|
|
874
|
+
const ternary = top().ternary;
|
|
875
|
+
if (
|
|
876
|
+
ternary > 3 ||
|
|
877
|
+
!CHECKPOINT_AFTER.has(src.charAt(i - 1)) ||
|
|
878
|
+
(frames.length > 1 && i - start < shareAfter)
|
|
879
|
+
) {
|
|
880
|
+
return undefined;
|
|
881
|
+
}
|
|
882
|
+
let state = ternary;
|
|
883
|
+
if (operandNext) state |= 1 << 2;
|
|
884
|
+
if (stmtNext) state |= 1 << 3;
|
|
885
|
+
if (keyNext) state |= 1 << 4;
|
|
886
|
+
if (pendingBody) {
|
|
887
|
+
state |= pendingBody.frame.kind === 'class' ? 1 << 5 : 1 << 6;
|
|
888
|
+
if (pendingBody.frame.operand) state |= 1 << 7;
|
|
889
|
+
}
|
|
890
|
+
if (arrowBody) state |= 1 << 8;
|
|
891
|
+
if (afterDot) state |= 1 << 9;
|
|
892
|
+
if (afterHeaderKeyword) state |= 1 << 10;
|
|
893
|
+
if (lineBreak) state |= 1 << 11;
|
|
894
|
+
if (RESTRICTED_KEYWORDS.has(lastKeyword)) state |= 1 << 12;
|
|
895
|
+
if (lastPunct === '=') state |= 1 << 13;
|
|
896
|
+
// A run of dots: one or two (a spread may follow), three, or more
|
|
897
|
+
if (lastPunct === '.') state |= Math.min(dots, 4) << 14;
|
|
898
|
+
const at = i * (1 << 17) + state;
|
|
899
|
+
if (frames.length === 1) return at;
|
|
900
|
+
// A function or class body to come may open at an outer level
|
|
901
|
+
const body = pendingBody ? frames.length - pendingBody.depth : '';
|
|
902
|
+
return `${at} ${stackId()} ${body}`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* May a string literal follow the word just read (`word`, or a variable
|
|
907
|
+
* reference) with nothing between? Only after a keyword taking an operand
|
|
908
|
+
* (`typeof'x'`, `case"a"`), `of` in a `for` header, or a modifier before a
|
|
909
|
+
* property name (`static'x'`, `get"y"() {}`); elsewhere the quote is an
|
|
910
|
+
* apostrophe (`don't`), and the text is no JavaScript.
|
|
911
|
+
*/
|
|
912
|
+
function stringMayFollowWord(): boolean {
|
|
913
|
+
return (
|
|
914
|
+
OPERAND_KEYWORDS.has(word) ||
|
|
915
|
+
(word === 'of' && top().header) ||
|
|
916
|
+
(keyNext && MODIFIERS.has(word))
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Close the frames from index `k` on. */
|
|
921
|
+
function truncate(k: number) {
|
|
922
|
+
if (lowest) {
|
|
923
|
+
for (let j = frames.length - 1; j >= k; j--) foldLowest(j);
|
|
924
|
+
lowest.length = k;
|
|
925
|
+
startsAt!.length = k;
|
|
926
|
+
}
|
|
927
|
+
frames.length = k;
|
|
928
|
+
if (stackIds) stackIds.length = k;
|
|
929
|
+
for (const list of Object.values(closers)) {
|
|
930
|
+
while (list.length && list[list.length - 1]! >= k) list.pop();
|
|
931
|
+
}
|
|
932
|
+
// A function or class body can't follow once its level is closed
|
|
933
|
+
if (pendingBody && pendingBody.depth > k) pendingBody = undefined;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const code = (ch: string, index: number) =>
|
|
937
|
+
handlers.code?.(ch, index, nesting);
|
|
938
|
+
const literal = (text: string, index: number) =>
|
|
939
|
+
handlers.literal?.(text, index, nesting);
|
|
940
|
+
/** Literal text from `from` to `to`, sliced only for a handler. */
|
|
941
|
+
const literalSpan = (from: number, to: number) =>
|
|
942
|
+
handlers.literal?.(src.slice(from, to), from, nesting);
|
|
943
|
+
|
|
944
|
+
/** Track the word just read (`i` is the index just past it). */
|
|
945
|
+
function endWord() {
|
|
946
|
+
if (!word) return;
|
|
947
|
+
// A property name is never a keyword: `a.return`, `{ in: 1 }`
|
|
948
|
+
const keyword = afterDot || keyNext ? '' : word;
|
|
949
|
+
const inOperandPosition = operandNext && !stmtNext;
|
|
950
|
+
operandNext =
|
|
951
|
+
OPERAND_KEYWORDS.has(keyword) ||
|
|
952
|
+
// `for (x of …)`, but `of` is an identifier where an operand goes
|
|
953
|
+
(keyword === 'of' && top().header && !operandNext);
|
|
954
|
+
afterHeaderKeyword = HEADER_KEYWORDS.has(keyword);
|
|
955
|
+
if (keyword === 'function' || keyword === 'class') {
|
|
956
|
+
// An expression where an operand is expected, else a declaration: at
|
|
957
|
+
// a statement start, or after an operand and a line break (ASI)
|
|
958
|
+
const body = frame(keyword === 'class' ? 'class' : 'block', '}');
|
|
959
|
+
body.operand = inOperandPosition;
|
|
960
|
+
pendingBody = { depth: frames.length, frame: body };
|
|
961
|
+
bodyStarts++;
|
|
962
|
+
}
|
|
963
|
+
// `get name()`, `static _x = 1`: the property name is still to come
|
|
964
|
+
keyNext =
|
|
965
|
+
keyNext &&
|
|
966
|
+
MODIFIERS.has(word) &&
|
|
967
|
+
KEY_START_RE.test(src.charAt(skipTrivia(src, i)));
|
|
968
|
+
stmtNext =
|
|
969
|
+
keyword === 'else' ||
|
|
970
|
+
keyword === 'do' ||
|
|
971
|
+
(keyword === 'async' && stmtNext);
|
|
972
|
+
arrowBody = false;
|
|
973
|
+
afterDot = false;
|
|
974
|
+
lastPunct = '';
|
|
975
|
+
lastKeyword = keyword;
|
|
976
|
+
lineBreak = false;
|
|
977
|
+
word = '';
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** A string, template or regex literal, or a variable reference, ended. */
|
|
981
|
+
function endOperand() {
|
|
982
|
+
endWord();
|
|
983
|
+
operandNext = false;
|
|
984
|
+
stmtNext = false;
|
|
985
|
+
arrowBody = false;
|
|
986
|
+
keyNext = false;
|
|
987
|
+
afterHeaderKeyword = false;
|
|
988
|
+
afterDot = false;
|
|
989
|
+
lastPunct = '';
|
|
990
|
+
lastKeyword = '';
|
|
991
|
+
lineBreak = false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Track an operator token: `operandNext` tells what may follow it. */
|
|
995
|
+
function endPunct(punct: string, nextIsOperand: boolean) {
|
|
996
|
+
operandNext = nextIsOperand;
|
|
997
|
+
stmtNext = false;
|
|
998
|
+
arrowBody = false;
|
|
999
|
+
keyNext = false;
|
|
1000
|
+
afterDot = punct === '.' && !nextIsOperand;
|
|
1001
|
+
afterHeaderKeyword = false;
|
|
1002
|
+
lastPunct = punct;
|
|
1003
|
+
lastKeyword = '';
|
|
1004
|
+
lineBreak = false;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* A line break between tokens. After `return`, `break` or `continue` it
|
|
1009
|
+
* ends the statement (ASI): `return⏎function f() {}` declares `f`.
|
|
1010
|
+
*/
|
|
1011
|
+
function lineBreakSeen() {
|
|
1012
|
+
lineBreak = true;
|
|
1013
|
+
if (RESTRICTED_KEYWORDS.has(lastKeyword)) {
|
|
1014
|
+
operandNext = true;
|
|
1015
|
+
stmtNext = true;
|
|
1016
|
+
lastKeyword = '';
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function open(f: Frame) {
|
|
1021
|
+
push(f);
|
|
1022
|
+
stmtNext = f.kind === 'block';
|
|
1023
|
+
keyNext = f.kind === 'object' || f.kind === 'class';
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function openBrace() {
|
|
1027
|
+
let f: Frame;
|
|
1028
|
+
if (pendingBody?.depth === frames.length) {
|
|
1029
|
+
f = pendingBody.frame;
|
|
1030
|
+
pendingBody = undefined;
|
|
1031
|
+
} else if (!operandNext || stmtNext || arrowBody) {
|
|
1032
|
+
f = frame('block', '}');
|
|
1033
|
+
} else {
|
|
1034
|
+
f = frame('object', '}');
|
|
1035
|
+
f.operand = true;
|
|
1036
|
+
}
|
|
1037
|
+
f.open = i;
|
|
1038
|
+
endPunct('{', true);
|
|
1039
|
+
const end = known(f);
|
|
1040
|
+
if (end === undefined) open(f);
|
|
1041
|
+
else skipTo = { frame: f, end };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/** State after the `}` closing `closed`, a block or literal. */
|
|
1045
|
+
function afterBrace(closed: Frame) {
|
|
1046
|
+
if (closed.operand) {
|
|
1047
|
+
endPunct('}', false);
|
|
1048
|
+
} else {
|
|
1049
|
+
// A block: a statement (or the next class member) may follow
|
|
1050
|
+
endPunct('}', true);
|
|
1051
|
+
stmtNext = top().kind === 'block';
|
|
1052
|
+
keyNext = top().kind === 'class';
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Close the innermost frame that `c` closes; a stray closer, with no such
|
|
1058
|
+
* frame within the innermost braces (a block, an object literal, a class
|
|
1059
|
+
* body or an interpolation), is ignored. So a stray `)` or `]` never
|
|
1060
|
+
* closes the braces around it, and how a `{…}` ends depends only on the
|
|
1061
|
+
* code inside it.
|
|
1062
|
+
*/
|
|
1063
|
+
function close(c: (typeof CLOSERS)[number]) {
|
|
1064
|
+
const k = Math.max(innermost(c), innermost('}'));
|
|
1065
|
+
lookedFor(c, k);
|
|
1066
|
+
if (k === 0 || frames[k]!.closer !== c) {
|
|
1067
|
+
endPunct(c, c === '}');
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const closed = frames[k]!;
|
|
1071
|
+
if (lowest && c !== '}') {
|
|
1072
|
+
// Its entry complete, with those of the frames still open in it
|
|
1073
|
+
for (let j = frames.length - 1; j > k; j--) foldLowest(j);
|
|
1074
|
+
// Whether code in it started a function or class body: none is to come
|
|
1075
|
+
const bodyStarted = bodyStarts !== startsAt![k];
|
|
1076
|
+
recordParen(k, i * 2 + +bodyStarted);
|
|
1077
|
+
}
|
|
1078
|
+
truncate(k);
|
|
1079
|
+
if (c === ']' && !ctx.lookahead) ctx.brackets.set(closed.open, i);
|
|
1080
|
+
if (c === ')') {
|
|
1081
|
+
// `if (…) %x = 1` vs `($n)%3`
|
|
1082
|
+
endPunct(c, closed.header);
|
|
1083
|
+
stmtNext = closed.header;
|
|
1084
|
+
} else if (c === ']') {
|
|
1085
|
+
endPunct(c, false);
|
|
1086
|
+
} else {
|
|
1087
|
+
record(closed, i);
|
|
1088
|
+
afterBrace(closed);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function trackCode(c: string) {
|
|
1093
|
+
if (WORD_CHAR_RE.test(c)) {
|
|
1094
|
+
word += c;
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
endWord();
|
|
1098
|
+
// A line break alone never changes operand/operator position:
|
|
1099
|
+
// `$x = 5\n%n` continues the expression, as in JavaScript.
|
|
1100
|
+
if (LINE_TERMINATOR_RE.test(c)) lineBreakSeen();
|
|
1101
|
+
if (SPACE_RE.test(c)) return;
|
|
1102
|
+
const t = top();
|
|
1103
|
+
switch (c) {
|
|
1104
|
+
case '(':
|
|
1105
|
+
case '[': {
|
|
1106
|
+
const f = frame('expr', c === '(' ? ')' : ']', i);
|
|
1107
|
+
f.header = c === '(' && afterHeaderKeyword;
|
|
1108
|
+
endPunct(c, true);
|
|
1109
|
+
const result = knownParen(f);
|
|
1110
|
+
if (result === undefined) open(f);
|
|
1111
|
+
else parenTo = { frame: f, result };
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
case '{':
|
|
1115
|
+
openBrace();
|
|
1116
|
+
break;
|
|
1117
|
+
case ')':
|
|
1118
|
+
case ']':
|
|
1119
|
+
case '}':
|
|
1120
|
+
close(c);
|
|
1121
|
+
break;
|
|
1122
|
+
case '.':
|
|
1123
|
+
// Property access, unless it is the spread `...`
|
|
1124
|
+
dots = lastPunct === '.' ? dots + 1 : 1;
|
|
1125
|
+
endPunct(c, dots === 3);
|
|
1126
|
+
break;
|
|
1127
|
+
case ';':
|
|
1128
|
+
endPunct(c, true);
|
|
1129
|
+
stmtNext = t.kind === 'block';
|
|
1130
|
+
keyNext = t.kind === 'class';
|
|
1131
|
+
break;
|
|
1132
|
+
case ',':
|
|
1133
|
+
endPunct(c, true);
|
|
1134
|
+
keyNext = t.kind === 'object';
|
|
1135
|
+
break;
|
|
1136
|
+
case '?':
|
|
1137
|
+
endPunct(c, true);
|
|
1138
|
+
t.ternary++;
|
|
1139
|
+
break;
|
|
1140
|
+
case ':':
|
|
1141
|
+
endPunct(c, true);
|
|
1142
|
+
// Not a conditional's `:`: an object literal value, or a statement
|
|
1143
|
+
// after a `case`, `default` or label
|
|
1144
|
+
if (t.ternary > 0) t.ternary--;
|
|
1145
|
+
else stmtNext = t.kind === 'block';
|
|
1146
|
+
break;
|
|
1147
|
+
case '#':
|
|
1148
|
+
// A private name: `this.#_x`
|
|
1149
|
+
endPunct(c, false);
|
|
1150
|
+
afterDot = true;
|
|
1151
|
+
break;
|
|
1152
|
+
case '*': {
|
|
1153
|
+
// A generator method: `{ *_gen() {} }`
|
|
1154
|
+
const key = keyNext;
|
|
1155
|
+
endPunct(c, true);
|
|
1156
|
+
keyNext = key;
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
case '>': {
|
|
1160
|
+
// `=>`: the body may be a block
|
|
1161
|
+
const arrow = lastPunct === '=';
|
|
1162
|
+
endPunct(c, true);
|
|
1163
|
+
arrowBody = arrow;
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
default:
|
|
1167
|
+
endPunct(c, true);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
while (i < src.length) {
|
|
1172
|
+
const ch = src.charAt(i);
|
|
1173
|
+
|
|
1174
|
+
// At the top level just past a `}`: go on as a scan that passed here in
|
|
1175
|
+
// the same state did
|
|
1176
|
+
const checkpoints = strict?.checkpoints;
|
|
1177
|
+
const key = checkpoints && checkpointKey();
|
|
1178
|
+
if (key !== undefined) {
|
|
1179
|
+
const known = checkpoints!.get(key);
|
|
1180
|
+
if (known === undefined) {
|
|
1181
|
+
strict!.passed.push(key);
|
|
1182
|
+
} else if (frames.length > 1 && known < 0) {
|
|
1183
|
+
// The frames open here may close before the error or the end of the
|
|
1184
|
+
// source, so their results are not known: none is recorded
|
|
1185
|
+
strict!.malformed = known === MALFORMED;
|
|
1186
|
+
return src.length;
|
|
1187
|
+
} else if (known === MALFORMED) {
|
|
1188
|
+
return malformed();
|
|
1189
|
+
} else if (known === UNCLOSED) {
|
|
1190
|
+
i = src.length;
|
|
1191
|
+
break;
|
|
1192
|
+
} else {
|
|
1193
|
+
strict!.stopped = true;
|
|
1194
|
+
return known;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Template literal text: escapes, the closing backtick, interpolations
|
|
1199
|
+
if (top().kind === 'template') {
|
|
1200
|
+
if (ch === '\\') {
|
|
1201
|
+
literal(src.slice(i, i + 2), i);
|
|
1202
|
+
i += 2;
|
|
1203
|
+
} else if (ch === '`') {
|
|
1204
|
+
literal(ch, i);
|
|
1205
|
+
if (frames.length === 1) return i + 1; // the end of `lexTemplate`
|
|
1206
|
+
record(top(), i);
|
|
1207
|
+
i++;
|
|
1208
|
+
truncate(frames.length - 1);
|
|
1209
|
+
endOperand();
|
|
1210
|
+
} else if (ch === '$' && src.charAt(i + 1) === '{') {
|
|
1211
|
+
const f = frame('expr', '}', i);
|
|
1212
|
+
f.interpolation = true;
|
|
1213
|
+
const end = known(f);
|
|
1214
|
+
if (end === MALFORMED) return malformed();
|
|
1215
|
+
if (end === UNCLOSED) {
|
|
1216
|
+
i = src.length;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
if (end !== undefined) {
|
|
1220
|
+
// An interpolation an earlier scan lexed: on with the text after it
|
|
1221
|
+
i = end + 1;
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
literal('${', i);
|
|
1225
|
+
i += 2;
|
|
1226
|
+
nesting++;
|
|
1227
|
+
endPunct('{', true);
|
|
1228
|
+
open(f);
|
|
1229
|
+
} else {
|
|
1230
|
+
literal(ch, i);
|
|
1231
|
+
i++;
|
|
1232
|
+
}
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// String literal — skip entirely
|
|
1237
|
+
if (ch === '"' || ch === "'") {
|
|
1238
|
+
if (strict && i > start && WORD_CHAR_RE.test(src.charAt(i - 1))) {
|
|
1239
|
+
if (!stringMayFollowWord()) return malformed();
|
|
1240
|
+
}
|
|
1241
|
+
endWord();
|
|
1242
|
+
const { end, closed } = scanStringLiteral(src, i);
|
|
1243
|
+
if (strict && !closed) return malformed();
|
|
1244
|
+
literalSpan(i, end);
|
|
1245
|
+
i = end;
|
|
1246
|
+
endOperand();
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (ch === '`') {
|
|
1251
|
+
endWord();
|
|
1252
|
+
const f = frame('template', '`', i);
|
|
1253
|
+
const end = known(f);
|
|
1254
|
+
if (end === MALFORMED) return malformed();
|
|
1255
|
+
if (end === UNCLOSED) {
|
|
1256
|
+
i = src.length;
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
if (end !== undefined) {
|
|
1260
|
+
// A template literal an earlier scan lexed: on after it
|
|
1261
|
+
i = end + 1;
|
|
1262
|
+
endOperand();
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
literal(ch, i);
|
|
1266
|
+
push(f);
|
|
1267
|
+
i++;
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (ch === '{' && strict?.stop?.(i)) {
|
|
1272
|
+
recordOpenParens(STOPPED - i);
|
|
1273
|
+
strict.stopped = true;
|
|
1274
|
+
return i;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// End of a template literal interpolation: the innermost `}` closer
|
|
1278
|
+
if (ch === '}') {
|
|
1279
|
+
const k = innermost('}');
|
|
1280
|
+
lookedFor('}', k);
|
|
1281
|
+
if (frames[k]!.interpolation) {
|
|
1282
|
+
endWord();
|
|
1283
|
+
record(frames[k]!, i);
|
|
1284
|
+
truncate(k);
|
|
1285
|
+
nesting--;
|
|
1286
|
+
literal(ch, i);
|
|
1287
|
+
i++;
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (ch === '/') {
|
|
1293
|
+
endWord();
|
|
1294
|
+
const next = src.charAt(i + 1);
|
|
1295
|
+
// Comment — skip entirely; it is not a token
|
|
1296
|
+
if (next === '/' || next === '*') {
|
|
1297
|
+
let end = findCommentEnd(src, i, strict?.cache);
|
|
1298
|
+
if (end < 0) {
|
|
1299
|
+
if (strict) return malformed();
|
|
1300
|
+
end = src.length;
|
|
1301
|
+
}
|
|
1302
|
+
literalSpan(i, end);
|
|
1303
|
+
if (next === '*' && lineBreakIn(src, i, end, strict?.cache)) {
|
|
1304
|
+
lineBreakSeen();
|
|
1305
|
+
}
|
|
1306
|
+
i = end;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
// Regex literal — only where an operand is expected
|
|
1310
|
+
if (operandNext) {
|
|
1311
|
+
const { end, closed } = scanRegex(src, i, strict?.cache);
|
|
1312
|
+
if (strict && !closed) return malformed();
|
|
1313
|
+
literalSpan(i, end);
|
|
1314
|
+
i = end;
|
|
1315
|
+
endOperand();
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// In a class body, a line break after a complete member starts the next
|
|
1321
|
+
// one (ASI): `_x = 1⏎_y = 2`, but `_x = a⏎instanceof B` continues it.
|
|
1322
|
+
if (!word && lineBreak && !operandNext && top().kind === 'class') {
|
|
1323
|
+
IDENT_RE.lastIndex = i;
|
|
1324
|
+
const next = IDENT_RE.exec(src)?.[0];
|
|
1325
|
+
if (next && next !== 'in' && next !== 'instanceof') keyNext = true;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// `$name`, `_name` or `@name` reference where an identifier starts (`@`
|
|
1329
|
+
// is no identifier character, so `typeof@x` holds one). The whole
|
|
1330
|
+
// identifier must be the sigil and a name: `$a$b` and `$café` are
|
|
1331
|
+
// identifiers of their own.
|
|
1332
|
+
if (ch === '@' || ((ch === '$' || ch === '_') && !word)) {
|
|
1333
|
+
endWord();
|
|
1334
|
+
IDENT_RE.lastIndex = i + 1;
|
|
1335
|
+
const name = IDENT_RE.exec(src)?.[0] ?? '';
|
|
1336
|
+
if (VAR_NAME_RE.test(name) && !isPropertyName(ch, i + 1 + name.length)) {
|
|
1337
|
+
handlers.variable?.(ch, name, i, nesting);
|
|
1338
|
+
i += 1 + name.length;
|
|
1339
|
+
endOperand();
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Transient reference — where an operand is expected, or as the target
|
|
1345
|
+
// of an assignment starting a line: `$x = 5\n%a = 1` would otherwise be
|
|
1346
|
+
// the invalid assignment `5 % a = 1`.
|
|
1347
|
+
if (ch === '%') {
|
|
1348
|
+
endWord();
|
|
1349
|
+
TRANS_NAME_RE.lastIndex = i + 1;
|
|
1350
|
+
const name = TRANS_NAME_RE.exec(src)?.[0];
|
|
1351
|
+
if (name) {
|
|
1352
|
+
const end = i + 1 + name.length;
|
|
1353
|
+
if (
|
|
1354
|
+
operandNext ||
|
|
1355
|
+
(lineBreak && ctx.lookahead && assignmentFollows(src, end, ctx))
|
|
1356
|
+
) {
|
|
1357
|
+
handlers.variable?.('%', name, i, nesting);
|
|
1358
|
+
i = end;
|
|
1359
|
+
endOperand();
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Increment/decrement: postfix after an operand on the same line (no line
|
|
1366
|
+
// break may precede postfix `++`), prefix otherwise.
|
|
1367
|
+
if ((ch === '+' || ch === '-') && src.charAt(i + 1) === ch) {
|
|
1368
|
+
endWord();
|
|
1369
|
+
const postfix = !operandNext && !lineBreak;
|
|
1370
|
+
code(ch, i);
|
|
1371
|
+
code(ch, i + 1);
|
|
1372
|
+
i += 2;
|
|
1373
|
+
endPunct(ch, !postfix);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// `??` and optional chaining `?.` (but `a?.5:1` is a conditional)
|
|
1378
|
+
if (ch === '?') {
|
|
1379
|
+
const next = src.charAt(i + 1);
|
|
1380
|
+
if (next === '?' || (next === '.' && !/\d/.test(src.charAt(i + 2)))) {
|
|
1381
|
+
endWord();
|
|
1382
|
+
code(ch, i);
|
|
1383
|
+
code(next, i + 1);
|
|
1384
|
+
i += 2;
|
|
1385
|
+
dots = 1;
|
|
1386
|
+
endPunct(next, next === '?');
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// End of an `outer` square bracket
|
|
1392
|
+
if (ch === outer.closer && frames.length === 1) break;
|
|
1393
|
+
|
|
1394
|
+
// Regular code character
|
|
1395
|
+
trackCode(ch);
|
|
1396
|
+
if (skipTo) {
|
|
1397
|
+
// A `{…}` frame an earlier scan lexed: continue after it
|
|
1398
|
+
const { frame: skipped, end } = skipTo;
|
|
1399
|
+
skipTo = undefined;
|
|
1400
|
+
if (end === MALFORMED) return malformed();
|
|
1401
|
+
if (end === UNCLOSED) {
|
|
1402
|
+
i = src.length;
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
i = end;
|
|
1406
|
+
afterBrace(skipped);
|
|
1407
|
+
i++;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (parenTo) {
|
|
1411
|
+
// A `(…)` or `[…]` frame an earlier scan lexed: continue after it
|
|
1412
|
+
const { frame: skipped, result } = parenTo;
|
|
1413
|
+
parenTo = undefined;
|
|
1414
|
+
if (result === MALFORMED) return malformed();
|
|
1415
|
+
if (result === UNCLOSED) {
|
|
1416
|
+
i = src.length;
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
if (result <= STOPPED) {
|
|
1420
|
+
recordOpenParens(result);
|
|
1421
|
+
strict!.stopped = true;
|
|
1422
|
+
return STOPPED - result;
|
|
1423
|
+
}
|
|
1424
|
+
i = Math.floor(result / 2);
|
|
1425
|
+
if (result % 2 === 1) pendingBody = undefined;
|
|
1426
|
+
if (skipped.closer === ')') {
|
|
1427
|
+
endPunct(')', skipped.header);
|
|
1428
|
+
stmtNext = skipped.header;
|
|
1429
|
+
} else {
|
|
1430
|
+
endPunct(']', false);
|
|
1431
|
+
}
|
|
1432
|
+
i++;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
code(ch, i);
|
|
1436
|
+
i++;
|
|
1437
|
+
}
|
|
1438
|
+
if (i >= src.length) {
|
|
1439
|
+
recordOpen(UNCLOSED);
|
|
1440
|
+
recordOpenParens(UNCLOSED);
|
|
1441
|
+
}
|
|
1442
|
+
if (!ctx.lookahead) {
|
|
1443
|
+
// Brackets left open here close at `i`: the end of `outer` or of `src`
|
|
1444
|
+
for (const f of frames) if (f.closer === ']') ctx.brackets.set(f.open, i);
|
|
1445
|
+
}
|
|
1446
|
+
return Math.min(i, src.length);
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Is the `$`/`_` word ending at `end` a property name: after `.`, a key
|
|
1450
|
+
* in an object literal (`{ _id: 1 }`, `{ _m() {} }`) or a member name in
|
|
1451
|
+
* a class body?
|
|
1452
|
+
*/
|
|
1453
|
+
function isPropertyName(sigil: string, end: number): boolean {
|
|
1454
|
+
if (afterDot) return true;
|
|
1455
|
+
if (!keyNext || sigil === '@') return false;
|
|
1456
|
+
if (top().kind === 'class') return true;
|
|
1457
|
+
const next = src.charAt(skipTrivia(src, end));
|
|
1458
|
+
return next === ':' || next === '(';
|
|
1459
|
+
}
|
|
1460
|
+
}
|