@rohal12/spindle 0.51.4 → 0.52.1
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 +4512 -1702
- package/dist/pkg/macro-registry.json +7 -7
- package/dist/pkg/story-variables.js +1636 -177
- package/package.json +5 -2
- package/src/action-registry.ts +70 -18
- package/src/automation/runner.ts +2 -1
- package/src/class-registry.ts +214 -103
- 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 +5 -31
- package/src/components/macros/Checkbox.tsx +7 -4
- package/src/components/macros/Computed.tsx +28 -14
- package/src/components/macros/For.tsx +29 -3
- package/src/components/macros/If.tsx +8 -0
- package/src/components/macros/Include.tsx +7 -6
- package/src/components/macros/MacroError.tsx +2 -1
- package/src/components/macros/MacroLink.tsx +12 -45
- package/src/components/macros/Meter.tsx +11 -3
- 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 +5 -2
- package/src/components/macros/SaveManager.tsx +25 -8
- package/src/components/macros/Set.tsx +5 -4
- 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 +31 -10
- package/src/components/macros/VarDisplay.tsx +21 -4
- package/src/components/macros/Widget.tsx +20 -1
- package/src/components/macros/WidgetInvocation.tsx +17 -75
- package/src/components/macros/arg-utils.ts +107 -1
- package/src/components/macros/detached-body.tsx +68 -0
- package/src/components/macros/option-utils.ts +3 -2
- package/src/define-macro.ts +32 -5
- package/src/execute-mutation.ts +271 -68
- package/src/expression.ts +91 -59
- package/src/hooks/use-action.ts +24 -3
- package/src/hooks/use-interpolate.ts +36 -5
- package/src/index.tsx +12 -2
- package/src/interpolation.ts +394 -96
- package/src/js-lexer.ts +1231 -97
- package/src/markup/ast.ts +7 -2
- package/src/markup/code-attributes.ts +64 -0
- package/src/markup/markdown.ts +188 -9
- package/src/markup/render.tsx +430 -49
- package/src/markup/tokenizer.ts +578 -110
- package/src/prng.ts +41 -8
- package/src/registry.ts +35 -0
- package/src/saves/save-manager.ts +346 -160
- package/src/saves/storage.ts +16 -7
- package/src/saves/types.ts +2 -1
- package/src/settings.ts +12 -9
- package/src/store.ts +640 -186
- package/src/story-api.ts +38 -73
- package/src/story-init.ts +1 -1
- package/src/story-variables.ts +98 -102
- package/src/triggers.ts +10 -12
- package/src/utils/counts.ts +45 -0
- 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 +99 -14
- package/src/utils/stable-key.ts +14 -9
- package/src/widgets/widget-registry.ts +9 -0
- package/types/index.d.ts +43 -7
- package/types/tooling.d.ts +1 -0
|
@@ -1,3 +1,1126 @@
|
|
|
1
|
+
//#region src/js-lexer.ts
|
|
2
|
+
/** Transient name after `%`: an identifier, so `%3` is never a reference. */
|
|
3
|
+
var TRANS_NAME_RE = /[A-Za-z_]\w*/y;
|
|
4
|
+
/** An assignment operator: `=`, `+=`, `??=`, … but not `==` or `=>`. */
|
|
5
|
+
var ASSIGN_OP_RE = /(?:\*\*|<<|>>>?|&&|\|\||\?\?|[-+*/%&|^])?=(?![=>])/y;
|
|
6
|
+
/** Flags after the closing `/` of a regex literal. */
|
|
7
|
+
var REGEX_FLAGS_RE = /\w*/y;
|
|
8
|
+
/**
|
|
9
|
+
* Characters of identifiers (with the `\u…` escapes they may contain) and
|
|
10
|
+
* numbers. Surrogates stand for the astral identifier characters they encode.
|
|
11
|
+
*/
|
|
12
|
+
var WORD_CHAR_RE = /[\p{ID_Continue}$\u200c\u200d\\\ud800-\udfff]/u;
|
|
13
|
+
/** An identifier, or the rest of one after a `$`, `_` or `@` sigil. */
|
|
14
|
+
var IDENT_RE = /[\p{ID_Continue}$\u200c\u200d]*/uy;
|
|
15
|
+
/** A sigil variable name: the whole identifier after the sigil. */
|
|
16
|
+
var VAR_NAME_RE = /^\w+$/;
|
|
17
|
+
/** What may start a property name after a `get`, `set`, … modifier. */
|
|
18
|
+
var KEY_START_RE = /[\p{ID_Continue}$\\"'[*#]/u;
|
|
19
|
+
var SPACE_RE = /\s/;
|
|
20
|
+
var LINE_TERMINATOR_RE = /[\n\r\u2028\u2029]/;
|
|
21
|
+
var LINE_TERMINATOR_G = /[\n\r\u2028\u2029]/g;
|
|
22
|
+
/**
|
|
23
|
+
* Keywords followed by an operand rather than an operator (a declaration's
|
|
24
|
+
* binding counts as one: `const of of xs`, `let { _a: x } = o`).
|
|
25
|
+
*/
|
|
26
|
+
var OPERAND_KEYWORDS = new Set([
|
|
27
|
+
"await",
|
|
28
|
+
"case",
|
|
29
|
+
"const",
|
|
30
|
+
"delete",
|
|
31
|
+
"do",
|
|
32
|
+
"else",
|
|
33
|
+
"in",
|
|
34
|
+
"instanceof",
|
|
35
|
+
"let",
|
|
36
|
+
"new",
|
|
37
|
+
"return",
|
|
38
|
+
"throw",
|
|
39
|
+
"typeof",
|
|
40
|
+
"var",
|
|
41
|
+
"void",
|
|
42
|
+
"yield"
|
|
43
|
+
]);
|
|
44
|
+
/** Keywords whose parenthesised header is followed by a statement. */
|
|
45
|
+
var HEADER_KEYWORDS = new Set([
|
|
46
|
+
"for",
|
|
47
|
+
"if",
|
|
48
|
+
"while",
|
|
49
|
+
"with"
|
|
50
|
+
]);
|
|
51
|
+
/** Keywords that a line break ends the statement after. */
|
|
52
|
+
var RESTRICTED_KEYWORDS = new Set([
|
|
53
|
+
"break",
|
|
54
|
+
"continue",
|
|
55
|
+
"return"
|
|
56
|
+
]);
|
|
57
|
+
/** Words that may precede a property name in an object literal or class. */
|
|
58
|
+
var MODIFIERS = new Set([
|
|
59
|
+
"async",
|
|
60
|
+
"get",
|
|
61
|
+
"set",
|
|
62
|
+
"static"
|
|
63
|
+
]);
|
|
64
|
+
/**
|
|
65
|
+
* Scan the `"…"` or `'…'` string literal opening at `start`. `end` is the
|
|
66
|
+
* index just past its closing quote, or `src.length` when it is unterminated
|
|
67
|
+
* (`closed` false). A backslash escapes the character after it, so a quote
|
|
68
|
+
* after an even run of backslashes closes the string and one after an odd
|
|
69
|
+
* run does not.
|
|
70
|
+
*/
|
|
71
|
+
function scanStringLiteral(src, start) {
|
|
72
|
+
const quote = src.charAt(start);
|
|
73
|
+
let i = start + 1;
|
|
74
|
+
while (i < src.length) {
|
|
75
|
+
const c = src.charAt(i);
|
|
76
|
+
if (c === "\\") i += 2;
|
|
77
|
+
else if (c === quote) return {
|
|
78
|
+
end: i + 1,
|
|
79
|
+
closed: true
|
|
80
|
+
};
|
|
81
|
+
else i++;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
end: src.length,
|
|
85
|
+
closed: false
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Scan the regex literal (with flags) opening at `start`. `end` is the index
|
|
90
|
+
* just past it; an unterminated one (`closed` false) ends at the line break
|
|
91
|
+
* (escaped or not) or the end of the source.
|
|
92
|
+
*/
|
|
93
|
+
function scanRegex(src, start, cache) {
|
|
94
|
+
const pending = [];
|
|
95
|
+
const done = (end, closed) => {
|
|
96
|
+
if (cache) for (const at of pending) cache.regexes.set(at, end * 2 + +closed);
|
|
97
|
+
return {
|
|
98
|
+
end,
|
|
99
|
+
closed
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
let i = start + 1;
|
|
103
|
+
while (i < src.length) {
|
|
104
|
+
const c = src.charAt(i);
|
|
105
|
+
if (c === "\\") {
|
|
106
|
+
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return done(i + 1, false);
|
|
107
|
+
i += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (LINE_TERMINATOR_RE.test(c)) return done(i, false);
|
|
111
|
+
if (c === "[") {
|
|
112
|
+
i = scanRegexClass(src, i, cache);
|
|
113
|
+
if (src.charAt(i) !== "]") return done(i, false);
|
|
114
|
+
i++;
|
|
115
|
+
const known = cache?.regexes.get(i);
|
|
116
|
+
if (known !== void 0) return done(Math.floor(known / 2), known % 2 === 1);
|
|
117
|
+
pending.push(i);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (c === "/") {
|
|
121
|
+
REGEX_FLAGS_RE.lastIndex = i + 1;
|
|
122
|
+
const flags = REGEX_FLAGS_RE.exec(src)?.[0].length ?? 0;
|
|
123
|
+
return done(i + 1 + flags, true);
|
|
124
|
+
}
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
return done(src.length, false);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Index of the `]` closing the regex character class opening at `open`, or
|
|
131
|
+
* of the line break (or the end of the source) that leaves it unterminated.
|
|
132
|
+
* Every `[` in a class reads the rest of it the same way, so the result is
|
|
133
|
+
* recorded for them too: scans of `/[/[/[…` from each `/` share one pass.
|
|
134
|
+
*/
|
|
135
|
+
function scanRegexClass(src, open, cache) {
|
|
136
|
+
const known = cache?.classes.get(open);
|
|
137
|
+
if (known !== void 0) return known;
|
|
138
|
+
const opens = [open];
|
|
139
|
+
let i = open + 1;
|
|
140
|
+
while (i < src.length) {
|
|
141
|
+
const c = src.charAt(i);
|
|
142
|
+
if (c === "\\") {
|
|
143
|
+
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) {
|
|
144
|
+
i++;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
i += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (c === "]" || LINE_TERMINATOR_RE.test(c)) break;
|
|
151
|
+
if (c === "[" && cache) opens.push(i);
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
i = Math.min(i, src.length);
|
|
155
|
+
if (cache) for (const o of opens) cache.classes.set(o, i);
|
|
156
|
+
return i;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The next index from `from` on where `find` matches (-1 for none), for
|
|
160
|
+
* searches from increasing positions: a search from within the stretch the
|
|
161
|
+
* last one covered has the same answer.
|
|
162
|
+
*/
|
|
163
|
+
function nextMatch(memo, from, find) {
|
|
164
|
+
if (memo && from >= memo.from && (memo.at < 0 || from <= memo.at)) return memo.at;
|
|
165
|
+
const at = find(from);
|
|
166
|
+
if (memo) {
|
|
167
|
+
memo.from = from;
|
|
168
|
+
memo.at = at;
|
|
169
|
+
}
|
|
170
|
+
return at;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Index just past the comment opening at `start` (`//` or `/*`), or -1 for
|
|
174
|
+
* an unterminated `/*` comment.
|
|
175
|
+
*/
|
|
176
|
+
function findCommentEnd(src, start, cache) {
|
|
177
|
+
if (src.charAt(start + 1) === "/") {
|
|
178
|
+
const end = nextLineBreak(src, start, cache);
|
|
179
|
+
return end < 0 ? src.length : end;
|
|
180
|
+
}
|
|
181
|
+
const end = nextMatch(cache?.commentClose, start + 2, (from) => src.indexOf("*/", from));
|
|
182
|
+
return end < 0 ? -1 : end + 2;
|
|
183
|
+
}
|
|
184
|
+
/** Next line break from `from` on (-1 for none). */
|
|
185
|
+
function nextLineBreak(src, from, cache) {
|
|
186
|
+
return nextMatch(cache?.lineEnd, from, (at) => {
|
|
187
|
+
LINE_TERMINATOR_G.lastIndex = at;
|
|
188
|
+
return LINE_TERMINATOR_G.exec(src)?.index ?? -1;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** Is there a line break between `from` and `to`? */
|
|
192
|
+
function lineBreakIn(src, from, to, cache) {
|
|
193
|
+
if (!cache) return LINE_TERMINATOR_RE.test(src.slice(from, to));
|
|
194
|
+
const at = nextLineBreak(src, from, cache);
|
|
195
|
+
return at >= 0 && at < to;
|
|
196
|
+
}
|
|
197
|
+
/** Index just past the comment opening at `start` (`//` or `/*`). */
|
|
198
|
+
function skipComment(src, start) {
|
|
199
|
+
const end = findCommentEnd(src, start);
|
|
200
|
+
return end < 0 ? src.length : end;
|
|
201
|
+
}
|
|
202
|
+
/** Index of the first character from `i` on that is no space or comment. */
|
|
203
|
+
function skipTrivia(src, i) {
|
|
204
|
+
while (i < src.length) {
|
|
205
|
+
const c = src.charAt(i);
|
|
206
|
+
if (SPACE_RE.test(c)) i++;
|
|
207
|
+
else if (c === "/" && "/*".includes(src.charAt(i + 1))) i = skipComment(src, i);
|
|
208
|
+
else break;
|
|
209
|
+
}
|
|
210
|
+
return i;
|
|
211
|
+
}
|
|
212
|
+
/** Characters that a scan checkpoint follows (`checkpointKey`). */
|
|
213
|
+
var CHECKPOINT_AFTER = new Set([
|
|
214
|
+
"}",
|
|
215
|
+
"\"",
|
|
216
|
+
"'",
|
|
217
|
+
"`",
|
|
218
|
+
"/",
|
|
219
|
+
"\n",
|
|
220
|
+
"\r",
|
|
221
|
+
"\u2028",
|
|
222
|
+
"\u2029",
|
|
223
|
+
" ",
|
|
224
|
+
" "
|
|
225
|
+
]);
|
|
226
|
+
/**
|
|
227
|
+
* How far into a scan its results start to be shared within brackets
|
|
228
|
+
* (`checkpointKey`, `knownParen`). Most scans end sooner, so they don't pay
|
|
229
|
+
* for recording results no other scan will use; a long one pays this much
|
|
230
|
+
* before it can use what earlier scans recorded, which keeps scans from
|
|
231
|
+
* many starts about linear.
|
|
232
|
+
*/
|
|
233
|
+
var SHARE_AFTER = 256;
|
|
234
|
+
/** A frame result: it is still open at the end of the source. */
|
|
235
|
+
var UNCLOSED = -1;
|
|
236
|
+
/** A frame result: a lexical error inside it ends the scan. */
|
|
237
|
+
var MALFORMED = -2;
|
|
238
|
+
/** A frame result: the scan stops at index `s` inside it (`STOPPED - s`). */
|
|
239
|
+
var STOPPED = -3;
|
|
240
|
+
/** The closers whose search for their frame a frame result may depend on. */
|
|
241
|
+
var CLOSERS = [
|
|
242
|
+
")",
|
|
243
|
+
"]",
|
|
244
|
+
"}"
|
|
245
|
+
];
|
|
246
|
+
var FRAME_KINDS = [
|
|
247
|
+
"block",
|
|
248
|
+
"object",
|
|
249
|
+
"class",
|
|
250
|
+
"template"
|
|
251
|
+
];
|
|
252
|
+
/**
|
|
253
|
+
* Key of a frame result, for the frames whose code lexes the same whatever
|
|
254
|
+
* surrounds them: braces (a block, an object literal, a class body), a
|
|
255
|
+
* template literal and its interpolations. Undefined for parentheses and
|
|
256
|
+
* square brackets, which a stray closer inside may close.
|
|
257
|
+
*/
|
|
258
|
+
function frameKey(f) {
|
|
259
|
+
const kind = f.interpolation ? 4 : FRAME_KINDS.indexOf(f.kind);
|
|
260
|
+
return kind < 0 ? void 0 : f.open * 5 + kind;
|
|
261
|
+
}
|
|
262
|
+
function createJsScanCache() {
|
|
263
|
+
return {
|
|
264
|
+
checkpoints: /* @__PURE__ */ new Map(),
|
|
265
|
+
stacks: /* @__PURE__ */ new Map(),
|
|
266
|
+
parens: /* @__PURE__ */ new Map(),
|
|
267
|
+
shareAfter: SHARE_AFTER,
|
|
268
|
+
braces: /* @__PURE__ */ new Map(),
|
|
269
|
+
brackets: /* @__PURE__ */ new Map(),
|
|
270
|
+
classes: /* @__PURE__ */ new Map(),
|
|
271
|
+
regexes: /* @__PURE__ */ new Map(),
|
|
272
|
+
lineEnd: {
|
|
273
|
+
from: Infinity,
|
|
274
|
+
at: -1
|
|
275
|
+
},
|
|
276
|
+
commentClose: {
|
|
277
|
+
from: Infinity,
|
|
278
|
+
at: -1
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
var newContext = () => ({
|
|
283
|
+
lookahead: true,
|
|
284
|
+
brackets: /* @__PURE__ */ new Map()
|
|
285
|
+
});
|
|
286
|
+
/** Index of the `]` matching the `[` at `open`, or `src.length`. */
|
|
287
|
+
function matchBracket(src, open, ctx) {
|
|
288
|
+
let close = ctx.brackets.get(open);
|
|
289
|
+
if (close === void 0) {
|
|
290
|
+
const ahead = {
|
|
291
|
+
lookahead: false,
|
|
292
|
+
brackets: ctx.brackets
|
|
293
|
+
};
|
|
294
|
+
close = scan(src, {}, open + 1, 0, frame("expr", "]", open), ahead);
|
|
295
|
+
ctx.brackets.set(open, close);
|
|
296
|
+
}
|
|
297
|
+
return close;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Is the code at `i`, just past a `%name`, the rest of an assignment target
|
|
301
|
+
* and its operator: ` = 1`, `.a[b] += 2`, but not `== 1`, `=> 1` or `% 2`?
|
|
302
|
+
*/
|
|
303
|
+
function assignmentFollows(src, i, ctx) {
|
|
304
|
+
for (;;) {
|
|
305
|
+
i = skipTrivia(src, i);
|
|
306
|
+
const c = src.charAt(i);
|
|
307
|
+
if (c === "." && src.charAt(i + 1) !== ".") {
|
|
308
|
+
IDENT_RE.lastIndex = skipTrivia(src, i + 1);
|
|
309
|
+
if (!IDENT_RE.exec(src)?.[0]) return false;
|
|
310
|
+
i = IDENT_RE.lastIndex;
|
|
311
|
+
} else if (c === "[") {
|
|
312
|
+
i = matchBracket(src, i, ctx);
|
|
313
|
+
if (i >= src.length) return false;
|
|
314
|
+
i++;
|
|
315
|
+
} else break;
|
|
316
|
+
}
|
|
317
|
+
ASSIGN_OP_RE.lastIndex = i;
|
|
318
|
+
return ASSIGN_OP_RE.test(src);
|
|
319
|
+
}
|
|
320
|
+
function frame(kind, closer, open = -1) {
|
|
321
|
+
return {
|
|
322
|
+
kind,
|
|
323
|
+
closer,
|
|
324
|
+
open,
|
|
325
|
+
header: false,
|
|
326
|
+
operand: false,
|
|
327
|
+
ternary: 0,
|
|
328
|
+
interpolation: false
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Walk `src`, reporting code characters, literal text and variable
|
|
333
|
+
* references to `handlers` in source order. Every character is reported
|
|
334
|
+
* exactly once (a variable reference covers its sigil and name). Returns
|
|
335
|
+
* `src.length`.
|
|
336
|
+
*/
|
|
337
|
+
function lexJs(src, handlers, goal = "expression") {
|
|
338
|
+
return scan(src, handlers, 0, 0, frame(goal === "statements" ? "block" : "expr", ""), newContext());
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Find where the code starting at `start` ends, lexing it as JavaScript:
|
|
342
|
+
* braces, quotes and backticks inside string, template and regex literals and
|
|
343
|
+
* comments don't count.
|
|
344
|
+
*
|
|
345
|
+
* Without `stop`, the code ends at the first `}` in code outside the
|
|
346
|
+
* brackets it opened (the `}` closing a `{…}` around it); with `stop`, at
|
|
347
|
+
* the first `{` in code, at any depth, for which `stop` holds. Returns the
|
|
348
|
+
* index of that `}` or `{`.
|
|
349
|
+
*
|
|
350
|
+
* Returns -1 when there is no such end, or when the code before it is not
|
|
351
|
+
* well-formed JavaScript as far as a lexer can tell: an unterminated string,
|
|
352
|
+
* regex literal or block comment, or a quote directly after an identifier or
|
|
353
|
+
* number (`don't`, but not `typeof'x'`). Callers fall back to a more lenient
|
|
354
|
+
* reading there, so text that only looks like code is not swallowed by an
|
|
355
|
+
* apostrophe or a stray quote.
|
|
356
|
+
*/
|
|
357
|
+
function findCodeEnd(src, start, { goal = "expression", stop, stopKey, cache } = {}) {
|
|
358
|
+
const strict = {
|
|
359
|
+
stop,
|
|
360
|
+
malformed: false,
|
|
361
|
+
stopped: false,
|
|
362
|
+
passed: []
|
|
363
|
+
};
|
|
364
|
+
strict.cache = cache;
|
|
365
|
+
if (cache && !stop) strict.braces = cache.braces;
|
|
366
|
+
if (cache && (!stop || stopKey !== void 0)) {
|
|
367
|
+
const kindKey = `${goal} ${stop ? `stop ${stopKey}` : "}"}`;
|
|
368
|
+
let checkpoints = cache.checkpoints.get(kindKey);
|
|
369
|
+
if (!checkpoints) cache.checkpoints.set(kindKey, checkpoints = /* @__PURE__ */ new Map());
|
|
370
|
+
strict.checkpoints = checkpoints;
|
|
371
|
+
let parens = cache.parens.get(kindKey);
|
|
372
|
+
if (!parens) cache.parens.set(kindKey, parens = /* @__PURE__ */ new Map());
|
|
373
|
+
strict.parens = parens;
|
|
374
|
+
}
|
|
375
|
+
const ctx = {
|
|
376
|
+
lookahead: true,
|
|
377
|
+
brackets: cache?.brackets ?? /* @__PURE__ */ new Map(),
|
|
378
|
+
strict
|
|
379
|
+
};
|
|
380
|
+
const end = scan(src, {}, start, 0, frame(goal === "statements" ? "block" : "expr", stop ? "" : "}", start - 1), ctx);
|
|
381
|
+
if (strict.checkpoints) {
|
|
382
|
+
const ended = stop ? strict.stopped : end < src.length;
|
|
383
|
+
const result = strict.malformed ? MALFORMED : ended ? end : UNCLOSED;
|
|
384
|
+
for (const at of strict.passed) strict.checkpoints.set(at, result);
|
|
385
|
+
}
|
|
386
|
+
if (strict.malformed) return -1;
|
|
387
|
+
if (stop) return strict.stopped ? end : -1;
|
|
388
|
+
return end < src.length ? end : -1;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Scan from `start` within `outer`. Returns `src.length`, or the index of
|
|
392
|
+
* the `]` closing an `outer` square bracket, or the index just past the
|
|
393
|
+
* backtick closing an `outer` template literal.
|
|
394
|
+
*
|
|
395
|
+
* Nested brackets, template literals and their interpolations are frames
|
|
396
|
+
* on a stack, not recursive calls: however deep the nesting, the scan
|
|
397
|
+
* cannot overflow the call stack.
|
|
398
|
+
*/
|
|
399
|
+
function scan(src, handlers, start, nesting, outer, ctx) {
|
|
400
|
+
const frames = [outer];
|
|
401
|
+
const top = () => frames[frames.length - 1];
|
|
402
|
+
/**
|
|
403
|
+
* Indices of the open frames closed by `)`, `]` and `}` (above `outer`),
|
|
404
|
+
* innermost last: the frame a closer closes is found without walking the
|
|
405
|
+
* stack.
|
|
406
|
+
*/
|
|
407
|
+
const closers = {
|
|
408
|
+
")": [],
|
|
409
|
+
"]": [],
|
|
410
|
+
"}": []
|
|
411
|
+
};
|
|
412
|
+
const innermost = (c) => {
|
|
413
|
+
const list = closers[c];
|
|
414
|
+
return list.length ? list[list.length - 1] : 0;
|
|
415
|
+
};
|
|
416
|
+
let i = start;
|
|
417
|
+
let operandNext = true;
|
|
418
|
+
let stmtNext = outer.kind === "block";
|
|
419
|
+
let arrowBody = false;
|
|
420
|
+
let keyNext = false;
|
|
421
|
+
let word = "";
|
|
422
|
+
let afterDot = false;
|
|
423
|
+
let dots = 0;
|
|
424
|
+
let afterHeaderKeyword = false;
|
|
425
|
+
let lastPunct = "";
|
|
426
|
+
let lastKeyword = "";
|
|
427
|
+
let lineBreak = false;
|
|
428
|
+
/** The next `{` at this depth opens a function or class body. */
|
|
429
|
+
let pendingBody;
|
|
430
|
+
const strict = ctx.strict;
|
|
431
|
+
const braces = strict?.braces;
|
|
432
|
+
const stacks = strict?.checkpoints && strict.cache?.stacks;
|
|
433
|
+
/**
|
|
434
|
+
* With checkpoints, per open frame the id of the stack up to it, found
|
|
435
|
+
* when a checkpoint needs it: the kind and flags of each frame, and the
|
|
436
|
+
* conditional-expression count of each but the innermost (which changes
|
|
437
|
+
* only while it is innermost, and is part of the checkpoint state). How a
|
|
438
|
+
* scan goes on depends on these, not on where the frames opened.
|
|
439
|
+
*/
|
|
440
|
+
const stackIds = stacks ? [0] : void 0;
|
|
441
|
+
/** The id of the stack of open frames (`stackIds`). */
|
|
442
|
+
function stackId() {
|
|
443
|
+
let k = frames.length - 1;
|
|
444
|
+
while (stackIds[k] === void 0) k--;
|
|
445
|
+
for (k++; k < frames.length; k++) {
|
|
446
|
+
const f = frames[k];
|
|
447
|
+
const key = `${stackIds[k - 1]} ${frames[k - 1].ternary} ${f.kind} ${f.closer} ${+f.header}${+f.operand}${+f.interpolation}`;
|
|
448
|
+
let id = stacks.get(key);
|
|
449
|
+
if (id === void 0) stacks.set(key, id = stacks.size + 1);
|
|
450
|
+
stackIds[k] = id;
|
|
451
|
+
}
|
|
452
|
+
return stackIds[frames.length - 1];
|
|
453
|
+
}
|
|
454
|
+
/** A `{…}` frame result found in `braces`, to skip to. */
|
|
455
|
+
let skipTo;
|
|
456
|
+
const parens = strict?.parens;
|
|
457
|
+
const shareAfter = strict?.cache?.shareAfter ?? SHARE_AFTER;
|
|
458
|
+
/**
|
|
459
|
+
* With `parens`, per open frame and per closer in `CLOSERS`: the lowest
|
|
460
|
+
* frame index the closer looked for its frame at, while this frame or one
|
|
461
|
+
* opened in it was innermost. Below a frame's own index, the code in it
|
|
462
|
+
* depended on what is around it. A frame's entry takes in those of the
|
|
463
|
+
* frames opened in it as they close.
|
|
464
|
+
*/
|
|
465
|
+
const lowest = parens ? [[
|
|
466
|
+
Infinity,
|
|
467
|
+
Infinity,
|
|
468
|
+
Infinity
|
|
469
|
+
]] : void 0;
|
|
470
|
+
/**
|
|
471
|
+
* How many function or class bodies to come the scan has started, and with
|
|
472
|
+
* `parens`, that count when each open frame opened. Code in a `(…)` or
|
|
473
|
+
* `[…]` that starts one replaces the one to come, and the new one can't
|
|
474
|
+
* follow once the frame is closed: none is to come then.
|
|
475
|
+
*/
|
|
476
|
+
let bodyStarts = 0;
|
|
477
|
+
const startsAt = parens ? [0] : void 0;
|
|
478
|
+
/** A `(…)` or `[…]` frame result found in `parens`, to skip to. */
|
|
479
|
+
let parenTo;
|
|
480
|
+
/** The closer `c` looked for its frame and found index `k`. */
|
|
481
|
+
function lookedFor(c, k) {
|
|
482
|
+
if (!lowest) return;
|
|
483
|
+
const entry = lowest[lowest.length - 1];
|
|
484
|
+
const n = CLOSERS.indexOf(c);
|
|
485
|
+
if (k < entry[n]) entry[n] = k;
|
|
486
|
+
}
|
|
487
|
+
/** Take the entry of the frame at index `k` into the one below it. */
|
|
488
|
+
function foldLowest(k) {
|
|
489
|
+
const from = lowest[k];
|
|
490
|
+
const into = lowest[k - 1];
|
|
491
|
+
for (let n = 0; n < 3; n++) if (from[n] < into[n]) into[n] = from[n];
|
|
492
|
+
}
|
|
493
|
+
/** Key of a `(…)` or `[…]` frame result, but for the closers it met. */
|
|
494
|
+
const parenKey = (f) => (f.open * 3 + (f.closer === "]" ? 2 : +f.header)) * 8;
|
|
495
|
+
/**
|
|
496
|
+
* Record how the frame at index `k` ends, if it is a `(…)` or `[…]`, its
|
|
497
|
+
* `lowest` entry complete: under the closers that looked below it, which
|
|
498
|
+
* found nothing to close (else the frame was closed with them).
|
|
499
|
+
*/
|
|
500
|
+
function recordParen(k, result) {
|
|
501
|
+
const f = frames[k];
|
|
502
|
+
if (k === 0 || f.closer !== ")" && f.closer !== "]") return;
|
|
503
|
+
if (f.open - start < shareAfter) return;
|
|
504
|
+
let met = 0;
|
|
505
|
+
for (let n = 0; n < 3; n++) if (lowest[k][n] < k) met |= 1 << n;
|
|
506
|
+
parens.set(parenKey(f) + met, result);
|
|
507
|
+
}
|
|
508
|
+
/** Record how the open `(…)` and `[…]` frames end: the scan ends in them. */
|
|
509
|
+
function recordOpenParens(result) {
|
|
510
|
+
if (!lowest) return;
|
|
511
|
+
for (let k = frames.length - 1; k > 0; k--) {
|
|
512
|
+
recordParen(k, result);
|
|
513
|
+
foldLowest(k);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* How an earlier scan found a `(…)` or `[…]` frame opening here to end,
|
|
518
|
+
* if it did: a result recorded under closers that find nothing to close
|
|
519
|
+
* around the frame here too. The frame is not open yet.
|
|
520
|
+
*/
|
|
521
|
+
function knownParen(f) {
|
|
522
|
+
if (!parens || f.open - start < shareAfter) return void 0;
|
|
523
|
+
const at = [
|
|
524
|
+
Math.max(innermost(")"), innermost("}")),
|
|
525
|
+
Math.max(innermost("]"), innermost("}")),
|
|
526
|
+
innermost("}")
|
|
527
|
+
];
|
|
528
|
+
let stray = 0;
|
|
529
|
+
for (let n = 0; n < 3; n++) {
|
|
530
|
+
const k = at[n];
|
|
531
|
+
if (k === 0 || n < 2 && frames[k].closer !== CLOSERS[n]) stray |= 1 << n;
|
|
532
|
+
}
|
|
533
|
+
const key = parenKey(f);
|
|
534
|
+
for (let met = stray;; met = met - 1 & stray) {
|
|
535
|
+
const result = parens.get(key + met);
|
|
536
|
+
if (result !== void 0) {
|
|
537
|
+
for (let n = 0; n < 3; n++) if (met & 1 << n) lookedFor(CLOSERS[n], at[n]);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
if (met === 0) return void 0;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/** Record how the open frames end, when the scan ends in them. */
|
|
544
|
+
function recordOpen(result) {
|
|
545
|
+
if (!braces) return;
|
|
546
|
+
for (let k = 1; k < frames.length; k++) record(frames[k], result);
|
|
547
|
+
}
|
|
548
|
+
/** Record how a frame ends. */
|
|
549
|
+
function record(f, result) {
|
|
550
|
+
const key = braces && frameKey(f);
|
|
551
|
+
if (key !== void 0) braces.set(key, result);
|
|
552
|
+
}
|
|
553
|
+
/** How an earlier scan found the frame `f` to end, if it did. */
|
|
554
|
+
const known = (f) => {
|
|
555
|
+
const key = braces && frameKey(f);
|
|
556
|
+
return key === void 0 ? void 0 : braces.get(key);
|
|
557
|
+
};
|
|
558
|
+
/** End a strict scan at a lexical error. */
|
|
559
|
+
const malformed = () => {
|
|
560
|
+
recordOpen(MALFORMED);
|
|
561
|
+
recordOpenParens(MALFORMED);
|
|
562
|
+
strict.malformed = true;
|
|
563
|
+
return src.length;
|
|
564
|
+
};
|
|
565
|
+
function push(f) {
|
|
566
|
+
stackIds?.push(void 0);
|
|
567
|
+
closers[f.closer]?.push(frames.length);
|
|
568
|
+
frames.push(f);
|
|
569
|
+
lowest?.push([
|
|
570
|
+
Infinity,
|
|
571
|
+
Infinity,
|
|
572
|
+
Infinity
|
|
573
|
+
]);
|
|
574
|
+
startsAt?.push(bodyStarts);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Just past a token or a space, line break or comment, but not within a
|
|
578
|
+
* word: the key of the position and the whole scan state there, the open
|
|
579
|
+
* frames included, which decide how the scan goes on (or undefined
|
|
580
|
+
* elsewhere). No word is being read there, and the character before is no
|
|
581
|
+
* word character, so a quote after it starts a string in any scan.
|
|
582
|
+
*
|
|
583
|
+
* Within brackets too, once the scan is long (`SHARE_AFTER`). Scans that
|
|
584
|
+
* never passed such a point in the same state each ran on to the end of
|
|
585
|
+
* the source, so many of them took quadratic time: inside an unclosed `(`
|
|
586
|
+
* in `{(}{(}{(…` (there was no point within brackets), or after the regex
|
|
587
|
+
* literals of `{a'</a </p>…` (there was no point after a space).
|
|
588
|
+
*/
|
|
589
|
+
function checkpointKey() {
|
|
590
|
+
const ternary = top().ternary;
|
|
591
|
+
if (ternary > 3 || !CHECKPOINT_AFTER.has(src.charAt(i - 1)) || frames.length > 1 && i - start < shareAfter) return;
|
|
592
|
+
let state = ternary;
|
|
593
|
+
if (operandNext) state |= 4;
|
|
594
|
+
if (stmtNext) state |= 8;
|
|
595
|
+
if (keyNext) state |= 16;
|
|
596
|
+
if (pendingBody) {
|
|
597
|
+
state |= pendingBody.frame.kind === "class" ? 32 : 64;
|
|
598
|
+
if (pendingBody.frame.operand) state |= 128;
|
|
599
|
+
}
|
|
600
|
+
if (arrowBody) state |= 256;
|
|
601
|
+
if (afterDot) state |= 512;
|
|
602
|
+
if (afterHeaderKeyword) state |= 1024;
|
|
603
|
+
if (lineBreak) state |= 2048;
|
|
604
|
+
if (RESTRICTED_KEYWORDS.has(lastKeyword)) state |= 4096;
|
|
605
|
+
if (lastPunct === "=") state |= 8192;
|
|
606
|
+
if (lastPunct === ".") state |= Math.min(dots, 4) << 14;
|
|
607
|
+
const at = i * (1 << 17) + state;
|
|
608
|
+
if (frames.length === 1) return at;
|
|
609
|
+
const body = pendingBody ? frames.length - pendingBody.depth : "";
|
|
610
|
+
return `${at} ${stackId()} ${body}`;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* May a string literal follow the word just read (`word`, or a variable
|
|
614
|
+
* reference) with nothing between? Only after a keyword taking an operand
|
|
615
|
+
* (`typeof'x'`, `case"a"`), `of` in a `for` header, or a modifier before a
|
|
616
|
+
* property name (`static'x'`, `get"y"() {}`); elsewhere the quote is an
|
|
617
|
+
* apostrophe (`don't`), and the text is no JavaScript.
|
|
618
|
+
*/
|
|
619
|
+
function stringMayFollowWord() {
|
|
620
|
+
return OPERAND_KEYWORDS.has(word) || word === "of" && top().header || keyNext && MODIFIERS.has(word);
|
|
621
|
+
}
|
|
622
|
+
/** Close the frames from index `k` on. */
|
|
623
|
+
function truncate(k) {
|
|
624
|
+
if (lowest) {
|
|
625
|
+
for (let j = frames.length - 1; j >= k; j--) foldLowest(j);
|
|
626
|
+
lowest.length = k;
|
|
627
|
+
startsAt.length = k;
|
|
628
|
+
}
|
|
629
|
+
frames.length = k;
|
|
630
|
+
if (stackIds) stackIds.length = k;
|
|
631
|
+
for (const list of Object.values(closers)) while (list.length && list[list.length - 1] >= k) list.pop();
|
|
632
|
+
if (pendingBody && pendingBody.depth > k) pendingBody = void 0;
|
|
633
|
+
}
|
|
634
|
+
const code = (ch, index) => handlers.code?.(ch, index, nesting);
|
|
635
|
+
const literal = (text, index) => handlers.literal?.(text, index, nesting);
|
|
636
|
+
/** Literal text from `from` to `to`, sliced only for a handler. */
|
|
637
|
+
const literalSpan = (from, to) => handlers.literal?.(src.slice(from, to), from, nesting);
|
|
638
|
+
/** Track the word just read (`i` is the index just past it). */
|
|
639
|
+
function endWord() {
|
|
640
|
+
if (!word) return;
|
|
641
|
+
const keyword = afterDot || keyNext ? "" : word;
|
|
642
|
+
const inOperandPosition = operandNext && !stmtNext;
|
|
643
|
+
operandNext = OPERAND_KEYWORDS.has(keyword) || keyword === "of" && top().header && !operandNext;
|
|
644
|
+
afterHeaderKeyword = HEADER_KEYWORDS.has(keyword);
|
|
645
|
+
if (keyword === "function" || keyword === "class") {
|
|
646
|
+
const body = frame(keyword === "class" ? "class" : "block", "}");
|
|
647
|
+
body.operand = inOperandPosition;
|
|
648
|
+
pendingBody = {
|
|
649
|
+
depth: frames.length,
|
|
650
|
+
frame: body
|
|
651
|
+
};
|
|
652
|
+
bodyStarts++;
|
|
653
|
+
}
|
|
654
|
+
keyNext = keyNext && MODIFIERS.has(word) && KEY_START_RE.test(src.charAt(skipTrivia(src, i)));
|
|
655
|
+
stmtNext = keyword === "else" || keyword === "do" || keyword === "async" && stmtNext;
|
|
656
|
+
arrowBody = false;
|
|
657
|
+
afterDot = false;
|
|
658
|
+
lastPunct = "";
|
|
659
|
+
lastKeyword = keyword;
|
|
660
|
+
lineBreak = false;
|
|
661
|
+
word = "";
|
|
662
|
+
}
|
|
663
|
+
/** A string, template or regex literal, or a variable reference, ended. */
|
|
664
|
+
function endOperand() {
|
|
665
|
+
endWord();
|
|
666
|
+
operandNext = false;
|
|
667
|
+
stmtNext = false;
|
|
668
|
+
arrowBody = false;
|
|
669
|
+
keyNext = false;
|
|
670
|
+
afterHeaderKeyword = false;
|
|
671
|
+
afterDot = false;
|
|
672
|
+
lastPunct = "";
|
|
673
|
+
lastKeyword = "";
|
|
674
|
+
lineBreak = false;
|
|
675
|
+
}
|
|
676
|
+
/** Track an operator token: `operandNext` tells what may follow it. */
|
|
677
|
+
function endPunct(punct, nextIsOperand) {
|
|
678
|
+
operandNext = nextIsOperand;
|
|
679
|
+
stmtNext = false;
|
|
680
|
+
arrowBody = false;
|
|
681
|
+
keyNext = false;
|
|
682
|
+
afterDot = punct === "." && !nextIsOperand;
|
|
683
|
+
afterHeaderKeyword = false;
|
|
684
|
+
lastPunct = punct;
|
|
685
|
+
lastKeyword = "";
|
|
686
|
+
lineBreak = false;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* A line break between tokens. After `return`, `break` or `continue` it
|
|
690
|
+
* ends the statement (ASI): `return⏎function f() {}` declares `f`.
|
|
691
|
+
*/
|
|
692
|
+
function lineBreakSeen() {
|
|
693
|
+
lineBreak = true;
|
|
694
|
+
if (RESTRICTED_KEYWORDS.has(lastKeyword)) {
|
|
695
|
+
operandNext = true;
|
|
696
|
+
stmtNext = true;
|
|
697
|
+
lastKeyword = "";
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function open(f) {
|
|
701
|
+
push(f);
|
|
702
|
+
stmtNext = f.kind === "block";
|
|
703
|
+
keyNext = f.kind === "object" || f.kind === "class";
|
|
704
|
+
}
|
|
705
|
+
function openBrace() {
|
|
706
|
+
let f;
|
|
707
|
+
if (pendingBody?.depth === frames.length) {
|
|
708
|
+
f = pendingBody.frame;
|
|
709
|
+
pendingBody = void 0;
|
|
710
|
+
} else if (!operandNext || stmtNext || arrowBody) f = frame("block", "}");
|
|
711
|
+
else {
|
|
712
|
+
f = frame("object", "}");
|
|
713
|
+
f.operand = true;
|
|
714
|
+
}
|
|
715
|
+
f.open = i;
|
|
716
|
+
endPunct("{", true);
|
|
717
|
+
const end = known(f);
|
|
718
|
+
if (end === void 0) open(f);
|
|
719
|
+
else skipTo = {
|
|
720
|
+
frame: f,
|
|
721
|
+
end
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/** State after the `}` closing `closed`, a block or literal. */
|
|
725
|
+
function afterBrace(closed) {
|
|
726
|
+
if (closed.operand) endPunct("}", false);
|
|
727
|
+
else {
|
|
728
|
+
endPunct("}", true);
|
|
729
|
+
stmtNext = top().kind === "block";
|
|
730
|
+
keyNext = top().kind === "class";
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Close the innermost frame that `c` closes; a stray closer, with no such
|
|
735
|
+
* frame within the innermost braces (a block, an object literal, a class
|
|
736
|
+
* body or an interpolation), is ignored. So a stray `)` or `]` never
|
|
737
|
+
* closes the braces around it, and how a `{…}` ends depends only on the
|
|
738
|
+
* code inside it.
|
|
739
|
+
*/
|
|
740
|
+
function close(c) {
|
|
741
|
+
const k = Math.max(innermost(c), innermost("}"));
|
|
742
|
+
lookedFor(c, k);
|
|
743
|
+
if (k === 0 || frames[k].closer !== c) {
|
|
744
|
+
endPunct(c, c === "}");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const closed = frames[k];
|
|
748
|
+
if (lowest && c !== "}") {
|
|
749
|
+
for (let j = frames.length - 1; j > k; j--) foldLowest(j);
|
|
750
|
+
const bodyStarted = bodyStarts !== startsAt[k];
|
|
751
|
+
recordParen(k, i * 2 + +bodyStarted);
|
|
752
|
+
}
|
|
753
|
+
truncate(k);
|
|
754
|
+
if (c === "]" && !ctx.lookahead) ctx.brackets.set(closed.open, i);
|
|
755
|
+
if (c === ")") {
|
|
756
|
+
endPunct(c, closed.header);
|
|
757
|
+
stmtNext = closed.header;
|
|
758
|
+
} else if (c === "]") endPunct(c, false);
|
|
759
|
+
else {
|
|
760
|
+
record(closed, i);
|
|
761
|
+
afterBrace(closed);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function trackCode(c) {
|
|
765
|
+
if (WORD_CHAR_RE.test(c)) {
|
|
766
|
+
word += c;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
endWord();
|
|
770
|
+
if (LINE_TERMINATOR_RE.test(c)) lineBreakSeen();
|
|
771
|
+
if (SPACE_RE.test(c)) return;
|
|
772
|
+
const t = top();
|
|
773
|
+
switch (c) {
|
|
774
|
+
case "(":
|
|
775
|
+
case "[": {
|
|
776
|
+
const f = frame("expr", c === "(" ? ")" : "]", i);
|
|
777
|
+
f.header = c === "(" && afterHeaderKeyword;
|
|
778
|
+
endPunct(c, true);
|
|
779
|
+
const result = knownParen(f);
|
|
780
|
+
if (result === void 0) open(f);
|
|
781
|
+
else parenTo = {
|
|
782
|
+
frame: f,
|
|
783
|
+
result
|
|
784
|
+
};
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case "{":
|
|
788
|
+
openBrace();
|
|
789
|
+
break;
|
|
790
|
+
case ")":
|
|
791
|
+
case "]":
|
|
792
|
+
case "}":
|
|
793
|
+
close(c);
|
|
794
|
+
break;
|
|
795
|
+
case ".":
|
|
796
|
+
dots = lastPunct === "." ? dots + 1 : 1;
|
|
797
|
+
endPunct(c, dots === 3);
|
|
798
|
+
break;
|
|
799
|
+
case ";":
|
|
800
|
+
endPunct(c, true);
|
|
801
|
+
stmtNext = t.kind === "block";
|
|
802
|
+
keyNext = t.kind === "class";
|
|
803
|
+
break;
|
|
804
|
+
case ",":
|
|
805
|
+
endPunct(c, true);
|
|
806
|
+
keyNext = t.kind === "object";
|
|
807
|
+
break;
|
|
808
|
+
case "?":
|
|
809
|
+
endPunct(c, true);
|
|
810
|
+
t.ternary++;
|
|
811
|
+
break;
|
|
812
|
+
case ":":
|
|
813
|
+
endPunct(c, true);
|
|
814
|
+
if (t.ternary > 0) t.ternary--;
|
|
815
|
+
else stmtNext = t.kind === "block";
|
|
816
|
+
break;
|
|
817
|
+
case "#":
|
|
818
|
+
endPunct(c, false);
|
|
819
|
+
afterDot = true;
|
|
820
|
+
break;
|
|
821
|
+
case "*": {
|
|
822
|
+
const key = keyNext;
|
|
823
|
+
endPunct(c, true);
|
|
824
|
+
keyNext = key;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case ">": {
|
|
828
|
+
const arrow = lastPunct === "=";
|
|
829
|
+
endPunct(c, true);
|
|
830
|
+
arrowBody = arrow;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
default: endPunct(c, true);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
while (i < src.length) {
|
|
837
|
+
const ch = src.charAt(i);
|
|
838
|
+
const checkpoints = strict?.checkpoints;
|
|
839
|
+
const key = checkpoints && checkpointKey();
|
|
840
|
+
if (key !== void 0) {
|
|
841
|
+
const known = checkpoints.get(key);
|
|
842
|
+
if (known === void 0) strict.passed.push(key);
|
|
843
|
+
else if (frames.length > 1 && known < 0) {
|
|
844
|
+
strict.malformed = known === MALFORMED;
|
|
845
|
+
return src.length;
|
|
846
|
+
} else if (known === MALFORMED) return malformed();
|
|
847
|
+
else if (known === UNCLOSED) {
|
|
848
|
+
i = src.length;
|
|
849
|
+
break;
|
|
850
|
+
} else {
|
|
851
|
+
strict.stopped = true;
|
|
852
|
+
return known;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (top().kind === "template") {
|
|
856
|
+
if (ch === "\\") {
|
|
857
|
+
literal(src.slice(i, i + 2), i);
|
|
858
|
+
i += 2;
|
|
859
|
+
} else if (ch === "`") {
|
|
860
|
+
literal(ch, i);
|
|
861
|
+
if (frames.length === 1) return i + 1;
|
|
862
|
+
record(top(), i);
|
|
863
|
+
i++;
|
|
864
|
+
truncate(frames.length - 1);
|
|
865
|
+
endOperand();
|
|
866
|
+
} else if (ch === "$" && src.charAt(i + 1) === "{") {
|
|
867
|
+
const f = frame("expr", "}", i);
|
|
868
|
+
f.interpolation = true;
|
|
869
|
+
const end = known(f);
|
|
870
|
+
if (end === MALFORMED) return malformed();
|
|
871
|
+
if (end === UNCLOSED) {
|
|
872
|
+
i = src.length;
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
if (end !== void 0) {
|
|
876
|
+
i = end + 1;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
literal("${", i);
|
|
880
|
+
i += 2;
|
|
881
|
+
nesting++;
|
|
882
|
+
endPunct("{", true);
|
|
883
|
+
open(f);
|
|
884
|
+
} else {
|
|
885
|
+
literal(ch, i);
|
|
886
|
+
i++;
|
|
887
|
+
}
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (ch === "\"" || ch === "'") {
|
|
891
|
+
if (strict && i > start && WORD_CHAR_RE.test(src.charAt(i - 1))) {
|
|
892
|
+
if (!stringMayFollowWord()) return malformed();
|
|
893
|
+
}
|
|
894
|
+
endWord();
|
|
895
|
+
const { end, closed } = scanStringLiteral(src, i);
|
|
896
|
+
if (strict && !closed) return malformed();
|
|
897
|
+
literalSpan(i, end);
|
|
898
|
+
i = end;
|
|
899
|
+
endOperand();
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (ch === "`") {
|
|
903
|
+
endWord();
|
|
904
|
+
const f = frame("template", "`", i);
|
|
905
|
+
const end = known(f);
|
|
906
|
+
if (end === MALFORMED) return malformed();
|
|
907
|
+
if (end === UNCLOSED) {
|
|
908
|
+
i = src.length;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
if (end !== void 0) {
|
|
912
|
+
i = end + 1;
|
|
913
|
+
endOperand();
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
literal(ch, i);
|
|
917
|
+
push(f);
|
|
918
|
+
i++;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (ch === "{" && strict?.stop?.(i)) {
|
|
922
|
+
recordOpenParens(STOPPED - i);
|
|
923
|
+
strict.stopped = true;
|
|
924
|
+
return i;
|
|
925
|
+
}
|
|
926
|
+
if (ch === "}") {
|
|
927
|
+
const k = innermost("}");
|
|
928
|
+
lookedFor("}", k);
|
|
929
|
+
if (frames[k].interpolation) {
|
|
930
|
+
endWord();
|
|
931
|
+
record(frames[k], i);
|
|
932
|
+
truncate(k);
|
|
933
|
+
nesting--;
|
|
934
|
+
literal(ch, i);
|
|
935
|
+
i++;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (ch === "/") {
|
|
940
|
+
endWord();
|
|
941
|
+
const next = src.charAt(i + 1);
|
|
942
|
+
if (next === "/" || next === "*") {
|
|
943
|
+
let end = findCommentEnd(src, i, strict?.cache);
|
|
944
|
+
if (end < 0) {
|
|
945
|
+
if (strict) return malformed();
|
|
946
|
+
end = src.length;
|
|
947
|
+
}
|
|
948
|
+
literalSpan(i, end);
|
|
949
|
+
if (next === "*" && lineBreakIn(src, i, end, strict?.cache)) lineBreakSeen();
|
|
950
|
+
i = end;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (operandNext) {
|
|
954
|
+
const { end, closed } = scanRegex(src, i, strict?.cache);
|
|
955
|
+
if (strict && !closed) return malformed();
|
|
956
|
+
literalSpan(i, end);
|
|
957
|
+
i = end;
|
|
958
|
+
endOperand();
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (!word && lineBreak && !operandNext && top().kind === "class") {
|
|
963
|
+
IDENT_RE.lastIndex = i;
|
|
964
|
+
const next = IDENT_RE.exec(src)?.[0];
|
|
965
|
+
if (next && next !== "in" && next !== "instanceof") keyNext = true;
|
|
966
|
+
}
|
|
967
|
+
if (ch === "@" || (ch === "$" || ch === "_") && !word) {
|
|
968
|
+
endWord();
|
|
969
|
+
IDENT_RE.lastIndex = i + 1;
|
|
970
|
+
const name = IDENT_RE.exec(src)?.[0] ?? "";
|
|
971
|
+
if (VAR_NAME_RE.test(name) && !isPropertyName(ch, i + 1 + name.length)) {
|
|
972
|
+
handlers.variable?.(ch, name, i, nesting);
|
|
973
|
+
i += 1 + name.length;
|
|
974
|
+
endOperand();
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (ch === "%") {
|
|
979
|
+
endWord();
|
|
980
|
+
TRANS_NAME_RE.lastIndex = i + 1;
|
|
981
|
+
const name = TRANS_NAME_RE.exec(src)?.[0];
|
|
982
|
+
if (name) {
|
|
983
|
+
const end = i + 1 + name.length;
|
|
984
|
+
if (operandNext || lineBreak && ctx.lookahead && assignmentFollows(src, end, ctx)) {
|
|
985
|
+
handlers.variable?.("%", name, i, nesting);
|
|
986
|
+
i = end;
|
|
987
|
+
endOperand();
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if ((ch === "+" || ch === "-") && src.charAt(i + 1) === ch) {
|
|
993
|
+
endWord();
|
|
994
|
+
const postfix = !operandNext && !lineBreak;
|
|
995
|
+
code(ch, i);
|
|
996
|
+
code(ch, i + 1);
|
|
997
|
+
i += 2;
|
|
998
|
+
endPunct(ch, !postfix);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (ch === "?") {
|
|
1002
|
+
const next = src.charAt(i + 1);
|
|
1003
|
+
if (next === "?" || next === "." && !/\d/.test(src.charAt(i + 2))) {
|
|
1004
|
+
endWord();
|
|
1005
|
+
code(ch, i);
|
|
1006
|
+
code(next, i + 1);
|
|
1007
|
+
i += 2;
|
|
1008
|
+
dots = 1;
|
|
1009
|
+
endPunct(next, next === "?");
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (ch === outer.closer && frames.length === 1) break;
|
|
1014
|
+
trackCode(ch);
|
|
1015
|
+
if (skipTo) {
|
|
1016
|
+
const { frame: skipped, end } = skipTo;
|
|
1017
|
+
skipTo = void 0;
|
|
1018
|
+
if (end === MALFORMED) return malformed();
|
|
1019
|
+
if (end === UNCLOSED) {
|
|
1020
|
+
i = src.length;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
i = end;
|
|
1024
|
+
afterBrace(skipped);
|
|
1025
|
+
i++;
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
if (parenTo) {
|
|
1029
|
+
const { frame: skipped, result } = parenTo;
|
|
1030
|
+
parenTo = void 0;
|
|
1031
|
+
if (result === MALFORMED) return malformed();
|
|
1032
|
+
if (result === UNCLOSED) {
|
|
1033
|
+
i = src.length;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
if (result <= STOPPED) {
|
|
1037
|
+
recordOpenParens(result);
|
|
1038
|
+
strict.stopped = true;
|
|
1039
|
+
return STOPPED - result;
|
|
1040
|
+
}
|
|
1041
|
+
i = Math.floor(result / 2);
|
|
1042
|
+
if (result % 2 === 1) pendingBody = void 0;
|
|
1043
|
+
if (skipped.closer === ")") {
|
|
1044
|
+
endPunct(")", skipped.header);
|
|
1045
|
+
stmtNext = skipped.header;
|
|
1046
|
+
} else endPunct("]", false);
|
|
1047
|
+
i++;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
code(ch, i);
|
|
1051
|
+
i++;
|
|
1052
|
+
}
|
|
1053
|
+
if (i >= src.length) {
|
|
1054
|
+
recordOpen(UNCLOSED);
|
|
1055
|
+
recordOpenParens(UNCLOSED);
|
|
1056
|
+
}
|
|
1057
|
+
if (!ctx.lookahead) {
|
|
1058
|
+
for (const f of frames) if (f.closer === "]") ctx.brackets.set(f.open, i);
|
|
1059
|
+
}
|
|
1060
|
+
return Math.min(i, src.length);
|
|
1061
|
+
/**
|
|
1062
|
+
* Is the `$`/`_` word ending at `end` a property name: after `.`, a key
|
|
1063
|
+
* in an object literal (`{ _id: 1 }`, `{ _m() {} }`) or a member name in
|
|
1064
|
+
* a class body?
|
|
1065
|
+
*/
|
|
1066
|
+
function isPropertyName(sigil, end) {
|
|
1067
|
+
if (afterDot) return true;
|
|
1068
|
+
if (!keyNext || sigil === "@") return false;
|
|
1069
|
+
if (top().kind === "class") return true;
|
|
1070
|
+
const next = src.charAt(skipTrivia(src, end));
|
|
1071
|
+
return next === ":" || next === "(";
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/markup/code-attributes.ts
|
|
1076
|
+
/**
|
|
1077
|
+
* Attributes whose value is code with braces of its own, not text: event
|
|
1078
|
+
* handlers (`on…`, JavaScript), `pattern` (a regular expression, as in
|
|
1079
|
+
* `\p{L}{2,3}`) and `srcdoc` (an HTML document, with its own scripts and
|
|
1080
|
+
* styles). Their braces are code, so they are not read as markup: only
|
|
1081
|
+
* sigil references (`{$x}`, `{_x}`, `{@x}`, `{%x}` and expressions starting
|
|
1082
|
+
* with one) are resolved, and any other `{` is literal.
|
|
1083
|
+
*/
|
|
1084
|
+
function isCodeAttribute(name) {
|
|
1085
|
+
const lower = name.toLowerCase();
|
|
1086
|
+
return lower.startsWith("on") || lower === "pattern" || lower === "srcdoc";
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Split a code attribute value into literal text and its `{…}` blocks that
|
|
1090
|
+
* start with a sigil. Braces inside strings don't close a block; an
|
|
1091
|
+
* unclosed block, any other brace and backslashes are text. A block whose
|
|
1092
|
+
* sigil is not followed by a word character (`{$(…)}`) is code, kept
|
|
1093
|
+
* verbatim: not resolved, and not decoded like text.
|
|
1094
|
+
*/
|
|
1095
|
+
function splitSigilTemplate(template) {
|
|
1096
|
+
const parts = [];
|
|
1097
|
+
const memo = createScanMemo();
|
|
1098
|
+
let text = "";
|
|
1099
|
+
let i = 0;
|
|
1100
|
+
while (i < template.length) {
|
|
1101
|
+
const brace = template.indexOf("{", i);
|
|
1102
|
+
if (brace === -1) {
|
|
1103
|
+
text += template.slice(i);
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
text += template.slice(i, brace);
|
|
1107
|
+
i = brace + 1;
|
|
1108
|
+
const sigil = template[i];
|
|
1109
|
+
const end = sigil === "$" || sigil === "_" || sigil === "@" || sigil === "%" ? scanBalancedBrace(template, i, memo) : -1;
|
|
1110
|
+
if (end === -1) {
|
|
1111
|
+
text += "{";
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (text) parts.push({ text });
|
|
1115
|
+
text = "";
|
|
1116
|
+
const block = template.slice(i, end);
|
|
1117
|
+
parts.push(/^.\w/.test(block) ? { expr: block } : { verbatim: `{${block}}` });
|
|
1118
|
+
i = end + 1;
|
|
1119
|
+
}
|
|
1120
|
+
if (text) parts.push({ text });
|
|
1121
|
+
return parts;
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
1
1124
|
//#region src/markup/tokenizer.ts
|
|
2
1125
|
/** Tag name must start with a letter (covers standard and custom elements). */
|
|
3
1126
|
var VALID_TAG_START = /[a-zA-Z]/;
|
|
@@ -18,6 +1141,20 @@ var HTML_VOID_TAGS = new Set([
|
|
|
18
1141
|
"track",
|
|
19
1142
|
"wbr"
|
|
20
1143
|
]);
|
|
1144
|
+
/** Variable sigils: story ($), temporary (_), local (@), transient (%). */
|
|
1145
|
+
var SIGIL_CHARS = new Set([
|
|
1146
|
+
"$",
|
|
1147
|
+
"_",
|
|
1148
|
+
"@",
|
|
1149
|
+
"%"
|
|
1150
|
+
]);
|
|
1151
|
+
/**
|
|
1152
|
+
* Characters other than a sigil that open an expression after `{`:
|
|
1153
|
+
* `{(Math.max($a, 0))}`, `{!$done}`. Other characters that can start an
|
|
1154
|
+
* expression (quotes, digits, `[`, `-`) are left out, because braces around
|
|
1155
|
+
* them are common as literal text (JSON, regex quantifiers, `{[[link]]}`).
|
|
1156
|
+
*/
|
|
1157
|
+
var EXPRESSION_START = new Set(["(", "!"]);
|
|
21
1158
|
/** Macros whose body is JavaScript source, kept verbatim instead of tokenized. */
|
|
22
1159
|
var RAW_BODY_MACROS = new Set(["do"]);
|
|
23
1160
|
/**
|
|
@@ -84,7 +1221,7 @@ function parseSelectors(input, startIdx) {
|
|
|
84
1221
|
while (i < input.length) if (/[a-zA-Z0-9_-]/.test(input[i])) {
|
|
85
1222
|
name += input[i];
|
|
86
1223
|
i++;
|
|
87
|
-
} else if (input[i] === "{" && (input[i + 1]
|
|
1224
|
+
} else if (input[i] === "{" && SIGIL_CHARS.has(input[i + 1])) {
|
|
88
1225
|
const braceStart = i;
|
|
89
1226
|
i += 2;
|
|
90
1227
|
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
@@ -109,10 +1246,48 @@ function parseSelectors(input, startIdx) {
|
|
|
109
1246
|
* Parse HTML attributes from a string starting at position j.
|
|
110
1247
|
* Returns the attributes and the position after the last attribute.
|
|
111
1248
|
*/
|
|
112
|
-
function parseHtmlAttributes(input, j) {
|
|
1249
|
+
function parseHtmlAttributes(input, j, memo) {
|
|
113
1250
|
const attributes = {};
|
|
1251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1252
|
+
return {
|
|
1253
|
+
attributes,
|
|
1254
|
+
endIdx: scanAttributes(input, j, memo, (name, value) => {
|
|
1255
|
+
const lower = name.toLowerCase();
|
|
1256
|
+
if (seen.has(lower)) return;
|
|
1257
|
+
seen.add(lower);
|
|
1258
|
+
Object.defineProperty(attributes, name, {
|
|
1259
|
+
value,
|
|
1260
|
+
enumerable: true,
|
|
1261
|
+
writable: true,
|
|
1262
|
+
configurable: true
|
|
1263
|
+
});
|
|
1264
|
+
})
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Scan the attributes of a tag from position j, handing each to `add` when
|
|
1269
|
+
* given. Returns the position after the last attribute.
|
|
1270
|
+
*
|
|
1271
|
+
* Where the attributes end depends only on where an attribute starts, so
|
|
1272
|
+
* without `add` the result is recorded for every attribute start passed (in
|
|
1273
|
+
* `memo.tag`) and a recorded one is used instead of scanning on. An unquoted
|
|
1274
|
+
* value takes in a `<`, so in `<a x=<a x=<a x=…` each opener's scan would
|
|
1275
|
+
* otherwise run over all the openers after it, taking quadratic time. A tag
|
|
1276
|
+
* whose attributes end in its `>` is consumed, so collecting the attributes
|
|
1277
|
+
* of those (with `add`, unrecorded) reads each character once.
|
|
1278
|
+
*/
|
|
1279
|
+
function scanAttributes(input, j, memo, add) {
|
|
1280
|
+
const passed = [];
|
|
114
1281
|
while (j < input.length) {
|
|
115
1282
|
while (j < input.length && /\s/.test(input[j])) j++;
|
|
1283
|
+
if (!add) {
|
|
1284
|
+
const known = memo.tag.get(j);
|
|
1285
|
+
if (known !== void 0) {
|
|
1286
|
+
j = known;
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
passed.push(j);
|
|
1290
|
+
}
|
|
116
1291
|
if (j >= input.length || input[j] === ">" || input[j] === "/" && input[j + 1] === ">") break;
|
|
117
1292
|
const attrStart = j;
|
|
118
1293
|
while (j < input.length && /[a-zA-Z0-9_\-:@]/.test(input[j])) j++;
|
|
@@ -127,31 +1302,180 @@ function parseHtmlAttributes(input, j) {
|
|
|
127
1302
|
const quote = input[j];
|
|
128
1303
|
j++;
|
|
129
1304
|
const valStart = j;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const closeIdx = scanBalancedBrace(input, j + 1);
|
|
133
|
-
if (closeIdx !== -1) {
|
|
134
|
-
j = closeIdx + 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
} else if (input[j] === quote) break;
|
|
138
|
-
j++;
|
|
139
|
-
}
|
|
140
|
-
attributes[attrName] = input.slice(valStart, j);
|
|
1305
|
+
j = scanQuotedValue(input, j, quote, isCodeAttribute(attrName), memo);
|
|
1306
|
+
add?.(attrName, input.slice(valStart, j));
|
|
141
1307
|
if (j < input.length) j++;
|
|
142
1308
|
} else {
|
|
143
1309
|
const valStart = j;
|
|
144
|
-
|
|
145
|
-
|
|
1310
|
+
const run = memo.unquoted;
|
|
1311
|
+
if (j < run.from || j > run.to) {
|
|
1312
|
+
run.from = j;
|
|
1313
|
+
while (j < input.length && /[^\s>]/.test(input[j])) j++;
|
|
1314
|
+
run.to = j;
|
|
1315
|
+
}
|
|
1316
|
+
j = run.to;
|
|
1317
|
+
add?.(attrName, input.slice(valStart, j));
|
|
1318
|
+
}
|
|
1319
|
+
} else add?.(attrName, "");
|
|
1320
|
+
}
|
|
1321
|
+
for (const at of passed) memo.tag.set(at, j);
|
|
1322
|
+
return j;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Index of the quote ending the attribute value that starts at `j`, or the
|
|
1326
|
+
* end of the input. A `{…}` interpolation in it is skipped whole, so quotes
|
|
1327
|
+
* inside it don't end the value. A code attribute's value is no markup
|
|
1328
|
+
* (`isCodeAttribute`): only `{` and a sigil open a reference, other braces
|
|
1329
|
+
* and backslashes are text, as `splitSigilTemplate` reads them.
|
|
1330
|
+
*
|
|
1331
|
+
* The value reads the same from just past an interpolation, however the scan
|
|
1332
|
+
* got there, so the end is recorded there (in `memo.value`), and a recorded
|
|
1333
|
+
* one is used. An unclosed value that skips interpolations to the end of
|
|
1334
|
+
* the input (`<a x="}<a x={<a x="}…`) is then read once, not once per tag.
|
|
1335
|
+
*/
|
|
1336
|
+
function scanQuotedValue(input, j, quote, code, memo) {
|
|
1337
|
+
const variant = (quote === "\"" ? 0 : 2) + (code ? 1 : 0);
|
|
1338
|
+
const passed = [];
|
|
1339
|
+
let checkpoint = true;
|
|
1340
|
+
while (j < input.length) {
|
|
1341
|
+
if (checkpoint) {
|
|
1342
|
+
const known = memo.value.get(j * 4 + variant);
|
|
1343
|
+
if (known !== void 0) {
|
|
1344
|
+
j = known;
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
passed.push(j * 4 + variant);
|
|
1348
|
+
checkpoint = false;
|
|
1349
|
+
}
|
|
1350
|
+
if (!code && input[j] === "\\") {
|
|
1351
|
+
let k = j + 1;
|
|
1352
|
+
while (input[k] === "\\") k++;
|
|
1353
|
+
j = (input[k] === "{" || input[k] === "}") && (k - j) % 2 === 1 ? k + 1 : k;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (input[j] === "{") {
|
|
1357
|
+
const closeIdx = !code ? scanBlockClose(input, j, memo) : SIGIL_CHARS.has(input[j + 1]) ? scanBalancedBrace(input, j + 1, memo) : -1;
|
|
1358
|
+
if (closeIdx !== -1) {
|
|
1359
|
+
j = closeIdx + 1;
|
|
1360
|
+
checkpoint = true;
|
|
1361
|
+
continue;
|
|
146
1362
|
}
|
|
147
|
-
} else
|
|
1363
|
+
} else if (input[j] === quote) break;
|
|
1364
|
+
j++;
|
|
148
1365
|
}
|
|
1366
|
+
for (const at of passed) memo.value.set(at, j);
|
|
1367
|
+
return j;
|
|
1368
|
+
}
|
|
1369
|
+
function createScanMemo() {
|
|
149
1370
|
return {
|
|
150
|
-
|
|
151
|
-
|
|
1371
|
+
code: /* @__PURE__ */ new Map(),
|
|
1372
|
+
js: createJsScanCache(),
|
|
1373
|
+
brace: /* @__PURE__ */ new Map(),
|
|
1374
|
+
template: /* @__PURE__ */ new Map(),
|
|
1375
|
+
templateText: /* @__PURE__ */ new Map(),
|
|
1376
|
+
name: {
|
|
1377
|
+
from: 0,
|
|
1378
|
+
to: -1
|
|
1379
|
+
},
|
|
1380
|
+
unquoted: {
|
|
1381
|
+
from: 0,
|
|
1382
|
+
to: -1
|
|
1383
|
+
},
|
|
1384
|
+
link: /* @__PURE__ */ new Map(),
|
|
1385
|
+
tag: /* @__PURE__ */ new Map(),
|
|
1386
|
+
value: /* @__PURE__ */ new Map(),
|
|
1387
|
+
rawClose: /* @__PURE__ */ new Map()
|
|
152
1388
|
};
|
|
153
1389
|
}
|
|
154
1390
|
/**
|
|
1391
|
+
* Find the } closing the code that starts at position i: a `{$…}`
|
|
1392
|
+
* expression from its sigil on, an attribute interpolation. The code is
|
|
1393
|
+
* lexed as JavaScript, so only a } in code outside the brackets it opened
|
|
1394
|
+
* counts — not one in a string, template or regex literal or a comment —
|
|
1395
|
+
* and `/` after an operand divides.
|
|
1396
|
+
*
|
|
1397
|
+
* Text that is not well-formed JavaScript (an apostrophe, an unterminated
|
|
1398
|
+
* literal or comment, unbalanced brackets) is scanned leniently instead, as
|
|
1399
|
+
* prose-like macro arguments are: braces count except inside string and
|
|
1400
|
+
* template literals, and a quote that can't start a string (apostrophe,
|
|
1401
|
+
* escaped, not closed on its line) is text.
|
|
1402
|
+
*
|
|
1403
|
+
* Returns the index of the closing } or -1 if there is none. Pass the same
|
|
1404
|
+
* memo to repeated scans of one input to share their work.
|
|
1405
|
+
*/
|
|
1406
|
+
function scanBalancedBrace(input, i, memo = createScanMemo()) {
|
|
1407
|
+
return scanClose(input, i, i, memo);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Index of the } closing the macro whose content (name, then arguments)
|
|
1411
|
+
* starts at `contentStart`, or -1. The name runs up to whitespace or the }
|
|
1412
|
+
* (as `parseMacroContent` reads it); the arguments after it are code. The
|
|
1413
|
+
* lenient scan covers the whole content, as it always has.
|
|
1414
|
+
*/
|
|
1415
|
+
function scanMacroClose(input, contentStart, memo) {
|
|
1416
|
+
const run = memo.name;
|
|
1417
|
+
if (contentStart < run.from || contentStart > run.to) {
|
|
1418
|
+
let k = contentStart;
|
|
1419
|
+
while (k < input.length && input[k] !== "}" && !/\s/.test(input[k])) k++;
|
|
1420
|
+
run.from = contentStart;
|
|
1421
|
+
run.to = k;
|
|
1422
|
+
}
|
|
1423
|
+
return scanClose(input, run.to, contentStart, memo);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Index of the } closing the `{…}` block opened at `open`, read as passage
|
|
1427
|
+
* text reads it: a macro (`{name args}`, `{/name}`) has its arguments lexed
|
|
1428
|
+
* after its name, an expression (`{$…}`, `{(…)}`, `{!…}`) from its first
|
|
1429
|
+
* character, after any `.class#id` selectors. Any other block is scanned as
|
|
1430
|
+
* code from just past the {. Returns -1 if it is unclosed.
|
|
1431
|
+
*/
|
|
1432
|
+
function scanBlockClose(input, open, memo) {
|
|
1433
|
+
let at = open + 1;
|
|
1434
|
+
const c = input[at];
|
|
1435
|
+
if (c === "." || c === "#") {
|
|
1436
|
+
at = parseSelectors(input, at).endIdx;
|
|
1437
|
+
if (input[at] === " ") at++;
|
|
1438
|
+
}
|
|
1439
|
+
const first = input[at];
|
|
1440
|
+
if (first !== void 0 && (first === "/" || /[a-zA-Z]/.test(first))) return scanMacroClose(input, at, memo);
|
|
1441
|
+
return scanBalancedBrace(input, at, memo);
|
|
1442
|
+
}
|
|
1443
|
+
/** Lex the code from `codeStart`, else scan leniently from `lenientStart`. */
|
|
1444
|
+
function scanClose(input, codeStart, lenientStart, memo) {
|
|
1445
|
+
let end = memo.code.get(lenientStart);
|
|
1446
|
+
if (end === void 0) {
|
|
1447
|
+
end = findCodeEnd(input, codeStart, { cache: memo.js });
|
|
1448
|
+
if (end === -1) end = scanBraceLenient(input, lenientStart, memo);
|
|
1449
|
+
memo.code.set(lenientStart, end);
|
|
1450
|
+
}
|
|
1451
|
+
return end;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Index of the ]] closing the link whose text starts at `from`, or -1. A
|
|
1455
|
+
* [[ inside opens a nested pair. Each nested [[ starts a scan that reads the
|
|
1456
|
+
* rest the same way, so its result is recorded too: unclosed [[s don't each
|
|
1457
|
+
* scan to the end of the input.
|
|
1458
|
+
*/
|
|
1459
|
+
function scanLinkClose(input, from, memo) {
|
|
1460
|
+
const known = memo.link.get(from);
|
|
1461
|
+
if (known !== void 0) return known;
|
|
1462
|
+
const opens = [from];
|
|
1463
|
+
let i = from;
|
|
1464
|
+
while (i < input.length) if (input[i] === "[" && input[i + 1] === "[") {
|
|
1465
|
+
i += 2;
|
|
1466
|
+
const inner = memo.link.get(i);
|
|
1467
|
+
if (inner === void 0) opens.push(i);
|
|
1468
|
+
else if (inner === -1) break;
|
|
1469
|
+
else i = inner + 2;
|
|
1470
|
+
} else if (input[i] === "]" && input[i + 1] === "]") {
|
|
1471
|
+
memo.link.set(opens.pop(), i);
|
|
1472
|
+
if (!opens.length) return i;
|
|
1473
|
+
i += 2;
|
|
1474
|
+
} else i++;
|
|
1475
|
+
for (const open of opens) memo.link.set(open, -1);
|
|
1476
|
+
return -1;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
155
1479
|
* Skip a '…' or "…" string literal opening at i.
|
|
156
1480
|
* Returns the index just past the closing quote, or -1 if the string is
|
|
157
1481
|
* not closed on the same line (JS strings can't span lines unescaped).
|
|
@@ -169,24 +1493,6 @@ function skipQuoted(input, i) {
|
|
|
169
1493
|
return -1;
|
|
170
1494
|
}
|
|
171
1495
|
/**
|
|
172
|
-
* Skip a `…` template literal opening at i, including ${…} parts.
|
|
173
|
-
* Returns the index just past the closing backtick, or -1 if unclosed.
|
|
174
|
-
*/
|
|
175
|
-
function skipTemplate(input, i) {
|
|
176
|
-
let j = i + 1;
|
|
177
|
-
while (j < input.length) {
|
|
178
|
-
const c = input[j];
|
|
179
|
-
if (c === "\\") j += 2;
|
|
180
|
-
else if (c === "`") return j + 1;
|
|
181
|
-
else if (c === "$" && input[j + 1] === "{") {
|
|
182
|
-
const closeIdx = scanBalancedBrace(input, j + 2);
|
|
183
|
-
if (closeIdx === -1) return -1;
|
|
184
|
-
j = closeIdx + 1;
|
|
185
|
-
} else j++;
|
|
186
|
-
}
|
|
187
|
-
return -1;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
1496
|
* A quote directly after a letter/digit is an apostrophe (don't), not a
|
|
191
1497
|
* string; after a backslash it is an escaped attribute delimiter (\").
|
|
192
1498
|
*/
|
|
@@ -194,39 +1500,131 @@ var NON_STRING_QUOTE_PREFIX = /[\p{L}\p{N}_\\]/u;
|
|
|
194
1500
|
/**
|
|
195
1501
|
* Scan for the balanced closing } starting at position i (just past the {).
|
|
196
1502
|
* Braces inside string and template literals are ignored. A quote that
|
|
197
|
-
* can't start a string (apostrophe, escaped, unterminated) counts as text
|
|
1503
|
+
* can't start a string (apostrophe, escaped, unterminated) counts as text,
|
|
1504
|
+
* and so does a backtick without a closing one.
|
|
198
1505
|
* Returns the index of the closing } or -1 if unbalanced.
|
|
1506
|
+
*
|
|
1507
|
+
* Template literals and their ${…} parts are scans on a stack, not
|
|
1508
|
+
* recursive calls, so deep nesting can't overflow the call stack. How the
|
|
1509
|
+
* text reads depends only on where a scan is, so a scan passing a point —
|
|
1510
|
+
* just past a {, a string or a template literal — goes on as a scan
|
|
1511
|
+
* starting there would: its result there is recorded too, and a recorded
|
|
1512
|
+
* result is used instead of scanning on. Scans from many starts in one
|
|
1513
|
+
* input then take about linear time.
|
|
199
1514
|
*/
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1515
|
+
function scanBraceLenient(input, i, memo) {
|
|
1516
|
+
const known = memo.brace.get(i);
|
|
1517
|
+
if (known !== void 0) return known;
|
|
1518
|
+
const stack = [{
|
|
1519
|
+
template: false,
|
|
1520
|
+
start: i,
|
|
1521
|
+
levels: [[]]
|
|
1522
|
+
}];
|
|
1523
|
+
for (;;) {
|
|
1524
|
+
const scan = stack[stack.length - 1];
|
|
1525
|
+
let end;
|
|
1526
|
+
if (scan.template) {
|
|
1527
|
+
let point = true;
|
|
1528
|
+
while (end === void 0 && i < input.length) {
|
|
1529
|
+
if (point) {
|
|
1530
|
+
const known = memo.templateText.get(i);
|
|
1531
|
+
if (known !== void 0) {
|
|
1532
|
+
end = known;
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
scan.passed.push(i);
|
|
1536
|
+
point = false;
|
|
1537
|
+
}
|
|
1538
|
+
const c = input[i];
|
|
1539
|
+
if (c === "\\") {
|
|
1540
|
+
i += 2;
|
|
1541
|
+
point = true;
|
|
1542
|
+
} else if (c === "`") end = i + 1;
|
|
1543
|
+
else if (c === "$" && input[i + 1] === "{") {
|
|
1544
|
+
const inner = memo.brace.get(i + 2);
|
|
1545
|
+
if (inner === void 0) break;
|
|
1546
|
+
if (inner === -1) end = -1;
|
|
1547
|
+
else {
|
|
1548
|
+
i = inner + 1;
|
|
1549
|
+
point = true;
|
|
1550
|
+
}
|
|
1551
|
+
} else i++;
|
|
1552
|
+
}
|
|
1553
|
+
if (end === void 0 && i < input.length) {
|
|
1554
|
+
stack.push({
|
|
1555
|
+
template: false,
|
|
1556
|
+
start: i + 2,
|
|
1557
|
+
levels: [[]]
|
|
1558
|
+
});
|
|
1559
|
+
i += 2;
|
|
211
1560
|
continue;
|
|
212
1561
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1562
|
+
end ?? (end = -1);
|
|
1563
|
+
} else {
|
|
1564
|
+
const { levels } = scan;
|
|
1565
|
+
/** The } at `close` ends the innermost level. */
|
|
1566
|
+
const closeLevel = (close) => {
|
|
1567
|
+
for (const at of levels.pop()) memo.brace.set(at, close);
|
|
1568
|
+
if (!levels.length) end = close;
|
|
1569
|
+
};
|
|
1570
|
+
while (end === void 0 && i < input.length) {
|
|
1571
|
+
const c = input[i];
|
|
1572
|
+
let at = -1;
|
|
1573
|
+
if (c === "{") {
|
|
1574
|
+
levels.push([]);
|
|
1575
|
+
at = ++i;
|
|
1576
|
+
} else if (c === "}") closeLevel(i++);
|
|
1577
|
+
else if ((c === "\"" || c === "'") && !(i > 0 && NON_STRING_QUOTE_PREFIX.test(input[i - 1]))) {
|
|
1578
|
+
const close = skipQuoted(input, i);
|
|
1579
|
+
if (close === -1) i++;
|
|
1580
|
+
else at = i = close;
|
|
1581
|
+
} else if (c === "`") {
|
|
1582
|
+
const close = memo.template.get(i);
|
|
1583
|
+
if (close === void 0) break;
|
|
1584
|
+
if (close === -1) i++;
|
|
1585
|
+
else at = i = close;
|
|
1586
|
+
} else i++;
|
|
1587
|
+
if (at === -1) continue;
|
|
1588
|
+
const known = memo.brace.get(at);
|
|
1589
|
+
if (known === void 0) levels[levels.length - 1].push(at);
|
|
1590
|
+
else if (known === -1) i = input.length;
|
|
1591
|
+
else {
|
|
1592
|
+
closeLevel(known);
|
|
1593
|
+
i = known + 1;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (end === void 0 && i < input.length) {
|
|
1597
|
+
stack.push({
|
|
1598
|
+
template: true,
|
|
1599
|
+
start: i,
|
|
1600
|
+
levels: [],
|
|
1601
|
+
passed: []
|
|
1602
|
+
});
|
|
1603
|
+
i++;
|
|
217
1604
|
continue;
|
|
218
1605
|
}
|
|
1606
|
+
if (end === void 0) {
|
|
1607
|
+
end = -1;
|
|
1608
|
+
for (const level of levels) for (const at of level) memo.brace.set(at, -1);
|
|
1609
|
+
}
|
|
219
1610
|
}
|
|
220
|
-
|
|
1611
|
+
stack.pop();
|
|
1612
|
+
(scan.template ? memo.template : memo.brace).set(scan.start, end);
|
|
1613
|
+
for (const at of scan.passed ?? []) memo.templateText.set(at, end);
|
|
1614
|
+
const parent = stack[stack.length - 1];
|
|
1615
|
+
if (!parent) return end;
|
|
1616
|
+
if (parent.template) i = end === -1 ? input.length : end + 1;
|
|
1617
|
+
else i = end === -1 ? scan.start + 1 : end;
|
|
221
1618
|
}
|
|
222
|
-
return -1;
|
|
223
1619
|
}
|
|
224
1620
|
/**
|
|
225
1621
|
* Single-pass tokenizer for Twine passage content.
|
|
226
1622
|
* Recognizes: [[links]], {$variable}, {_temporary}, {macroName args}
|
|
227
1623
|
*/
|
|
228
|
-
function tokenize(input) {
|
|
1624
|
+
function tokenize(input, options = {}) {
|
|
1625
|
+
const textMode = options.text === true;
|
|
229
1626
|
const tokens = [];
|
|
1627
|
+
const memo = createScanMemo();
|
|
230
1628
|
let i = 0;
|
|
231
1629
|
let textStart = 0;
|
|
232
1630
|
function flushText(end) {
|
|
@@ -238,20 +1636,44 @@ function tokenize(input) {
|
|
|
238
1636
|
});
|
|
239
1637
|
}
|
|
240
1638
|
/**
|
|
241
|
-
* After an opening raw-body macro ({do}), emit everything up to
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
1639
|
+
* After an opening raw-body macro ({do}), emit everything up to its
|
|
1640
|
+
* {/name} as a single text token so JavaScript source is not parsed as
|
|
1641
|
+
* markup. The body is lexed as JavaScript statements: a {/do} in code ends
|
|
1642
|
+
* it, one inside a string, template or regex literal or a comment does
|
|
1643
|
+
* not. If the body is not well-formed JavaScript up to a {/do} in code,
|
|
1644
|
+
* the first {/do} ends it. Without a closer, nothing is consumed and the
|
|
1645
|
+
* AST builder reports it.
|
|
245
1646
|
*/
|
|
246
1647
|
function consumeRawBody(name, isClose) {
|
|
247
1648
|
const lower = name.toLowerCase();
|
|
248
1649
|
if (isClose || !RAW_BODY_MACROS.has(lower)) return;
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
1650
|
+
const closer = `\\{/${lower}\\s*\\}`;
|
|
1651
|
+
let last = memo.rawClose.get(lower);
|
|
1652
|
+
if (!last || i < last.from || last.at >= 0 && i > last.at) {
|
|
1653
|
+
const firstRe = new RegExp(closer, "gi");
|
|
1654
|
+
firstRe.lastIndex = i;
|
|
1655
|
+
last = {
|
|
1656
|
+
from: i,
|
|
1657
|
+
at: firstRe.exec(input)?.index ?? -1
|
|
1658
|
+
};
|
|
1659
|
+
memo.rawClose.set(lower, last);
|
|
1660
|
+
}
|
|
1661
|
+
const first = last.at;
|
|
1662
|
+
if (first === -1) return;
|
|
1663
|
+
const atRe = new RegExp(closer, "iy");
|
|
1664
|
+
const closerAt = (k) => {
|
|
1665
|
+
atRe.lastIndex = k;
|
|
1666
|
+
return atRe.test(input);
|
|
1667
|
+
};
|
|
1668
|
+
let closeStart = findCodeEnd(input, i, {
|
|
1669
|
+
goal: "statements",
|
|
1670
|
+
stop: closerAt,
|
|
1671
|
+
stopKey: lower,
|
|
1672
|
+
cache: memo.js
|
|
1673
|
+
});
|
|
1674
|
+
if (closeStart === -1) closeStart = first;
|
|
1675
|
+
atRe.lastIndex = closeStart;
|
|
1676
|
+
const closeEnd = closeStart + atRe.exec(input)[0].length;
|
|
255
1677
|
flushText(closeStart);
|
|
256
1678
|
tokens.push({
|
|
257
1679
|
type: "macro",
|
|
@@ -262,11 +1684,46 @@ function tokenize(input) {
|
|
|
262
1684
|
i = closeEnd;
|
|
263
1685
|
textStart = closeEnd;
|
|
264
1686
|
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Push an expression token for the `{…}` block opened at `start` whose
|
|
1689
|
+
* expression starts at `exprStart`, flushing the text before it first
|
|
1690
|
+
* when `flush`. Returns false, consuming nothing, if the block is unclosed.
|
|
1691
|
+
*/
|
|
1692
|
+
function pushExpression(exprStart, start, flush, className, id) {
|
|
1693
|
+
const closeIdx = scanBalancedBrace(input, exprStart, memo);
|
|
1694
|
+
if (closeIdx === -1) return false;
|
|
1695
|
+
if (flush) flushText(start);
|
|
1696
|
+
const token = {
|
|
1697
|
+
type: "expression",
|
|
1698
|
+
expression: input.slice(exprStart, closeIdx),
|
|
1699
|
+
start,
|
|
1700
|
+
end: closeIdx + 1
|
|
1701
|
+
};
|
|
1702
|
+
if (className) token.className = className;
|
|
1703
|
+
if (id) token.id = id;
|
|
1704
|
+
tokens.push(token);
|
|
1705
|
+
i = textStart = closeIdx + 1;
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
265
1708
|
while (i < input.length) {
|
|
266
1709
|
if (input[i] === "\\") {
|
|
267
1710
|
let k = i + 1;
|
|
268
1711
|
while (input[k] === "\\") k++;
|
|
269
1712
|
const next = input[k];
|
|
1713
|
+
if (textMode && (next === "{" || next === "}")) {
|
|
1714
|
+
const escaped = (k - i) % 2 === 1;
|
|
1715
|
+
const end = escaped ? k + 1 : k;
|
|
1716
|
+
const value = "\\".repeat(k - i >> 1) + (escaped ? next : "");
|
|
1717
|
+
flushText(i);
|
|
1718
|
+
if (value) tokens.push({
|
|
1719
|
+
type: "text",
|
|
1720
|
+
value,
|
|
1721
|
+
start: i,
|
|
1722
|
+
end
|
|
1723
|
+
});
|
|
1724
|
+
i = textStart = end;
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
270
1727
|
if ((next === "{" || next === "}") && (k - i) % 2 === 1) {
|
|
271
1728
|
flushText(k - 1);
|
|
272
1729
|
tokens.push({
|
|
@@ -282,7 +1739,7 @@ function tokenize(input) {
|
|
|
282
1739
|
i = k;
|
|
283
1740
|
continue;
|
|
284
1741
|
}
|
|
285
|
-
if (input[i] === "[" && input[i + 1] === "[") {
|
|
1742
|
+
if (!textMode && input[i] === "[" && input[i + 1] === "[") {
|
|
286
1743
|
flushText(i);
|
|
287
1744
|
const start = i;
|
|
288
1745
|
i += 2;
|
|
@@ -295,23 +1752,15 @@ function tokenize(input) {
|
|
|
295
1752
|
i = parsed.endIdx;
|
|
296
1753
|
if (input[i] === " ") i++;
|
|
297
1754
|
}
|
|
298
|
-
let depth = 1;
|
|
299
1755
|
const innerStart = i;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
i += 2;
|
|
303
|
-
} else if (input[i] === "]" && input[i + 1] === "]") {
|
|
304
|
-
depth--;
|
|
305
|
-
if (depth === 0) break;
|
|
306
|
-
i += 2;
|
|
307
|
-
} else i++;
|
|
308
|
-
if (depth !== 0) {
|
|
1756
|
+
const closeIdx = scanLinkClose(input, innerStart, memo);
|
|
1757
|
+
if (closeIdx === -1) {
|
|
309
1758
|
i = start + 2;
|
|
310
1759
|
textStart = start;
|
|
311
1760
|
continue;
|
|
312
1761
|
}
|
|
313
|
-
const inner = input.slice(innerStart,
|
|
314
|
-
i
|
|
1762
|
+
const inner = input.slice(innerStart, closeIdx);
|
|
1763
|
+
i = closeIdx + 2;
|
|
315
1764
|
const { display, target } = parseLink(inner);
|
|
316
1765
|
const linkToken = {
|
|
317
1766
|
type: "link",
|
|
@@ -359,7 +1808,7 @@ function tokenize(input) {
|
|
|
359
1808
|
textStart = i;
|
|
360
1809
|
continue;
|
|
361
1810
|
}
|
|
362
|
-
const closeIdx$ = scanBalancedBrace(input, nameStart);
|
|
1811
|
+
const closeIdx$ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
363
1812
|
if (closeIdx$ !== -1) {
|
|
364
1813
|
const expression = input.slice(afterSelectors, closeIdx$);
|
|
365
1814
|
i = closeIdx$ + 1;
|
|
@@ -399,7 +1848,7 @@ function tokenize(input) {
|
|
|
399
1848
|
textStart = i;
|
|
400
1849
|
continue;
|
|
401
1850
|
}
|
|
402
|
-
const closeIdx_ = scanBalancedBrace(input, nameStart);
|
|
1851
|
+
const closeIdx_ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
403
1852
|
if (closeIdx_ !== -1) {
|
|
404
1853
|
const expression = input.slice(afterSelectors, closeIdx_);
|
|
405
1854
|
i = closeIdx_ + 1;
|
|
@@ -439,7 +1888,7 @@ function tokenize(input) {
|
|
|
439
1888
|
textStart = i;
|
|
440
1889
|
continue;
|
|
441
1890
|
}
|
|
442
|
-
const closeIdx_at = scanBalancedBrace(input, nameStart);
|
|
1891
|
+
const closeIdx_at = scanBalancedBrace(input, nameStart - 1, memo);
|
|
443
1892
|
if (closeIdx_at !== -1) {
|
|
444
1893
|
const expression = input.slice(afterSelectors, closeIdx_at);
|
|
445
1894
|
i = closeIdx_at + 1;
|
|
@@ -479,7 +1928,7 @@ function tokenize(input) {
|
|
|
479
1928
|
textStart = i;
|
|
480
1929
|
continue;
|
|
481
1930
|
}
|
|
482
|
-
const closeIdx_pct = scanBalancedBrace(input, nameStart);
|
|
1931
|
+
const closeIdx_pct = scanBalancedBrace(input, nameStart - 1, memo);
|
|
483
1932
|
if (closeIdx_pct !== -1) {
|
|
484
1933
|
const expression = input.slice(afterSelectors, closeIdx_pct);
|
|
485
1934
|
i = closeIdx_pct + 1;
|
|
@@ -499,9 +1948,10 @@ function tokenize(input) {
|
|
|
499
1948
|
textStart = start;
|
|
500
1949
|
continue;
|
|
501
1950
|
}
|
|
1951
|
+
if (EXPRESSION_START.has(charAfter) && pushExpression(afterSelectors, start, false, className, id)) continue;
|
|
502
1952
|
if (charAfter !== void 0 && /[a-zA-Z]/.test(charAfter)) {
|
|
503
1953
|
const contentStart = afterSelectors;
|
|
504
|
-
const closeIdx =
|
|
1954
|
+
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
505
1955
|
if (closeIdx === -1) {
|
|
506
1956
|
i = start + 1;
|
|
507
1957
|
textStart = start;
|
|
@@ -547,7 +1997,7 @@ function tokenize(input) {
|
|
|
547
1997
|
textStart = i;
|
|
548
1998
|
continue;
|
|
549
1999
|
}
|
|
550
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2000
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
551
2001
|
if (closeIdx !== -1) {
|
|
552
2002
|
const expression = input.slice(start + 1, closeIdx);
|
|
553
2003
|
i = closeIdx + 1;
|
|
@@ -582,7 +2032,7 @@ function tokenize(input) {
|
|
|
582
2032
|
textStart = i;
|
|
583
2033
|
continue;
|
|
584
2034
|
}
|
|
585
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2035
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
586
2036
|
if (closeIdx !== -1) {
|
|
587
2037
|
const expression = input.slice(start + 1, closeIdx);
|
|
588
2038
|
i = closeIdx + 1;
|
|
@@ -617,7 +2067,7 @@ function tokenize(input) {
|
|
|
617
2067
|
textStart = i;
|
|
618
2068
|
continue;
|
|
619
2069
|
}
|
|
620
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2070
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
621
2071
|
if (closeIdx !== -1) {
|
|
622
2072
|
const expression = input.slice(start + 1, closeIdx);
|
|
623
2073
|
i = closeIdx + 1;
|
|
@@ -652,7 +2102,7 @@ function tokenize(input) {
|
|
|
652
2102
|
textStart = i;
|
|
653
2103
|
continue;
|
|
654
2104
|
}
|
|
655
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2105
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
656
2106
|
if (closeIdx !== -1) {
|
|
657
2107
|
const expression = input.slice(start + 1, closeIdx);
|
|
658
2108
|
i = closeIdx + 1;
|
|
@@ -669,10 +2119,15 @@ function tokenize(input) {
|
|
|
669
2119
|
textStart = start;
|
|
670
2120
|
continue;
|
|
671
2121
|
}
|
|
2122
|
+
if (EXPRESSION_START.has(nextChar)) {
|
|
2123
|
+
if (pushExpression(i + 1, start, true)) continue;
|
|
2124
|
+
i++;
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
672
2127
|
if (nextChar !== void 0 && (nextChar === "/" || /[a-zA-Z]/.test(nextChar))) {
|
|
673
2128
|
flushText(i);
|
|
674
2129
|
const contentStart = i + 1;
|
|
675
|
-
const closeIdx =
|
|
2130
|
+
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
676
2131
|
if (closeIdx === -1) {
|
|
677
2132
|
i = start + 1;
|
|
678
2133
|
textStart = start;
|
|
@@ -696,7 +2151,7 @@ function tokenize(input) {
|
|
|
696
2151
|
i++;
|
|
697
2152
|
continue;
|
|
698
2153
|
}
|
|
699
|
-
if (input[i] === "<") {
|
|
2154
|
+
if (!textMode && input[i] === "<") {
|
|
700
2155
|
const start = i;
|
|
701
2156
|
let j = i + 1;
|
|
702
2157
|
const isClose = input[j] === "/";
|
|
@@ -729,8 +2184,8 @@ function tokenize(input) {
|
|
|
729
2184
|
continue;
|
|
730
2185
|
}
|
|
731
2186
|
} else {
|
|
732
|
-
const
|
|
733
|
-
j =
|
|
2187
|
+
const attrsStart = j;
|
|
2188
|
+
j = scanAttributes(input, attrsStart, memo);
|
|
734
2189
|
let isSelfClose = HTML_VOID_TAGS.has(tagLower);
|
|
735
2190
|
if (input[j] === "/") {
|
|
736
2191
|
isSelfClose = true;
|
|
@@ -742,7 +2197,7 @@ function tokenize(input) {
|
|
|
742
2197
|
tokens.push({
|
|
743
2198
|
type: "html",
|
|
744
2199
|
tag,
|
|
745
|
-
attributes:
|
|
2200
|
+
attributes: parseHtmlAttributes(input, attrsStart, memo).attributes,
|
|
746
2201
|
isClose: false,
|
|
747
2202
|
isSelfClose,
|
|
748
2203
|
start,
|
|
@@ -762,14 +2217,41 @@ function tokenize(input) {
|
|
|
762
2217
|
return tokens;
|
|
763
2218
|
}
|
|
764
2219
|
//#endregion
|
|
2220
|
+
//#region src/utils/error-message.ts
|
|
2221
|
+
/**
|
|
2222
|
+
* The message to show for a caught value. Anything can be thrown, not only
|
|
2223
|
+
* Errors: an Error gives its message, a string is its own message, and any
|
|
2224
|
+
* other value gives `fallback` when one is passed, else its string form
|
|
2225
|
+
* (never `undefined`). An empty message also gives `fallback`, or for an
|
|
2226
|
+
* Error its string form (e.g. "Error").
|
|
2227
|
+
*/
|
|
2228
|
+
function errorMessage(err, fallback) {
|
|
2229
|
+
if (err instanceof Error) return err.message || fallback || String(err);
|
|
2230
|
+
if (typeof err === "string" && err !== "") return err;
|
|
2231
|
+
return fallback ?? String(err);
|
|
2232
|
+
}
|
|
2233
|
+
//#endregion
|
|
2234
|
+
//#region src/utils/namespace.ts
|
|
2235
|
+
/** The variable name no namespace can hold. */
|
|
2236
|
+
var RESERVED_NAME = "__proto__";
|
|
2237
|
+
/** A namespace with no prototype holding the own entries of `sources`. */
|
|
2238
|
+
function createNamespace(...sources) {
|
|
2239
|
+
const ns = Object.create(null);
|
|
2240
|
+
for (const source of sources) {
|
|
2241
|
+
if (!source) continue;
|
|
2242
|
+
for (const key of Object.keys(source)) ns[key] = source[key];
|
|
2243
|
+
}
|
|
2244
|
+
return ns;
|
|
2245
|
+
}
|
|
2246
|
+
Object.freeze(createNamespace());
|
|
2247
|
+
//#endregion
|
|
765
2248
|
//#region src/story-variables.ts
|
|
766
2249
|
function declarationRegex(sigil) {
|
|
767
2250
|
const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
768
2251
|
return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
|
|
769
2252
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
var INTERP_START_RE = /^[$_@%]\w/;
|
|
2253
|
+
/** A `$name` reference with its dot path, at a `$` the lexer found. */
|
|
2254
|
+
var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
|
|
773
2255
|
/** Quoted first argument of an input macro naming a story variable. */
|
|
774
2256
|
var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
|
|
775
2257
|
var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
@@ -812,17 +2294,18 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
812
2294
|
const match = line.match(DECLARATION_RE);
|
|
813
2295
|
if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
|
|
814
2296
|
const [, name, expr] = match;
|
|
2297
|
+
if (name === "__proto__") throw new Error(`${passageName}: "${sigil}${name}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`);
|
|
815
2298
|
let value;
|
|
816
2299
|
try {
|
|
817
2300
|
value = new Function("return (" + expr + ")")();
|
|
818
2301
|
} catch (err) {
|
|
819
|
-
throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${err
|
|
2302
|
+
throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${errorMessage(err)}`);
|
|
820
2303
|
}
|
|
821
2304
|
let fieldSchema;
|
|
822
2305
|
try {
|
|
823
2306
|
fieldSchema = inferSchema(value);
|
|
824
2307
|
} catch (err) {
|
|
825
|
-
throw new Error(`${passageName}: ${err
|
|
2308
|
+
throw new Error(`${passageName}: ${errorMessage(err)}`);
|
|
826
2309
|
}
|
|
827
2310
|
schema.set(name, {
|
|
828
2311
|
...fieldSchema,
|
|
@@ -854,6 +2337,7 @@ function extractForLocals(content) {
|
|
|
854
2337
|
function validateRef(ref, schema, forLocals) {
|
|
855
2338
|
const parts = ref.split(".");
|
|
856
2339
|
const rootName = parts[0];
|
|
2340
|
+
if (rootName === "__proto__") return `"$${rootName}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`;
|
|
857
2341
|
if (forLocals.has(rootName)) return null;
|
|
858
2342
|
const rootSchema = schema.get(rootName);
|
|
859
2343
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
|
@@ -888,89 +2372,59 @@ var BUILTIN_STORE_VAR_MACROS = [
|
|
|
888
2372
|
"textarea",
|
|
889
2373
|
"textbox"
|
|
890
2374
|
];
|
|
891
|
-
|
|
892
|
-
function scanRefs(segment, onRef) {
|
|
893
|
-
for (const match of segment.matchAll(VAR_REF_RE)) onRef(match[1]);
|
|
894
|
-
}
|
|
895
|
-
/** Index of the quote closing the string opened at `start` (or the end). */
|
|
896
|
-
function findClosingQuote(code, start) {
|
|
897
|
-
const quote = code[start];
|
|
898
|
-
let i = start + 1;
|
|
899
|
-
while (i < code.length && code[i] !== quote) i += code[i] === "\\" ? 2 : 1;
|
|
900
|
-
return Math.min(i, code.length);
|
|
901
|
-
}
|
|
2375
|
+
var NO_STORE_VAR_MACROS = /* @__PURE__ */ new Set();
|
|
902
2376
|
/**
|
|
903
|
-
* Scan
|
|
904
|
-
*
|
|
905
|
-
* resolve at runtime. A bare `$word` in literal text is not a reference.
|
|
2377
|
+
* Scan the value of a code attribute (`onclick`, see isCodeAttribute) for
|
|
2378
|
+
* the `{$…}` references resolved in it; its other braces are code.
|
|
906
2379
|
*/
|
|
907
|
-
function
|
|
908
|
-
|
|
909
|
-
while (i !== -1) {
|
|
910
|
-
const next = INTERP_START_RE.test(text.slice(i + 1, i + 3)) ? scanCode(text, i + 1, onRef, true) : i + 1;
|
|
911
|
-
i = text.indexOf("{", next);
|
|
912
|
-
}
|
|
2380
|
+
function scanSigilReferences(value, onRef) {
|
|
2381
|
+
for (const part of splitSigilTemplate(value)) if ("expr" in part) scanCode(part.expr, onRef);
|
|
913
2382
|
}
|
|
914
2383
|
/**
|
|
915
|
-
* Scan
|
|
916
|
-
*
|
|
917
|
-
*
|
|
2384
|
+
* Scan literal text (string contents, HTML attribute values) for the markup
|
|
2385
|
+
* that labels and HTML attributes evaluate at runtime: `{$…}` displays,
|
|
2386
|
+
* expressions and macros, read as in passage text. A bare `$word` in
|
|
2387
|
+
* literal text is not a reference.
|
|
918
2388
|
*/
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
while (i < code.length) {
|
|
923
|
-
const ch = code[i];
|
|
924
|
-
if (ch === "\\") i += 2;
|
|
925
|
-
else if (ch === "`") {
|
|
926
|
-
scanInterpolations(code.slice(textStart, i), onRef);
|
|
927
|
-
return i + 1;
|
|
928
|
-
} else if (ch === "$" && code[i + 1] === "{") {
|
|
929
|
-
scanInterpolations(code.slice(textStart, i), onRef);
|
|
930
|
-
i = textStart = scanCode(code, i + 2, onRef, true);
|
|
931
|
-
} else i++;
|
|
932
|
-
}
|
|
933
|
-
scanInterpolations(code.slice(textStart), onRef);
|
|
934
|
-
return code.length;
|
|
2389
|
+
function scanInterpolations(text, onRef, storeVarMacros = NO_STORE_VAR_MACROS) {
|
|
2390
|
+
if (!text.includes("{")) return;
|
|
2391
|
+
collectTokenRefs(text, tokenize(text, { text: true }), storeVarMacros, onRef);
|
|
935
2392
|
}
|
|
936
2393
|
/**
|
|
937
|
-
* Report `$var` references in JavaScript code
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
2394
|
+
* Report `$var` references in JavaScript code, lexed as the expression
|
|
2395
|
+
* engine lexes it (`lexJs`): references in code, but none inside string or
|
|
2396
|
+
* regex literals or comments, nor property names (`a.$b`, `{ $b: 1 }`).
|
|
2397
|
+
* The text of string and template literals is scanned for the markup that
|
|
2398
|
+
* labels evaluate (`{$…}`); the `${…}` parts of template literals are code.
|
|
942
2399
|
*/
|
|
943
|
-
function scanCode(code,
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
2400
|
+
function scanCode(code, onRef, goal = "expression") {
|
|
2401
|
+
/** Open template literals: their text so far, null in an interpolation. */
|
|
2402
|
+
const templates = [];
|
|
2403
|
+
lexJs(code, {
|
|
2404
|
+
variable(sigil, _name, index) {
|
|
2405
|
+
if (sigil !== "$") return;
|
|
2406
|
+
VAR_PATH_RE.lastIndex = index;
|
|
2407
|
+
onRef(VAR_PATH_RE.exec(code)[1]);
|
|
2408
|
+
},
|
|
2409
|
+
literal(text, _index, nesting) {
|
|
2410
|
+
const template = templates[templates.length - 1];
|
|
2411
|
+
if (template?.nesting === nesting) if (template.text === null) template.text = "";
|
|
2412
|
+
else if (text === "`" || text === "${") {
|
|
2413
|
+
scanInterpolations(template.text, onRef);
|
|
2414
|
+
if (text === "`") templates.pop();
|
|
2415
|
+
else template.text = null;
|
|
2416
|
+
} else template.text += text;
|
|
2417
|
+
else if (text === "`") templates.push({
|
|
2418
|
+
nesting,
|
|
2419
|
+
text: ""
|
|
2420
|
+
});
|
|
2421
|
+
else if (text[0] === "\"" || text[0] === "'") {
|
|
2422
|
+
const { closed } = scanStringLiteral(text, 0);
|
|
2423
|
+
scanInterpolations(text.slice(1, closed ? -1 : void 0), onRef);
|
|
961
2424
|
}
|
|
962
|
-
segStart = i;
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
if (nested && ch === "{") depth++;
|
|
966
|
-
else if (nested && ch === "}" && depth-- === 0) {
|
|
967
|
-
scanRefs(code.slice(segStart, i), onRef);
|
|
968
|
-
return i + 1;
|
|
969
2425
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
scanRefs(code.slice(segStart), onRef);
|
|
973
|
-
return code.length;
|
|
2426
|
+
}, goal);
|
|
2427
|
+
for (const t of templates) if (t.text) scanInterpolations(t.text, onRef);
|
|
974
2428
|
}
|
|
975
2429
|
/**
|
|
976
2430
|
* Report the `$var` references a passage evaluates at runtime: `{$var}`
|
|
@@ -979,15 +2433,19 @@ function scanCode(code, start, onRef, nested = false) {
|
|
|
979
2433
|
* interpolations in HTML attributes. Prose is literal text and not scanned.
|
|
980
2434
|
*/
|
|
981
2435
|
function collectPassageRefs(content, storeVarMacros, onRef) {
|
|
982
|
-
|
|
2436
|
+
collectTokenRefs(content, tokenize(content), storeVarMacros, onRef);
|
|
2437
|
+
}
|
|
2438
|
+
/** Report the `$var` references in the tokens of `content`. */
|
|
2439
|
+
function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
|
|
983
2440
|
for (let t = 0; t < tokens.length; t++) {
|
|
984
2441
|
const token = tokens[t];
|
|
985
2442
|
if (token.type === "variable") {
|
|
986
2443
|
if (token.scope === "variable" && token.name) onRef(token.name);
|
|
987
|
-
} else if (token.type === "expression") scanCode(token.expression,
|
|
988
|
-
else if (token.type === "html") for (const value of Object.
|
|
2444
|
+
} else if (token.type === "expression") scanCode(token.expression, onRef);
|
|
2445
|
+
else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) if (isCodeAttribute(name)) scanSigilReferences(value, onRef);
|
|
2446
|
+
else scanInterpolations(value, onRef, storeVarMacros);
|
|
989
2447
|
else if (token.type === "macro" && !token.isClose) {
|
|
990
|
-
scanCode(token.rawArgs,
|
|
2448
|
+
scanCode(token.rawArgs, onRef);
|
|
991
2449
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
992
2450
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
|
|
993
2451
|
const quoted = QUOTED_VAR_ARG_RE.exec(first);
|
|
@@ -1001,7 +2459,7 @@ function collectPassageRefs(content, storeVarMacros, onRef) {
|
|
|
1001
2459
|
close++;
|
|
1002
2460
|
}
|
|
1003
2461
|
if (close < tokens.length) {
|
|
1004
|
-
scanCode(content.slice(token.end, tokens[close].start),
|
|
2462
|
+
scanCode(content.slice(token.end, tokens[close].start), onRef, "statements");
|
|
1005
2463
|
t = close;
|
|
1006
2464
|
}
|
|
1007
2465
|
}
|
|
@@ -1029,10 +2487,11 @@ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_M
|
|
|
1029
2487
|
return errors;
|
|
1030
2488
|
}
|
|
1031
2489
|
/**
|
|
1032
|
-
* Extract default values from the schema as a
|
|
2490
|
+
* Extract default values from the schema as a record without a prototype,
|
|
2491
|
+
* like the namespaces it initializes (see utils/namespace.ts).
|
|
1033
2492
|
*/
|
|
1034
2493
|
function extractDefaults(schema) {
|
|
1035
|
-
const defaults =
|
|
2494
|
+
const defaults = createNamespace();
|
|
1036
2495
|
for (const [name, varSchema] of schema) defaults[name] = varSchema.default;
|
|
1037
2496
|
return defaults;
|
|
1038
2497
|
}
|