@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
|
@@ -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,46 +1246,236 @@ 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++;
|
|
119
1294
|
const attrName = input.slice(attrStart, j);
|
|
120
1295
|
if (!attrName) break;
|
|
121
|
-
|
|
122
|
-
|
|
1296
|
+
let eqIdx = j;
|
|
1297
|
+
while (eqIdx < input.length && /\s/.test(input[eqIdx])) eqIdx++;
|
|
1298
|
+
if (input[eqIdx] === "=") {
|
|
1299
|
+
j = eqIdx + 1;
|
|
1300
|
+
while (j < input.length && /\s/.test(input[j])) j++;
|
|
123
1301
|
if (input[j] === "\"" || input[j] === "'") {
|
|
124
1302
|
const quote = input[j];
|
|
125
1303
|
j++;
|
|
126
1304
|
const valStart = j;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const closeIdx = scanBalancedBrace(input, j + 1);
|
|
130
|
-
if (closeIdx !== -1) {
|
|
131
|
-
j = closeIdx + 1;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
} else if (input[j] === quote) break;
|
|
135
|
-
j++;
|
|
136
|
-
}
|
|
137
|
-
attributes[attrName] = input.slice(valStart, j);
|
|
1305
|
+
j = scanQuotedValue(input, j, quote, isCodeAttribute(attrName), memo);
|
|
1306
|
+
add?.(attrName, input.slice(valStart, j));
|
|
138
1307
|
if (j < input.length) j++;
|
|
139
1308
|
} else {
|
|
140
1309
|
const valStart = j;
|
|
141
|
-
|
|
142
|
-
|
|
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;
|
|
143
1362
|
}
|
|
144
|
-
} else
|
|
1363
|
+
} else if (input[j] === quote) break;
|
|
1364
|
+
j++;
|
|
145
1365
|
}
|
|
1366
|
+
for (const at of passed) memo.value.set(at, j);
|
|
1367
|
+
return j;
|
|
1368
|
+
}
|
|
1369
|
+
function createScanMemo() {
|
|
146
1370
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
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()
|
|
149
1388
|
};
|
|
150
1389
|
}
|
|
151
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
|
+
/**
|
|
152
1479
|
* Skip a '…' or "…" string literal opening at i.
|
|
153
1480
|
* Returns the index just past the closing quote, or -1 if the string is
|
|
154
1481
|
* not closed on the same line (JS strings can't span lines unescaped).
|
|
@@ -166,24 +1493,6 @@ function skipQuoted(input, i) {
|
|
|
166
1493
|
return -1;
|
|
167
1494
|
}
|
|
168
1495
|
/**
|
|
169
|
-
* Skip a `…` template literal opening at i, including ${…} parts.
|
|
170
|
-
* Returns the index just past the closing backtick, or -1 if unclosed.
|
|
171
|
-
*/
|
|
172
|
-
function skipTemplate(input, i) {
|
|
173
|
-
let j = i + 1;
|
|
174
|
-
while (j < input.length) {
|
|
175
|
-
const c = input[j];
|
|
176
|
-
if (c === "\\") j += 2;
|
|
177
|
-
else if (c === "`") return j + 1;
|
|
178
|
-
else if (c === "$" && input[j + 1] === "{") {
|
|
179
|
-
const closeIdx = scanBalancedBrace(input, j + 2);
|
|
180
|
-
if (closeIdx === -1) return -1;
|
|
181
|
-
j = closeIdx + 1;
|
|
182
|
-
} else j++;
|
|
183
|
-
}
|
|
184
|
-
return -1;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
1496
|
* A quote directly after a letter/digit is an apostrophe (don't), not a
|
|
188
1497
|
* string; after a backslash it is an escaped attribute delimiter (\").
|
|
189
1498
|
*/
|
|
@@ -191,39 +1500,131 @@ var NON_STRING_QUOTE_PREFIX = /[\p{L}\p{N}_\\]/u;
|
|
|
191
1500
|
/**
|
|
192
1501
|
* Scan for the balanced closing } starting at position i (just past the {).
|
|
193
1502
|
* Braces inside string and template literals are ignored. A quote that
|
|
194
|
-
* 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.
|
|
195
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.
|
|
196
1514
|
*/
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
1560
|
continue;
|
|
209
1561
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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++;
|
|
214
1604
|
continue;
|
|
215
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
|
+
}
|
|
216
1610
|
}
|
|
217
|
-
|
|
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;
|
|
218
1618
|
}
|
|
219
|
-
return -1;
|
|
220
1619
|
}
|
|
221
1620
|
/**
|
|
222
1621
|
* Single-pass tokenizer for Twine passage content.
|
|
223
1622
|
* Recognizes: [[links]], {$variable}, {_temporary}, {macroName args}
|
|
224
1623
|
*/
|
|
225
|
-
function tokenize(input) {
|
|
1624
|
+
function tokenize(input, options = {}) {
|
|
1625
|
+
const textMode = options.text === true;
|
|
226
1626
|
const tokens = [];
|
|
1627
|
+
const memo = createScanMemo();
|
|
227
1628
|
let i = 0;
|
|
228
1629
|
let textStart = 0;
|
|
229
1630
|
function flushText(end) {
|
|
@@ -235,20 +1636,44 @@ function tokenize(input) {
|
|
|
235
1636
|
});
|
|
236
1637
|
}
|
|
237
1638
|
/**
|
|
238
|
-
* After an opening raw-body macro ({do}), emit everything up to
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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.
|
|
242
1646
|
*/
|
|
243
1647
|
function consumeRawBody(name, isClose) {
|
|
244
1648
|
const lower = name.toLowerCase();
|
|
245
1649
|
if (isClose || !RAW_BODY_MACROS.has(lower)) return;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
252
1677
|
flushText(closeStart);
|
|
253
1678
|
tokens.push({
|
|
254
1679
|
type: "macro",
|
|
@@ -259,20 +1684,62 @@ function tokenize(input) {
|
|
|
259
1684
|
i = closeEnd;
|
|
260
1685
|
textStart = closeEnd;
|
|
261
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
|
+
}
|
|
262
1708
|
while (i < input.length) {
|
|
263
|
-
if (input[i] === "\\"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1709
|
+
if (input[i] === "\\") {
|
|
1710
|
+
let k = i + 1;
|
|
1711
|
+
while (input[k] === "\\") k++;
|
|
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
|
+
}
|
|
1727
|
+
if ((next === "{" || next === "}") && (k - i) % 2 === 1) {
|
|
1728
|
+
flushText(k - 1);
|
|
1729
|
+
tokens.push({
|
|
1730
|
+
type: "text",
|
|
1731
|
+
value: next,
|
|
1732
|
+
start: k - 1,
|
|
1733
|
+
end: k + 1
|
|
1734
|
+
});
|
|
1735
|
+
i = k + 1;
|
|
1736
|
+
textStart = i;
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
i = k;
|
|
273
1740
|
continue;
|
|
274
1741
|
}
|
|
275
|
-
if (input[i] === "[" && input[i + 1] === "[") {
|
|
1742
|
+
if (!textMode && input[i] === "[" && input[i + 1] === "[") {
|
|
276
1743
|
flushText(i);
|
|
277
1744
|
const start = i;
|
|
278
1745
|
i += 2;
|
|
@@ -285,23 +1752,15 @@ function tokenize(input) {
|
|
|
285
1752
|
i = parsed.endIdx;
|
|
286
1753
|
if (input[i] === " ") i++;
|
|
287
1754
|
}
|
|
288
|
-
let depth = 1;
|
|
289
1755
|
const innerStart = i;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
i += 2;
|
|
293
|
-
} else if (input[i] === "]" && input[i + 1] === "]") {
|
|
294
|
-
depth--;
|
|
295
|
-
if (depth === 0) break;
|
|
296
|
-
i += 2;
|
|
297
|
-
} else i++;
|
|
298
|
-
if (depth !== 0) {
|
|
1756
|
+
const closeIdx = scanLinkClose(input, innerStart, memo);
|
|
1757
|
+
if (closeIdx === -1) {
|
|
299
1758
|
i = start + 2;
|
|
300
1759
|
textStart = start;
|
|
301
1760
|
continue;
|
|
302
1761
|
}
|
|
303
|
-
const inner = input.slice(innerStart,
|
|
304
|
-
i
|
|
1762
|
+
const inner = input.slice(innerStart, closeIdx);
|
|
1763
|
+
i = closeIdx + 2;
|
|
305
1764
|
const { display, target } = parseLink(inner);
|
|
306
1765
|
const linkToken = {
|
|
307
1766
|
type: "link",
|
|
@@ -349,7 +1808,7 @@ function tokenize(input) {
|
|
|
349
1808
|
textStart = i;
|
|
350
1809
|
continue;
|
|
351
1810
|
}
|
|
352
|
-
const closeIdx$ = scanBalancedBrace(input, nameStart);
|
|
1811
|
+
const closeIdx$ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
353
1812
|
if (closeIdx$ !== -1) {
|
|
354
1813
|
const expression = input.slice(afterSelectors, closeIdx$);
|
|
355
1814
|
i = closeIdx$ + 1;
|
|
@@ -389,7 +1848,7 @@ function tokenize(input) {
|
|
|
389
1848
|
textStart = i;
|
|
390
1849
|
continue;
|
|
391
1850
|
}
|
|
392
|
-
const closeIdx_ = scanBalancedBrace(input, nameStart);
|
|
1851
|
+
const closeIdx_ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
393
1852
|
if (closeIdx_ !== -1) {
|
|
394
1853
|
const expression = input.slice(afterSelectors, closeIdx_);
|
|
395
1854
|
i = closeIdx_ + 1;
|
|
@@ -429,7 +1888,7 @@ function tokenize(input) {
|
|
|
429
1888
|
textStart = i;
|
|
430
1889
|
continue;
|
|
431
1890
|
}
|
|
432
|
-
const closeIdx_at = scanBalancedBrace(input, nameStart);
|
|
1891
|
+
const closeIdx_at = scanBalancedBrace(input, nameStart - 1, memo);
|
|
433
1892
|
if (closeIdx_at !== -1) {
|
|
434
1893
|
const expression = input.slice(afterSelectors, closeIdx_at);
|
|
435
1894
|
i = closeIdx_at + 1;
|
|
@@ -469,7 +1928,7 @@ function tokenize(input) {
|
|
|
469
1928
|
textStart = i;
|
|
470
1929
|
continue;
|
|
471
1930
|
}
|
|
472
|
-
const closeIdx_pct = scanBalancedBrace(input, nameStart);
|
|
1931
|
+
const closeIdx_pct = scanBalancedBrace(input, nameStart - 1, memo);
|
|
473
1932
|
if (closeIdx_pct !== -1) {
|
|
474
1933
|
const expression = input.slice(afterSelectors, closeIdx_pct);
|
|
475
1934
|
i = closeIdx_pct + 1;
|
|
@@ -489,9 +1948,10 @@ function tokenize(input) {
|
|
|
489
1948
|
textStart = start;
|
|
490
1949
|
continue;
|
|
491
1950
|
}
|
|
1951
|
+
if (EXPRESSION_START.has(charAfter) && pushExpression(afterSelectors, start, false, className, id)) continue;
|
|
492
1952
|
if (charAfter !== void 0 && /[a-zA-Z]/.test(charAfter)) {
|
|
493
1953
|
const contentStart = afterSelectors;
|
|
494
|
-
const closeIdx =
|
|
1954
|
+
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
495
1955
|
if (closeIdx === -1) {
|
|
496
1956
|
i = start + 1;
|
|
497
1957
|
textStart = start;
|
|
@@ -537,7 +1997,7 @@ function tokenize(input) {
|
|
|
537
1997
|
textStart = i;
|
|
538
1998
|
continue;
|
|
539
1999
|
}
|
|
540
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2000
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
541
2001
|
if (closeIdx !== -1) {
|
|
542
2002
|
const expression = input.slice(start + 1, closeIdx);
|
|
543
2003
|
i = closeIdx + 1;
|
|
@@ -572,7 +2032,7 @@ function tokenize(input) {
|
|
|
572
2032
|
textStart = i;
|
|
573
2033
|
continue;
|
|
574
2034
|
}
|
|
575
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2035
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
576
2036
|
if (closeIdx !== -1) {
|
|
577
2037
|
const expression = input.slice(start + 1, closeIdx);
|
|
578
2038
|
i = closeIdx + 1;
|
|
@@ -607,7 +2067,7 @@ function tokenize(input) {
|
|
|
607
2067
|
textStart = i;
|
|
608
2068
|
continue;
|
|
609
2069
|
}
|
|
610
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2070
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
611
2071
|
if (closeIdx !== -1) {
|
|
612
2072
|
const expression = input.slice(start + 1, closeIdx);
|
|
613
2073
|
i = closeIdx + 1;
|
|
@@ -642,7 +2102,7 @@ function tokenize(input) {
|
|
|
642
2102
|
textStart = i;
|
|
643
2103
|
continue;
|
|
644
2104
|
}
|
|
645
|
-
const closeIdx = scanBalancedBrace(input, nameStart);
|
|
2105
|
+
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
646
2106
|
if (closeIdx !== -1) {
|
|
647
2107
|
const expression = input.slice(start + 1, closeIdx);
|
|
648
2108
|
i = closeIdx + 1;
|
|
@@ -659,10 +2119,15 @@ function tokenize(input) {
|
|
|
659
2119
|
textStart = start;
|
|
660
2120
|
continue;
|
|
661
2121
|
}
|
|
2122
|
+
if (EXPRESSION_START.has(nextChar)) {
|
|
2123
|
+
if (pushExpression(i + 1, start, true)) continue;
|
|
2124
|
+
i++;
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
662
2127
|
if (nextChar !== void 0 && (nextChar === "/" || /[a-zA-Z]/.test(nextChar))) {
|
|
663
2128
|
flushText(i);
|
|
664
2129
|
const contentStart = i + 1;
|
|
665
|
-
const closeIdx =
|
|
2130
|
+
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
666
2131
|
if (closeIdx === -1) {
|
|
667
2132
|
i = start + 1;
|
|
668
2133
|
textStart = start;
|
|
@@ -686,7 +2151,7 @@ function tokenize(input) {
|
|
|
686
2151
|
i++;
|
|
687
2152
|
continue;
|
|
688
2153
|
}
|
|
689
|
-
if (input[i] === "<") {
|
|
2154
|
+
if (!textMode && input[i] === "<") {
|
|
690
2155
|
const start = i;
|
|
691
2156
|
let j = i + 1;
|
|
692
2157
|
const isClose = input[j] === "/";
|
|
@@ -719,8 +2184,8 @@ function tokenize(input) {
|
|
|
719
2184
|
continue;
|
|
720
2185
|
}
|
|
721
2186
|
} else {
|
|
722
|
-
const
|
|
723
|
-
j =
|
|
2187
|
+
const attrsStart = j;
|
|
2188
|
+
j = scanAttributes(input, attrsStart, memo);
|
|
724
2189
|
let isSelfClose = HTML_VOID_TAGS.has(tagLower);
|
|
725
2190
|
if (input[j] === "/") {
|
|
726
2191
|
isSelfClose = true;
|
|
@@ -732,7 +2197,7 @@ function tokenize(input) {
|
|
|
732
2197
|
tokens.push({
|
|
733
2198
|
type: "html",
|
|
734
2199
|
tag,
|
|
735
|
-
attributes:
|
|
2200
|
+
attributes: parseHtmlAttributes(input, attrsStart, memo).attributes,
|
|
736
2201
|
isClose: false,
|
|
737
2202
|
isSelfClose,
|
|
738
2203
|
start,
|
|
@@ -752,14 +2217,41 @@ function tokenize(input) {
|
|
|
752
2217
|
return tokens;
|
|
753
2218
|
}
|
|
754
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
|
|
755
2248
|
//#region src/story-variables.ts
|
|
756
2249
|
function declarationRegex(sigil) {
|
|
757
2250
|
const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
758
2251
|
return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
|
|
759
2252
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
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;
|
|
763
2255
|
/** Quoted first argument of an input macro naming a story variable. */
|
|
764
2256
|
var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
|
|
765
2257
|
var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
@@ -802,17 +2294,18 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
802
2294
|
const match = line.match(DECLARATION_RE);
|
|
803
2295
|
if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
|
|
804
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)`);
|
|
805
2298
|
let value;
|
|
806
2299
|
try {
|
|
807
2300
|
value = new Function("return (" + expr + ")")();
|
|
808
2301
|
} catch (err) {
|
|
809
|
-
throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${err
|
|
2302
|
+
throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${errorMessage(err)}`);
|
|
810
2303
|
}
|
|
811
2304
|
let fieldSchema;
|
|
812
2305
|
try {
|
|
813
2306
|
fieldSchema = inferSchema(value);
|
|
814
2307
|
} catch (err) {
|
|
815
|
-
throw new Error(`${passageName}: ${err
|
|
2308
|
+
throw new Error(`${passageName}: ${errorMessage(err)}`);
|
|
816
2309
|
}
|
|
817
2310
|
schema.set(name, {
|
|
818
2311
|
...fieldSchema,
|
|
@@ -844,6 +2337,7 @@ function extractForLocals(content) {
|
|
|
844
2337
|
function validateRef(ref, schema, forLocals) {
|
|
845
2338
|
const parts = ref.split(".");
|
|
846
2339
|
const rootName = parts[0];
|
|
2340
|
+
if (rootName === "__proto__") return `"$${rootName}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`;
|
|
847
2341
|
if (forLocals.has(rootName)) return null;
|
|
848
2342
|
const rootSchema = schema.get(rootName);
|
|
849
2343
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
|
@@ -878,89 +2372,59 @@ var BUILTIN_STORE_VAR_MACROS = [
|
|
|
878
2372
|
"textarea",
|
|
879
2373
|
"textbox"
|
|
880
2374
|
];
|
|
881
|
-
|
|
882
|
-
function scanRefs(segment, onRef) {
|
|
883
|
-
for (const match of segment.matchAll(VAR_REF_RE)) onRef(match[1]);
|
|
884
|
-
}
|
|
885
|
-
/** Index of the quote closing the string opened at `start` (or the end). */
|
|
886
|
-
function findClosingQuote(code, start) {
|
|
887
|
-
const quote = code[start];
|
|
888
|
-
let i = start + 1;
|
|
889
|
-
while (i < code.length && code[i] !== quote) i += code[i] === "\\" ? 2 : 1;
|
|
890
|
-
return Math.min(i, code.length);
|
|
891
|
-
}
|
|
2375
|
+
var NO_STORE_VAR_MACROS = /* @__PURE__ */ new Set();
|
|
892
2376
|
/**
|
|
893
|
-
* Scan
|
|
894
|
-
*
|
|
895
|
-
* 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.
|
|
896
2379
|
*/
|
|
897
|
-
function
|
|
898
|
-
|
|
899
|
-
while (i !== -1) {
|
|
900
|
-
const next = INTERP_START_RE.test(text.slice(i + 1, i + 3)) ? scanCode(text, i + 1, onRef, true) : i + 1;
|
|
901
|
-
i = text.indexOf("{", next);
|
|
902
|
-
}
|
|
2380
|
+
function scanSigilReferences(value, onRef) {
|
|
2381
|
+
for (const part of splitSigilTemplate(value)) if ("expr" in part) scanCode(part.expr, onRef);
|
|
903
2382
|
}
|
|
904
2383
|
/**
|
|
905
|
-
* Scan
|
|
906
|
-
*
|
|
907
|
-
*
|
|
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.
|
|
908
2388
|
*/
|
|
909
|
-
function
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
while (i < code.length) {
|
|
913
|
-
const ch = code[i];
|
|
914
|
-
if (ch === "\\") i += 2;
|
|
915
|
-
else if (ch === "`") {
|
|
916
|
-
scanInterpolations(code.slice(textStart, i), onRef);
|
|
917
|
-
return i + 1;
|
|
918
|
-
} else if (ch === "$" && code[i + 1] === "{") {
|
|
919
|
-
scanInterpolations(code.slice(textStart, i), onRef);
|
|
920
|
-
i = textStart = scanCode(code, i + 2, onRef, true);
|
|
921
|
-
} else i++;
|
|
922
|
-
}
|
|
923
|
-
scanInterpolations(code.slice(textStart), onRef);
|
|
924
|
-
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);
|
|
925
2392
|
}
|
|
926
2393
|
/**
|
|
927
|
-
* Report `$var` references in JavaScript code
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
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.
|
|
932
2399
|
*/
|
|
933
|
-
function scanCode(code,
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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);
|
|
951
2424
|
}
|
|
952
|
-
segStart = i;
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
if (nested && ch === "{") depth++;
|
|
956
|
-
else if (nested && ch === "}" && depth-- === 0) {
|
|
957
|
-
scanRefs(code.slice(segStart, i), onRef);
|
|
958
|
-
return i + 1;
|
|
959
2425
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
scanRefs(code.slice(segStart), onRef);
|
|
963
|
-
return code.length;
|
|
2426
|
+
}, goal);
|
|
2427
|
+
for (const t of templates) if (t.text) scanInterpolations(t.text, onRef);
|
|
964
2428
|
}
|
|
965
2429
|
/**
|
|
966
2430
|
* Report the `$var` references a passage evaluates at runtime: `{$var}`
|
|
@@ -969,15 +2433,19 @@ function scanCode(code, start, onRef, nested = false) {
|
|
|
969
2433
|
* interpolations in HTML attributes. Prose is literal text and not scanned.
|
|
970
2434
|
*/
|
|
971
2435
|
function collectPassageRefs(content, storeVarMacros, onRef) {
|
|
972
|
-
|
|
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) {
|
|
973
2440
|
for (let t = 0; t < tokens.length; t++) {
|
|
974
2441
|
const token = tokens[t];
|
|
975
2442
|
if (token.type === "variable") {
|
|
976
2443
|
if (token.scope === "variable" && token.name) onRef(token.name);
|
|
977
|
-
} else if (token.type === "expression") scanCode(token.expression,
|
|
978
|
-
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);
|
|
979
2447
|
else if (token.type === "macro" && !token.isClose) {
|
|
980
|
-
scanCode(token.rawArgs,
|
|
2448
|
+
scanCode(token.rawArgs, onRef);
|
|
981
2449
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
982
2450
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
|
|
983
2451
|
const quoted = QUOTED_VAR_ARG_RE.exec(first);
|
|
@@ -991,7 +2459,7 @@ function collectPassageRefs(content, storeVarMacros, onRef) {
|
|
|
991
2459
|
close++;
|
|
992
2460
|
}
|
|
993
2461
|
if (close < tokens.length) {
|
|
994
|
-
scanCode(content.slice(token.end, tokens[close].start),
|
|
2462
|
+
scanCode(content.slice(token.end, tokens[close].start), onRef, "statements");
|
|
995
2463
|
t = close;
|
|
996
2464
|
}
|
|
997
2465
|
}
|
|
@@ -1019,10 +2487,11 @@ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_M
|
|
|
1019
2487
|
return errors;
|
|
1020
2488
|
}
|
|
1021
2489
|
/**
|
|
1022
|
-
* 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).
|
|
1023
2492
|
*/
|
|
1024
2493
|
function extractDefaults(schema) {
|
|
1025
|
-
const defaults =
|
|
2494
|
+
const defaults = createNamespace();
|
|
1026
2495
|
for (const [name, varSchema] of schema) defaults[name] = varSchema.default;
|
|
1027
2496
|
return defaults;
|
|
1028
2497
|
}
|