@rohal12/spindle 0.52.6 → 0.52.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +764 -735
- package/dist/pkg/macro-registry.json +18 -0
- package/dist/pkg/story-variables.js +54 -32
- package/package.json +1 -1
- package/src/action-registry.ts +9 -0
- package/src/class-registry.ts +1 -5
- package/src/components/Passage.tsx +35 -39
- package/src/components/PassageDialog.tsx +2 -2
- package/src/components/StoryInterface.tsx +2 -2
- package/src/components/macros/Checkbox.tsx +3 -8
- package/src/components/macros/Computed.tsx +1 -1
- package/src/components/macros/Cycle.tsx +3 -6
- package/src/components/macros/ExprDisplay.tsx +12 -29
- package/src/components/macros/If.tsx +35 -41
- package/src/components/macros/Include.tsx +2 -1
- package/src/components/macros/Listbox.tsx +5 -7
- package/src/components/macros/Print.tsx +3 -5
- package/src/components/macros/Radiobutton.tsx +3 -5
- package/src/components/macros/Repeat.tsx +2 -1
- package/src/components/macros/Switch.tsx +6 -13
- package/src/components/macros/Timed.tsx +14 -18
- package/src/components/macros/Type.tsx +2 -1
- package/src/components/macros/VarDisplay.tsx +12 -32
- package/src/components/macros/Widget.tsx +49 -20
- package/src/components/macros/branches.ts +18 -0
- package/src/components/macros/display.ts +19 -0
- package/src/components/macros/input-macro.ts +33 -11
- package/src/components/macros/macro-args.ts +7 -0
- package/src/components/macros/option-utils.ts +10 -7
- package/src/define-macro.ts +4 -6
- package/src/expression.ts +58 -40
- package/src/hooks/use-action.ts +7 -15
- package/src/index.tsx +5 -15
- package/src/interpolation.ts +34 -41
- package/src/js-lexer.ts +80 -45
- package/src/markup/ast.ts +4 -3
- package/src/markup/render.tsx +4 -12
- package/src/registry.ts +13 -12
- package/src/saves/save-manager.ts +18 -13
- package/src/saves/storage.ts +19 -20
- package/src/story-api.ts +30 -73
- package/src/structural.ts +1 -18
- package/src/types-drift-check.ts +37 -50
- package/src/utils/macro-names.ts +40 -0
- package/src/utils/namespace.ts +17 -0
- package/src/widgets/widget-registry.ts +5 -4
package/src/expression.ts
CHANGED
|
@@ -132,7 +132,7 @@ export function previousPassage(): Passage | undefined {
|
|
|
132
132
|
* visited()/rendered() and their has-forms, counting in the counters
|
|
133
133
|
* `counts()` returns. A missing name means the current passage.
|
|
134
134
|
*/
|
|
135
|
-
|
|
135
|
+
function countQueries(counts: () => Counts) {
|
|
136
136
|
const count = (name?: string): number =>
|
|
137
137
|
countOf(counts(), name ?? useStoryStore.getState().currentPassage);
|
|
138
138
|
return {
|
|
@@ -143,6 +143,31 @@ export function countQueries(counts: () => Counts) {
|
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The history functions of passage code and the story API: the current and
|
|
148
|
+
* previous passage, and visited()/rendered() with their has-forms, counting
|
|
149
|
+
* in the counters `visitCounts()` and `renderCounts()` return.
|
|
150
|
+
*/
|
|
151
|
+
export function historyQueries(
|
|
152
|
+
visitCounts: () => Counts,
|
|
153
|
+
renderCounts: () => Counts,
|
|
154
|
+
) {
|
|
155
|
+
const visits = countQueries(visitCounts);
|
|
156
|
+
const renders = countQueries(renderCounts);
|
|
157
|
+
return {
|
|
158
|
+
currentPassage,
|
|
159
|
+
previousPassage,
|
|
160
|
+
visited: visits.count,
|
|
161
|
+
hasVisited: visits.has,
|
|
162
|
+
hasVisitedAny: visits.any,
|
|
163
|
+
hasVisitedAll: visits.all,
|
|
164
|
+
rendered: renders.count,
|
|
165
|
+
hasRendered: renders.has,
|
|
166
|
+
hasRenderedAny: renders.any,
|
|
167
|
+
hasRenderedAll: renders.all,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
146
171
|
let cachedFns: ExpressionFns | null = null;
|
|
147
172
|
let cachedVisitCounts: Counts | null = null;
|
|
148
173
|
let cachedRenderCounts: Counts | null = null;
|
|
@@ -159,19 +184,11 @@ export function buildExpressionFns() {
|
|
|
159
184
|
return cachedFns;
|
|
160
185
|
}
|
|
161
186
|
|
|
162
|
-
const visits = countQueries(() => visitCounts);
|
|
163
|
-
const renders = countQueries(() => renderCounts);
|
|
164
187
|
cachedFns = {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
hasVisitedAny: visits.any,
|
|
170
|
-
hasVisitedAll: visits.all,
|
|
171
|
-
rendered: renders.count,
|
|
172
|
-
hasRendered: renders.has,
|
|
173
|
-
hasRenderedAny: renders.any,
|
|
174
|
-
hasRenderedAll: renders.all,
|
|
188
|
+
...historyQueries(
|
|
189
|
+
() => visitCounts,
|
|
190
|
+
() => renderCounts,
|
|
191
|
+
),
|
|
175
192
|
random,
|
|
176
193
|
randomInt,
|
|
177
194
|
};
|
|
@@ -195,21 +212,24 @@ function namespaceArg(ns: Namespace): Namespace {
|
|
|
195
212
|
return ns;
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
201
|
-
*/
|
|
202
|
-
export function evaluate(
|
|
203
|
-
expr: string,
|
|
215
|
+
/** The namespaces code runs in, as evaluate() and execute() take them. */
|
|
216
|
+
type Namespaces = [
|
|
204
217
|
variables: Namespace,
|
|
205
218
|
temporary: Namespace,
|
|
206
|
-
locals
|
|
207
|
-
transient
|
|
219
|
+
locals?: Namespace,
|
|
220
|
+
transient?: Namespace,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
/** Run compiled code in `namespaces`. */
|
|
224
|
+
function run(
|
|
225
|
+
fn: CompiledExpression,
|
|
226
|
+
...[
|
|
227
|
+
variables,
|
|
228
|
+
temporary,
|
|
229
|
+
locals = EMPTY_NAMESPACE,
|
|
230
|
+
transient = EMPTY_NAMESPACE,
|
|
231
|
+
]: Namespaces
|
|
208
232
|
): unknown {
|
|
209
|
-
const transformed = transform(expr);
|
|
210
|
-
// The line break keeps a trailing `// comment` from swallowing the `)`.
|
|
211
|
-
const body = `return (${transformed}\n);`;
|
|
212
|
-
const fn = getOrCompile(body, body);
|
|
213
233
|
return fn(
|
|
214
234
|
namespaceArg(variables),
|
|
215
235
|
namespaceArg(temporary),
|
|
@@ -219,26 +239,24 @@ export function evaluate(
|
|
|
219
239
|
);
|
|
220
240
|
}
|
|
221
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Evaluate an expression and return its value.
|
|
244
|
+
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
245
|
+
*/
|
|
246
|
+
export function evaluate(expr: string, ...namespaces: Namespaces): unknown {
|
|
247
|
+
const transformed = transform(expr);
|
|
248
|
+
// The line break keeps a trailing `// comment` from swallowing the `)`.
|
|
249
|
+
const body = `return (${transformed}\n);`;
|
|
250
|
+
return run(getOrCompile(body, body), ...namespaces);
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
/**
|
|
223
254
|
* Execute statements (no return value).
|
|
224
255
|
* e.g. execute("$health = 100; $name = 'Hero'", variables, temporary)
|
|
225
256
|
*/
|
|
226
|
-
export function execute(
|
|
227
|
-
code: string,
|
|
228
|
-
variables: Namespace,
|
|
229
|
-
temporary: Namespace,
|
|
230
|
-
locals: Namespace = EMPTY_NAMESPACE,
|
|
231
|
-
transient: Namespace = EMPTY_NAMESPACE,
|
|
232
|
-
): void {
|
|
257
|
+
export function execute(code: string, ...namespaces: Namespaces): void {
|
|
233
258
|
const transformed = transform(code, 'statements');
|
|
234
|
-
|
|
235
|
-
fn(
|
|
236
|
-
namespaceArg(variables),
|
|
237
|
-
namespaceArg(temporary),
|
|
238
|
-
namespaceArg(locals),
|
|
239
|
-
buildExpressionFns(),
|
|
240
|
-
namespaceArg(transient),
|
|
241
|
-
);
|
|
259
|
+
run(getOrCompile('exec:' + transformed, transformed), ...namespaces);
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
/**
|
package/src/hooks/use-action.ts
CHANGED
|
@@ -2,22 +2,15 @@ import { useLayoutEffect, useRef } from 'preact/hooks';
|
|
|
2
2
|
import {
|
|
3
3
|
registerAction,
|
|
4
4
|
generateActionId,
|
|
5
|
+
OPTIONAL_ACTION_FIELDS,
|
|
5
6
|
type ActionRegistration,
|
|
6
|
-
type ActionType,
|
|
7
7
|
type StoryAction,
|
|
8
8
|
} from '../action-registry';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** The action to register, and what its ID is generated from. */
|
|
11
|
+
export interface UseActionOptions extends Omit<StoryAction, 'id'> {
|
|
12
12
|
key: string;
|
|
13
13
|
authorId?: string;
|
|
14
|
-
label: string;
|
|
15
|
-
target?: string;
|
|
16
|
-
variable?: string;
|
|
17
|
-
options?: string[];
|
|
18
|
-
value?: unknown;
|
|
19
|
-
disabled?: boolean;
|
|
20
|
-
perform: (value?: unknown) => void;
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
export function useAction(opts: UseActionOptions): string {
|
|
@@ -39,11 +32,10 @@ export function useAction(opts: UseActionOptions): string {
|
|
|
39
32
|
label: opts.label,
|
|
40
33
|
perform: (...args) => performRef.current(...args),
|
|
41
34
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (opts.disabled !== undefined) action.disabled = opts.disabled;
|
|
35
|
+
for (const field of OPTIONAL_ACTION_FIELDS) {
|
|
36
|
+
if (opts[field] !== undefined)
|
|
37
|
+
Object.assign(action, { [field]: opts[field] });
|
|
38
|
+
}
|
|
47
39
|
return action;
|
|
48
40
|
};
|
|
49
41
|
|
package/src/index.tsx
CHANGED
|
@@ -19,9 +19,7 @@ import {
|
|
|
19
19
|
import { getMacroRegistry } from './registry';
|
|
20
20
|
import { tokenize } from './markup/tokenizer';
|
|
21
21
|
import { buildAST, registerBlockMacro } from './markup/ast';
|
|
22
|
-
import {
|
|
23
|
-
import { astContainsChildren } from './widgets/ast-scanner';
|
|
24
|
-
import { parseWidgetDef } from './components/macros/Widget';
|
|
22
|
+
import { parseWidgetDef, registerWidgetDef } from './components/macros/Widget';
|
|
25
23
|
import { errorMessage } from './utils/error-message';
|
|
26
24
|
import type { ASTNode } from './markup/ast';
|
|
27
25
|
import './macros/register-builtins';
|
|
@@ -174,23 +172,15 @@ export function boot() {
|
|
|
174
172
|
const widgetAST = buildAST(widgetTokens);
|
|
175
173
|
for (const node of widgetAST) {
|
|
176
174
|
if (node.type === 'macro' && node.name === 'widget' && node.rawArgs) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
'$_@',
|
|
180
|
-
);
|
|
181
|
-
const children = node.children as ASTNode[];
|
|
182
|
-
const isBlock = astContainsChildren(children);
|
|
175
|
+
// Read and registered as the {widget} macro does
|
|
176
|
+
const def = parseWidgetDef(node.rawArgs);
|
|
183
177
|
try {
|
|
184
|
-
|
|
178
|
+
registerWidgetDef(def, node.children as ASTNode[]);
|
|
185
179
|
} catch (err) {
|
|
186
180
|
// As the {widget} macro refuses it: the others still register
|
|
187
181
|
console.error(
|
|
188
|
-
`spindle: widget "${
|
|
182
|
+
`spindle: widget "${def.name}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`,
|
|
189
183
|
);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (isBlock) {
|
|
193
|
-
registerBlockMacro(widgetName);
|
|
194
184
|
}
|
|
195
185
|
}
|
|
196
186
|
}
|
package/src/interpolation.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { getMacro, getMacroText, isSubMacro } from './registry';
|
|
|
27
27
|
import type { MacroTextContext } from './registry';
|
|
28
28
|
import { getWidget } from './widgets/widget-registry';
|
|
29
29
|
import { splitArgs } from './components/macros/arg-utils';
|
|
30
|
+
import { display } from './components/macros/display';
|
|
30
31
|
import { splitSigilTemplate } from './markup/code-attributes';
|
|
31
32
|
import {
|
|
32
33
|
checkVariableName,
|
|
@@ -118,10 +119,6 @@ export function mapTextNodes(
|
|
|
118
119
|
/** Deep enough for any real nesting; stops a widget that includes itself. */
|
|
119
120
|
const MAX_DEPTH = 100;
|
|
120
121
|
|
|
121
|
-
function display(value: unknown): string {
|
|
122
|
-
return value == null ? '' : String(value);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
122
|
/**
|
|
126
123
|
* The value of a variable reference (`name` without its sigil, with an
|
|
127
124
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -154,6 +151,17 @@ function resolveVariable(
|
|
|
154
151
|
return value;
|
|
155
152
|
}
|
|
156
153
|
|
|
154
|
+
/** Evaluate an expression in `scope`. */
|
|
155
|
+
function evaluateIn(expr: string, scope: TextScope): unknown {
|
|
156
|
+
return evaluate(
|
|
157
|
+
expr,
|
|
158
|
+
scope.variables,
|
|
159
|
+
scope.temporary,
|
|
160
|
+
scope.locals,
|
|
161
|
+
scope.transient,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
157
165
|
class TextEvaluator {
|
|
158
166
|
readonly errors: TextError[] = [];
|
|
159
167
|
|
|
@@ -163,16 +171,6 @@ class TextEvaluator {
|
|
|
163
171
|
return text;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
private evaluate(expr: string, scope: TextScope): unknown {
|
|
167
|
-
return evaluate(
|
|
168
|
-
expr,
|
|
169
|
-
scope.variables,
|
|
170
|
-
scope.temporary,
|
|
171
|
-
scope.locals,
|
|
172
|
-
scope.transient,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
174
|
private node(node: ASTNode, scope: TextScope, depth: number): string {
|
|
177
175
|
switch (node.type) {
|
|
178
176
|
case 'text':
|
|
@@ -190,7 +188,7 @@ class TextEvaluator {
|
|
|
190
188
|
return this.variable(node.scope, node.name, scope);
|
|
191
189
|
case 'expression':
|
|
192
190
|
try {
|
|
193
|
-
return display(
|
|
191
|
+
return display(evaluateIn(node.expression, scope));
|
|
194
192
|
} catch (error) {
|
|
195
193
|
this.errors.push({ macro: 'expression', error });
|
|
196
194
|
return '';
|
|
@@ -255,7 +253,7 @@ class TextEvaluator {
|
|
|
255
253
|
const expr = argExprs[i];
|
|
256
254
|
if (expr !== undefined) {
|
|
257
255
|
try {
|
|
258
|
-
value =
|
|
256
|
+
value = evaluateIn(expr, scope);
|
|
259
257
|
} catch {
|
|
260
258
|
value = undefined;
|
|
261
259
|
}
|
|
@@ -279,7 +277,7 @@ class TextEvaluator {
|
|
|
279
277
|
}
|
|
280
278
|
|
|
281
279
|
const ctx: MacroTextContext = {
|
|
282
|
-
evaluate: (expr) =>
|
|
280
|
+
evaluate: (expr) => evaluateIn(expr, scope),
|
|
283
281
|
renderText: (nodes, locals) =>
|
|
284
282
|
this.nested(
|
|
285
283
|
node.name,
|
|
@@ -369,15 +367,7 @@ export function interpolateCode(
|
|
|
369
367
|
}
|
|
370
368
|
} else {
|
|
371
369
|
try {
|
|
372
|
-
text += display(
|
|
373
|
-
evaluate(
|
|
374
|
-
part.expr,
|
|
375
|
-
scope.variables,
|
|
376
|
-
scope.temporary,
|
|
377
|
-
scope.locals,
|
|
378
|
-
scope.transient,
|
|
379
|
-
),
|
|
380
|
-
);
|
|
370
|
+
text += display(evaluateIn(part.expr, scope));
|
|
381
371
|
} catch (error) {
|
|
382
372
|
errors.push({ macro: 'expression', error });
|
|
383
373
|
}
|
|
@@ -386,22 +376,28 @@ export function interpolateCode(
|
|
|
386
376
|
return { text, errors };
|
|
387
377
|
}
|
|
388
378
|
|
|
379
|
+
/** The namespaces of a TextScope, passed one by one. */
|
|
380
|
+
type ScopeArgs = [
|
|
381
|
+
variables: Record<string, unknown>,
|
|
382
|
+
temporary: Record<string, unknown>,
|
|
383
|
+
locals: Record<string, unknown>,
|
|
384
|
+
transient?: Record<string, unknown>,
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
function scopeOf(
|
|
388
|
+
...[variables, temporary, locals, transient = {}]: ScopeArgs
|
|
389
|
+
): TextScope {
|
|
390
|
+
return { variables, temporary, locals, transient };
|
|
391
|
+
}
|
|
392
|
+
|
|
389
393
|
/**
|
|
390
394
|
* Evaluate text-only markup to a string, throwing the first error met.
|
|
391
395
|
*/
|
|
392
396
|
export function interpolate(
|
|
393
397
|
template: string,
|
|
394
|
-
|
|
395
|
-
temporary: Record<string, unknown>,
|
|
396
|
-
locals: Record<string, unknown>,
|
|
397
|
-
transient: Record<string, unknown> = {},
|
|
398
|
+
...namespaces: ScopeArgs
|
|
398
399
|
): string {
|
|
399
|
-
const { text, errors } = interpolateText(template,
|
|
400
|
-
variables,
|
|
401
|
-
temporary,
|
|
402
|
-
locals,
|
|
403
|
-
transient,
|
|
404
|
-
});
|
|
400
|
+
const { text, errors } = interpolateText(template, scopeOf(...namespaces));
|
|
405
401
|
if (errors.length > 0) throw errors[0]!.error;
|
|
406
402
|
return text;
|
|
407
403
|
}
|
|
@@ -412,10 +408,7 @@ export function interpolate(
|
|
|
412
408
|
*/
|
|
413
409
|
export function interpolateExpression(
|
|
414
410
|
expr: string,
|
|
415
|
-
|
|
416
|
-
temporary: Record<string, unknown>,
|
|
417
|
-
locals: Record<string, unknown>,
|
|
418
|
-
transient: Record<string, unknown> = {},
|
|
411
|
+
...namespaces: ScopeArgs
|
|
419
412
|
): string {
|
|
420
|
-
return display(
|
|
413
|
+
return display(evaluateIn(expr, scopeOf(...namespaces)));
|
|
421
414
|
}
|
package/src/js-lexer.ts
CHANGED
|
@@ -397,6 +397,14 @@ const MALFORMED = -2;
|
|
|
397
397
|
/** A frame result: the scan stops at index `s` inside it (`STOPPED - s`). */
|
|
398
398
|
const STOPPED = -3;
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* The result of a frame that closes at `end`. `bodyStarted`: the code inside
|
|
402
|
+
* started a function or class body. That body replaced the one to come, and
|
|
403
|
+
* is gone once the frame is closed, so after the frame no body is to come —
|
|
404
|
+
* a scan that skips the frame must know, as must the frames around it.
|
|
405
|
+
*/
|
|
406
|
+
const frameEnd = (end: number, bodyStarted: boolean) => end * 2 + +bodyStarted;
|
|
407
|
+
|
|
400
408
|
/** The closers whose search for their frame a frame result may depend on. */
|
|
401
409
|
const CLOSERS = [')', ']', '}'] as const;
|
|
402
410
|
const FRAME_KINDS: readonly Frame['kind'][] = [
|
|
@@ -423,11 +431,13 @@ function frameKey(f: Frame): number | undefined {
|
|
|
423
431
|
*/
|
|
424
432
|
export interface JsScanCache {
|
|
425
433
|
/**
|
|
426
|
-
* Where each `{…}` frame, template literal or `${…}` interpolation closes
|
|
427
|
-
*
|
|
428
|
-
* code inside
|
|
429
|
-
*
|
|
430
|
-
*
|
|
434
|
+
* Where each `{…}` frame, template literal or `${…}` interpolation closes,
|
|
435
|
+
* as a frame result (`frameEnd`): its `}` or closing backtick, and whether
|
|
436
|
+
* the code inside started a function or class body; or `UNCLOSED` or
|
|
437
|
+
* `MALFORMED`. The code inside such a frame lexes the same whatever
|
|
438
|
+
* surrounds it, given where it opens and its kind — a stray `)` or `]`
|
|
439
|
+
* inside never closes it — so a later scan entering the same frame skips
|
|
440
|
+
* to its end.
|
|
431
441
|
*/
|
|
432
442
|
braces: Map<number, number>;
|
|
433
443
|
/** Look-ahead bracket matches (`ScanContext.brackets`). */
|
|
@@ -457,10 +467,8 @@ export interface JsScanCache {
|
|
|
457
467
|
stacks: Map<string, number>;
|
|
458
468
|
/**
|
|
459
469
|
* How `(…)` and `[…]` frames end, by the kind of scan and then by the frame
|
|
460
|
-
* (`parenKey`):
|
|
461
|
-
*
|
|
462
|
-
* can't follow once the frame is closed), `end * 2` otherwise, or
|
|
463
|
-
* `UNCLOSED`, `MALFORMED` or `STOPPED - s`. Unlike braces, a stray closer inside may close a frame
|
|
470
|
+
* (`parenKey`): a frame result (`frameEnd`), or `UNCLOSED`, `MALFORMED` or
|
|
471
|
+
* `STOPPED - s`. Unlike braces, a stray closer inside may close a frame
|
|
464
472
|
* around them, so the code inside lexes the same only around frames for
|
|
465
473
|
* which the closers it met find nothing to close; the key says which
|
|
466
474
|
* closers met none.
|
|
@@ -748,15 +756,34 @@ function scan(
|
|
|
748
756
|
: undefined;
|
|
749
757
|
/**
|
|
750
758
|
* How many function or class bodies to come the scan has started, and with
|
|
751
|
-
* `parens`, that count when each open frame opened. Code in a
|
|
752
|
-
*
|
|
753
|
-
* follow once the frame is closed: none is to come then.
|
|
759
|
+
* `braces` or `parens`, that count when each open frame opened. Code in a
|
|
760
|
+
* frame that starts one replaces the one to come, and the new one can't
|
|
761
|
+
* follow once the frame is closed: none is to come then. A scan skipping
|
|
762
|
+
* the frame learns this from its result (`frameEnd`, `resume`), and counts
|
|
763
|
+
* the body as started too, for the frames around it to record.
|
|
754
764
|
*/
|
|
755
765
|
let bodyStarts = 0;
|
|
756
|
-
const startsAt: number[] | undefined = parens ? [0] : undefined;
|
|
766
|
+
const startsAt: number[] | undefined = braces || parens ? [0] : undefined;
|
|
757
767
|
/** A `(…)` or `[…]` frame result found in `parens`, to skip to. */
|
|
758
768
|
let parenTo: { frame: Frame; result: number } | undefined;
|
|
759
769
|
|
|
770
|
+
/** The result of the frame at index `k`, closing at `i`. */
|
|
771
|
+
const closedAt = (k: number) =>
|
|
772
|
+
frameEnd(i, startsAt !== undefined && bodyStarts !== startsAt[k]);
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Go on after a frame an earlier scan lexed, whose result (`frameEnd`) is
|
|
776
|
+
* `result`: returns the index it closes at. A body started in it is gone
|
|
777
|
+
* after it, with the one it replaced.
|
|
778
|
+
*/
|
|
779
|
+
function resume(result: number): number {
|
|
780
|
+
if (result % 2 === 1) {
|
|
781
|
+
pendingBody = undefined;
|
|
782
|
+
bodyStarts++;
|
|
783
|
+
}
|
|
784
|
+
return Math.floor(result / 2);
|
|
785
|
+
}
|
|
786
|
+
|
|
760
787
|
/** The closer `c` looked for its frame and found index `k`. */
|
|
761
788
|
function lookedFor(c: (typeof CLOSERS)[number], k: number) {
|
|
762
789
|
if (!lowest) return;
|
|
@@ -933,8 +960,8 @@ function scan(
|
|
|
933
960
|
if (lowest) {
|
|
934
961
|
for (let j = frames.length - 1; j >= k; j--) foldLowest(j);
|
|
935
962
|
lowest.length = k;
|
|
936
|
-
startsAt!.length = k;
|
|
937
963
|
}
|
|
964
|
+
if (startsAt) startsAt.length = k;
|
|
938
965
|
frames.length = k;
|
|
939
966
|
if (stackIds) stackIds.length = k;
|
|
940
967
|
for (const list of Object.values(closers)) {
|
|
@@ -1034,6 +1061,19 @@ function scan(
|
|
|
1034
1061
|
keyNext = f.kind === 'object' || f.kind === 'class';
|
|
1035
1062
|
}
|
|
1036
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Go past frame `f` if an earlier scan lexed it: to just after it, or to
|
|
1066
|
+
* the end of the source when it never closes. Whether it did, or
|
|
1067
|
+
* MALFORMED when that scan found the code malformed.
|
|
1068
|
+
*/
|
|
1069
|
+
function skipLexed(f: Frame): boolean | typeof MALFORMED {
|
|
1070
|
+
const end = known(f);
|
|
1071
|
+
if (end === undefined) return false;
|
|
1072
|
+
if (end === MALFORMED) return MALFORMED;
|
|
1073
|
+
i = end === UNCLOSED ? src.length : resume(end) + 1;
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1037
1077
|
function openBrace() {
|
|
1038
1078
|
let f: Frame;
|
|
1039
1079
|
if (pendingBody?.depth === frames.length) {
|
|
@@ -1079,12 +1119,11 @@ function scan(
|
|
|
1079
1119
|
return;
|
|
1080
1120
|
}
|
|
1081
1121
|
const closed = frames[k]!;
|
|
1122
|
+
const result = closedAt(k);
|
|
1082
1123
|
if (lowest && c !== '}') {
|
|
1083
1124
|
// Its entry complete, with those of the frames still open in it
|
|
1084
1125
|
for (let j = frames.length - 1; j > k; j--) foldLowest(j);
|
|
1085
|
-
|
|
1086
|
-
const bodyStarted = bodyStarts !== startsAt![k];
|
|
1087
|
-
recordParen(k, i * 2 + +bodyStarted);
|
|
1126
|
+
recordParen(k, result);
|
|
1088
1127
|
}
|
|
1089
1128
|
truncate(k);
|
|
1090
1129
|
if (c === ']' && !ctx.lookahead) ctx.brackets.set(closed.open, i);
|
|
@@ -1095,7 +1134,7 @@ function scan(
|
|
|
1095
1134
|
} else if (c === ']') {
|
|
1096
1135
|
endPunct(c, false);
|
|
1097
1136
|
} else {
|
|
1098
|
-
record(closed,
|
|
1137
|
+
record(closed, result);
|
|
1099
1138
|
afterBrace(closed);
|
|
1100
1139
|
}
|
|
1101
1140
|
}
|
|
@@ -1214,24 +1253,17 @@ function scan(
|
|
|
1214
1253
|
} else if (ch === '`') {
|
|
1215
1254
|
literal(ch, i);
|
|
1216
1255
|
if (frames.length === 1) return i + 1; // the end of `lexTemplate`
|
|
1217
|
-
record(top(),
|
|
1256
|
+
record(top(), closedAt(frames.length - 1));
|
|
1218
1257
|
i++;
|
|
1219
1258
|
truncate(frames.length - 1);
|
|
1220
1259
|
endOperand();
|
|
1221
1260
|
} else if (ch === '$' && src.charAt(i + 1) === '{') {
|
|
1222
1261
|
const f = frame('expr', '}', i);
|
|
1223
1262
|
f.interpolation = true;
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
1228
|
-
break;
|
|
1229
|
-
}
|
|
1230
|
-
if (end !== undefined) {
|
|
1231
|
-
// An interpolation an earlier scan lexed: on with the text after it
|
|
1232
|
-
i = end + 1;
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1263
|
+
// An interpolation an earlier scan lexed: on with the text after it
|
|
1264
|
+
const skipped = skipLexed(f);
|
|
1265
|
+
if (skipped === MALFORMED) return malformed();
|
|
1266
|
+
if (skipped) continue;
|
|
1235
1267
|
literal('${', i);
|
|
1236
1268
|
i += 2;
|
|
1237
1269
|
nesting++;
|
|
@@ -1261,15 +1293,10 @@ function scan(
|
|
|
1261
1293
|
if (ch === '`') {
|
|
1262
1294
|
endWord();
|
|
1263
1295
|
const f = frame('template', '`', i);
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
if (end !== undefined) {
|
|
1271
|
-
// A template literal an earlier scan lexed: on after it
|
|
1272
|
-
i = end + 1;
|
|
1296
|
+
// A template literal an earlier scan lexed: on after it
|
|
1297
|
+
const skipped = skipLexed(f);
|
|
1298
|
+
if (skipped === MALFORMED) return malformed();
|
|
1299
|
+
if (skipped) {
|
|
1273
1300
|
endOperand();
|
|
1274
1301
|
continue;
|
|
1275
1302
|
}
|
|
@@ -1291,7 +1318,7 @@ function scan(
|
|
|
1291
1318
|
lookedFor('}', k);
|
|
1292
1319
|
if (frames[k]!.interpolation) {
|
|
1293
1320
|
endWord();
|
|
1294
|
-
record(frames[k]!,
|
|
1321
|
+
record(frames[k]!, closedAt(k));
|
|
1295
1322
|
truncate(k);
|
|
1296
1323
|
nesting--;
|
|
1297
1324
|
literal(ch, i);
|
|
@@ -1399,8 +1426,17 @@ function scan(
|
|
|
1399
1426
|
}
|
|
1400
1427
|
}
|
|
1401
1428
|
|
|
1402
|
-
// End of an `outer` square bracket
|
|
1403
|
-
|
|
1429
|
+
// End of an `outer` square bracket: a `]` that closes no `[` within the
|
|
1430
|
+
// innermost braces closes it, over any `(` left open in it, as it closes
|
|
1431
|
+
// a nested `[`. Look-ahead scans record where the `[`s nested in theirs
|
|
1432
|
+
// end (`ctx.brackets`), so one starting at a `[` must find the same.
|
|
1433
|
+
if (
|
|
1434
|
+
ch === outer.closer &&
|
|
1435
|
+
(frames.length === 1 ||
|
|
1436
|
+
(ch === ']' && innermost(']') === 0 && innermost('}') === 0))
|
|
1437
|
+
) {
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1404
1440
|
|
|
1405
1441
|
// Regular code character
|
|
1406
1442
|
trackCode(ch);
|
|
@@ -1413,7 +1449,7 @@ function scan(
|
|
|
1413
1449
|
i = src.length;
|
|
1414
1450
|
break;
|
|
1415
1451
|
}
|
|
1416
|
-
i = end;
|
|
1452
|
+
i = resume(end);
|
|
1417
1453
|
afterBrace(skipped);
|
|
1418
1454
|
i++;
|
|
1419
1455
|
continue;
|
|
@@ -1432,8 +1468,7 @@ function scan(
|
|
|
1432
1468
|
strict!.stopped = true;
|
|
1433
1469
|
return STOPPED - result;
|
|
1434
1470
|
}
|
|
1435
|
-
i =
|
|
1436
|
-
if (result % 2 === 1) pendingBody = undefined;
|
|
1471
|
+
i = resume(result);
|
|
1437
1472
|
if (skipped.closer === ')') {
|
|
1438
1473
|
endPunct(')', skipped.header);
|
|
1439
1474
|
stmtNext = skipped.header;
|
package/src/markup/ast.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type Token,
|
|
7
7
|
type VariableScope,
|
|
8
8
|
} from './tokenizer';
|
|
9
|
+
import { NameSet } from '../utils/macro-names';
|
|
9
10
|
|
|
10
11
|
export interface TextNode {
|
|
11
12
|
type: 'text';
|
|
@@ -51,7 +52,7 @@ export type ASTNode =
|
|
|
51
52
|
| HtmlNode;
|
|
52
53
|
|
|
53
54
|
/** Macros that require a closing tag and can contain children */
|
|
54
|
-
const BLOCK_MACROS = new
|
|
55
|
+
const BLOCK_MACROS = new NameSet([
|
|
55
56
|
'if',
|
|
56
57
|
'for',
|
|
57
58
|
'do',
|
|
@@ -70,12 +71,12 @@ const BLOCK_MACROS = new Set([
|
|
|
70
71
|
|
|
71
72
|
/** Register a custom macro as a block macro so the AST builder nests children. */
|
|
72
73
|
export function registerBlockMacro(name: string): void {
|
|
73
|
-
BLOCK_MACROS.add(name
|
|
74
|
+
BLOCK_MACROS.add(name);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/** Unregister a custom block macro (for test cleanup). */
|
|
77
78
|
export function unregisterBlockMacro(name: string): void {
|
|
78
|
-
BLOCK_MACROS.delete(name
|
|
79
|
+
BLOCK_MACROS.delete(name);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/** Map from branch macro name → required parent macro name */
|
package/src/markup/render.tsx
CHANGED
|
@@ -588,12 +588,9 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
|
588
588
|
|
|
589
589
|
function ChildrenSlot() {
|
|
590
590
|
const childrenAST = useContext(WidgetChildrenContext);
|
|
591
|
-
const
|
|
592
|
-
const inline = useContext(InlineContext);
|
|
593
|
-
const raw = useContext(RawTextContext);
|
|
594
|
-
const locals = useContext(LocalsValuesContext);
|
|
591
|
+
const renderOptions = useRenderOptions();
|
|
595
592
|
if (!childrenAST || childrenAST.length === 0) return null;
|
|
596
|
-
return <>{renderNodes(childrenAST,
|
|
593
|
+
return <>{renderNodes(childrenAST, renderOptions)}</>;
|
|
597
594
|
}
|
|
598
595
|
|
|
599
596
|
/**
|
|
@@ -669,10 +666,7 @@ function renderSingleNode(node: ASTNode): preact.ComponentChildren {
|
|
|
669
666
|
return (
|
|
670
667
|
<VarDisplay
|
|
671
668
|
key={key}
|
|
672
|
-
|
|
673
|
-
scope={node.scope}
|
|
674
|
-
className={node.className}
|
|
675
|
-
id={node.id}
|
|
669
|
+
node={node}
|
|
676
670
|
/>
|
|
677
671
|
);
|
|
678
672
|
|
|
@@ -680,9 +674,7 @@ function renderSingleNode(node: ASTNode): preact.ComponentChildren {
|
|
|
680
674
|
return (
|
|
681
675
|
<ExprDisplay
|
|
682
676
|
key={key}
|
|
683
|
-
|
|
684
|
-
className={node.className}
|
|
685
|
-
id={node.id}
|
|
677
|
+
node={node}
|
|
686
678
|
/>
|
|
687
679
|
);
|
|
688
680
|
|