@rohal12/spindle 0.52.9 → 0.54.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/README.md +5 -1
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +6484 -2558
- package/dist/pkg/story-variables.js +4009 -940
- package/dist/pkg/tooling.js +13 -1
- package/package.json +6 -1
- package/src/class-registry.ts +252 -250
- package/src/components/App.tsx +2 -0
- package/src/components/Passage.tsx +3 -3
- package/src/components/PassageDialog.tsx +2 -4
- package/src/components/RuntimeErrors.tsx +46 -0
- package/src/components/StoryInterface.tsx +2 -4
- package/src/components/macros/Computed.tsx +5 -2
- package/src/components/macros/Do.tsx +4 -2
- package/src/components/macros/Goto.tsx +13 -3
- package/src/components/macros/Include.tsx +2 -3
- package/src/components/macros/MacroError.tsx +14 -7
- package/src/components/macros/SaveManager.tsx +2 -2
- package/src/components/macros/VarDisplay.tsx +1 -1
- package/src/components/macros/Widget.tsx +5 -33
- package/src/index.tsx +57 -37
- package/src/interpolation.ts +4 -5
- package/src/markup/ast.ts +11 -226
- package/src/markup/code-attributes.ts +3 -3
- package/src/markup/code-end.ts +121 -0
- package/src/markup/parse.ts +118 -0
- package/src/markup/render.tsx +1 -1
- package/src/markup/spindle.d.peggy.ts +24 -0
- package/src/markup/spindle.peggy +463 -0
- package/src/markup/tokens.ts +95 -0
- package/src/markup/validate.ts +219 -0
- package/src/parser.ts +19 -4
- package/src/runtime-errors.ts +65 -0
- package/src/saves/format.ts +199 -0
- package/src/saves/save-manager.ts +45 -63
- package/src/saves/types.ts +71 -88
- package/src/store.ts +167 -87
- package/src/story-init.ts +3 -5
- package/src/story-variables.ts +18 -4
- package/src/structural.ts +266 -57
- package/src/styles.css +45 -0
- package/src/tooling.ts +50 -0
- package/src/types-drift-check.ts +28 -1
- package/src/utils/object-path.ts +13 -12
- package/src/utils/stable-key.ts +2 -1
- package/src/utils/value-kinds.ts +45 -0
- package/src/widgets/widget-def.ts +60 -0
- package/types/index.d.ts +32 -8
- package/types/tooling.d.ts +42 -0
- package/src/markup/tokenizer.ts +0 -1112
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
import {
|
|
3
|
+
dismissRuntimeError,
|
|
4
|
+
getRuntimeErrors,
|
|
5
|
+
subscribeRuntimeErrors,
|
|
6
|
+
} from '../runtime-errors';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The runtime errors shown on the page (see showRuntimeError): one banner
|
|
10
|
+
* each, announced to screen readers (role="alert"), until the player
|
|
11
|
+
* dismisses it. Style them with the `spindle-error-banner` classes.
|
|
12
|
+
*/
|
|
13
|
+
export function RuntimeErrors() {
|
|
14
|
+
const [errors, setErrors] = useState(getRuntimeErrors);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setErrors(getRuntimeErrors());
|
|
17
|
+
return subscribeRuntimeErrors(() => setErrors(getRuntimeErrors()));
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
if (errors.length === 0) return null;
|
|
21
|
+
return (
|
|
22
|
+
<div class="spindle-error-banners">
|
|
23
|
+
{errors.map((e) => (
|
|
24
|
+
<div
|
|
25
|
+
key={e.id}
|
|
26
|
+
class="spindle-error-banner"
|
|
27
|
+
role="alert"
|
|
28
|
+
>
|
|
29
|
+
<span class="spindle-error-banner-context">{e.context}</span>{' '}
|
|
30
|
+
<span class="spindle-error-banner-message">{e.message}</span>
|
|
31
|
+
{e.count > 1 && (
|
|
32
|
+
<span class="spindle-error-banner-count">{` (×${e.count})`}</span>
|
|
33
|
+
)}
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
class="spindle-error-banner-dismiss"
|
|
37
|
+
aria-label="Dismiss"
|
|
38
|
+
onClick={() => dismissRuntimeError(e.id)}
|
|
39
|
+
>
|
|
40
|
+
✕
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks';
|
|
2
2
|
import { useStoryFields } from '../hooks/use-story-fields';
|
|
3
|
-
import {
|
|
4
|
-
import { buildAST } from '../markup/ast';
|
|
3
|
+
import { parseMarkup } from '../markup/parse';
|
|
5
4
|
import { renderInlineNodes, NobrContext } from '../markup/render';
|
|
6
5
|
import { errorMessage } from '../utils/error-message';
|
|
7
6
|
|
|
@@ -18,8 +17,7 @@ export function StoryInterface() {
|
|
|
18
17
|
|
|
19
18
|
const rendered = useMemo(() => {
|
|
20
19
|
try {
|
|
21
|
-
const
|
|
22
|
-
const ast = buildAST(tokens);
|
|
20
|
+
const ast = parseMarkup(markup);
|
|
23
21
|
return <>{renderInlineNodes(ast)}</>;
|
|
24
22
|
} catch (err) {
|
|
25
23
|
return (
|
|
@@ -4,6 +4,7 @@ import { readState } from '../../execute-mutation';
|
|
|
4
4
|
import { deepEqual } from '../../structural';
|
|
5
5
|
import { defineMacro } from '../../define-macro';
|
|
6
6
|
import { MacroError, logMacroError } from './MacroError';
|
|
7
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
7
8
|
import { checkVariableName } from '../../utils/namespace';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -54,13 +55,15 @@ function computeAndApply(
|
|
|
54
55
|
prevRef: { current: unknown },
|
|
55
56
|
localsUpdate: ((key: string, value: unknown) => void) | null,
|
|
56
57
|
): void {
|
|
58
|
+
// Before evaluating: the expression may navigate away from this passage
|
|
59
|
+
const location = currentSourceLocation();
|
|
57
60
|
let newValue: unknown;
|
|
58
61
|
try {
|
|
59
62
|
// In program order, also when mutation code sets this off
|
|
60
63
|
const { variables, temporary, transient } = readState();
|
|
61
64
|
newValue = evaluate(expr, variables, temporary, getLocals(), transient);
|
|
62
65
|
} catch (err) {
|
|
63
|
-
logMacroError(`computed ${rawArgs}`, err);
|
|
66
|
+
logMacroError(`computed ${rawArgs}`, err, location);
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -70,7 +73,7 @@ function computeAndApply(
|
|
|
70
73
|
try {
|
|
71
74
|
localsUpdate!(name, newValue);
|
|
72
75
|
} catch (err) {
|
|
73
|
-
logMacroError(`computed ${rawArgs}`, err);
|
|
76
|
+
logMacroError(`computed ${rawArgs}`, err, location);
|
|
74
77
|
}
|
|
75
78
|
} else {
|
|
76
79
|
const state = useStoryStore.getState();
|
|
@@ -5,14 +5,16 @@ defineMacro({
|
|
|
5
5
|
name: 'do',
|
|
6
6
|
block: true,
|
|
7
7
|
render({ children = [] }, ctx) {
|
|
8
|
-
// The
|
|
8
|
+
// The parser keeps the body verbatim as a single text node
|
|
9
9
|
const code = ctx.collectText(children);
|
|
10
10
|
|
|
11
11
|
ctx.hooks.useLayoutEffect(() => {
|
|
12
|
+
// Before the code runs: it may navigate away from this passage
|
|
13
|
+
const location = ctx.sourceLocation();
|
|
12
14
|
try {
|
|
13
15
|
ctx.mutate(code);
|
|
14
16
|
} catch (err) {
|
|
15
|
-
logMacroError('do', err);
|
|
17
|
+
logMacroError('do', err, location);
|
|
16
18
|
}
|
|
17
19
|
}, []);
|
|
18
20
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { defineMacro } from '../../define-macro';
|
|
3
3
|
import { evaluatePassageName } from './macro-args';
|
|
4
|
+
import { logMacroError } from './MacroError';
|
|
4
5
|
|
|
5
6
|
defineMacro({
|
|
6
7
|
name: 'goto',
|
|
@@ -8,9 +9,18 @@ defineMacro({
|
|
|
8
9
|
parameters: [{ name: 'passage', type: 'expression', required: true }],
|
|
9
10
|
render(_props, ctx) {
|
|
10
11
|
ctx.hooks.useLayoutEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Reported like an error in {do}: thrown out of an effect, it would
|
|
13
|
+
// abort the rest of the render's effects (e.g. the session could not
|
|
14
|
+
// be written because the state holds a function). The location is
|
|
15
|
+
// this passage's, taken before navigating away from it
|
|
16
|
+
const location = ctx.sourceLocation();
|
|
17
|
+
try {
|
|
18
|
+
useStoryStore
|
|
19
|
+
.getState()
|
|
20
|
+
.navigate(evaluatePassageName(ctx.args.passage, ctx.evaluate!));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
logMacroError('goto', err, location);
|
|
23
|
+
}
|
|
14
24
|
}, []);
|
|
15
25
|
|
|
16
26
|
return null;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { useStoryFields } from '../../hooks/use-story-fields';
|
|
3
|
-
import {
|
|
4
|
-
import { buildAST } from '../../markup/ast';
|
|
3
|
+
import { parseMarkup } from '../../markup/parse';
|
|
5
4
|
import { NobrContext } from '../../markup/render';
|
|
6
5
|
import { defineMacro } from '../../define-macro';
|
|
7
6
|
import { evaluatePassageName } from './macro-args';
|
|
@@ -28,7 +27,7 @@ defineMacro({
|
|
|
28
27
|
// included passage changes, while re-renders of the same passage keep
|
|
29
28
|
// its macros mounted (#175).
|
|
30
29
|
const ast = ctx.hooks.useMemo(
|
|
31
|
-
() => (passage ?
|
|
30
|
+
() => (passage ? parseMarkup(passage.content) : null),
|
|
32
31
|
[passage],
|
|
33
32
|
);
|
|
34
33
|
|
|
@@ -19,12 +19,18 @@ export function MacroError({
|
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Log an error in the macro `{label}` (its name, or name and arguments) at
|
|
24
|
+
* `location`, the source location of the macro's passage. Take it before
|
|
25
|
+
* running the code that failed (see currentSourceLocation): code that
|
|
26
|
+
* navigates changes the current passage.
|
|
27
|
+
*/
|
|
28
|
+
export function logMacroError(
|
|
29
|
+
label: string,
|
|
30
|
+
error: unknown,
|
|
31
|
+
location: string = currentSourceLocation(),
|
|
32
|
+
): void {
|
|
33
|
+
console.error(`spindle: Error in {${label}}${location}:`, error);
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -38,11 +44,12 @@ export function useRunOnce(macro: string, rawArgs: string, effect: () => void) {
|
|
|
38
44
|
const failure = useRef<{ error: unknown } | null>();
|
|
39
45
|
if (failure.current === undefined) {
|
|
40
46
|
failure.current = null;
|
|
47
|
+
const location = currentSourceLocation();
|
|
41
48
|
try {
|
|
42
49
|
effect();
|
|
43
50
|
} catch (error) {
|
|
44
51
|
failure.current = { error };
|
|
45
|
-
logMacroError(`${macro} ${rawArgs}`, error);
|
|
52
|
+
logMacroError(`${macro} ${rawArgs}`, error, location);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
renameSave,
|
|
18
18
|
exportSave,
|
|
19
19
|
importSave,
|
|
20
|
-
|
|
20
|
+
decodeSavePayload,
|
|
21
21
|
saveWithHooks,
|
|
22
22
|
type PlaythroughGroup,
|
|
23
23
|
} from '../../saves/save-manager';
|
|
@@ -160,7 +160,7 @@ export function SaveManagerContent() {
|
|
|
160
160
|
// Stored records hold serialized variables; the store expects live
|
|
161
161
|
// ones. The game moves to the save's playthrough.
|
|
162
162
|
loadFromPayload(
|
|
163
|
-
|
|
163
|
+
decodeSavePayload(save.payload),
|
|
164
164
|
undefined,
|
|
165
165
|
save.meta.playthroughId,
|
|
166
166
|
);
|
|
@@ -3,7 +3,7 @@ import { useContext } from 'preact/hooks';
|
|
|
3
3
|
import { LocalsValuesContext } from '../../markup/render';
|
|
4
4
|
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
5
5
|
import { ownValue, variableNameError } from '../../utils/namespace';
|
|
6
|
-
import { SCOPE_SIGILS } from '../../markup/
|
|
6
|
+
import { SCOPE_SIGILS } from '../../markup/tokens';
|
|
7
7
|
import type { VariableNode } from '../../markup/ast';
|
|
8
8
|
import { display, wrapContent } from './display';
|
|
9
9
|
|
|
@@ -4,39 +4,11 @@ import { registerBlockMacro, type ASTNode } from '../../markup/ast';
|
|
|
4
4
|
import { defineMacro } from '../../define-macro';
|
|
5
5
|
import { checkVariableName } from '../../utils/namespace';
|
|
6
6
|
import { MacroError } from './MacroError';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{ name: 'name', type: 'text', required: true },
|
|
13
|
-
{ name: 'parameters', type: 'expression' },
|
|
14
|
-
] as const;
|
|
15
|
-
|
|
16
|
-
interface WidgetDef {
|
|
17
|
-
name: string;
|
|
18
|
-
params: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The widget a {widget} definition's arguments declare: its name and its
|
|
23
|
-
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
24
|
-
* Other words, such as `$name`, are not parameters.
|
|
25
|
-
*/
|
|
26
|
-
function widgetDef({
|
|
27
|
-
name = '',
|
|
28
|
-
parameters = '',
|
|
29
|
-
}: MacroArgs<typeof WIDGET_PARAMETERS>): WidgetDef {
|
|
30
|
-
return {
|
|
31
|
-
name,
|
|
32
|
-
params: parameters.split(/\s+/).filter((word) => word.startsWith('@')),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
37
|
-
export function parseWidgetDef(rawArgs: string): WidgetDef {
|
|
38
|
-
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
39
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
WIDGET_PARAMETERS,
|
|
9
|
+
widgetDef,
|
|
10
|
+
type WidgetDef,
|
|
11
|
+
} from '../../widgets/widget-def';
|
|
40
12
|
|
|
41
13
|
/**
|
|
42
14
|
* Register the widget a definition declares, with its body. Widgets whose
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render } from 'preact';
|
|
2
2
|
import { App } from './components/App';
|
|
3
|
-
import { parseStoryData } from './parser';
|
|
3
|
+
import { parseStoryData, type StoryData } from './parser';
|
|
4
4
|
import { useStoryStore, enterRuntimePhase } from './store';
|
|
5
5
|
import {
|
|
6
6
|
installStoryAPI,
|
|
@@ -16,10 +16,17 @@ import {
|
|
|
16
16
|
validatePassages,
|
|
17
17
|
extractDefaults,
|
|
18
18
|
} from './story-variables';
|
|
19
|
-
import { getMacroRegistry } from './registry';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
19
|
+
import { getMacro, getMacroRegistry, isSubMacro } from './registry';
|
|
20
|
+
import { getWidget } from './widgets/widget-registry';
|
|
21
|
+
import {
|
|
22
|
+
formatDiagnostic,
|
|
23
|
+
validateMarkup,
|
|
24
|
+
type MarkupPassage,
|
|
25
|
+
} from './markup/validate';
|
|
26
|
+
import { blockWidgetNames, parseWidgetDef } from './widgets/widget-def';
|
|
27
|
+
import { parseMarkup } from './markup/parse';
|
|
28
|
+
import { registerBlockMacro } from './markup/ast';
|
|
29
|
+
import { registerWidgetDef } from './components/macros/Widget';
|
|
23
30
|
import { errorMessage } from './utils/error-message';
|
|
24
31
|
import type { ASTNode } from './markup/ast';
|
|
25
32
|
import './macros/register-builtins';
|
|
@@ -45,10 +52,36 @@ function renderErrors(root: HTMLElement, errors: string[]) {
|
|
|
45
52
|
root.appendChild(container);
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
/** Show the validation errors instead of the story, and stop booting. */
|
|
56
|
+
function stopWithErrors(errors: string[]): never {
|
|
57
|
+
const root = document.getElementById('root');
|
|
58
|
+
if (root) renderErrors(root, errors);
|
|
59
|
+
throw new Error(
|
|
60
|
+
`spindle: ${errors.length} validation error(s):\n${errors.join('\n')}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The markup errors (malformed markup, unknown macros) of the passages for
|
|
66
|
+
* which `only` holds, with the macros and widgets known now.
|
|
67
|
+
*/
|
|
68
|
+
function markupErrors(
|
|
69
|
+
storyData: StoryData,
|
|
70
|
+
only: (passage: MarkupPassage) => boolean,
|
|
71
|
+
): string[] {
|
|
72
|
+
return validateMarkup(storyData.passages.values(), {
|
|
73
|
+
isKnownMacro: (name) =>
|
|
74
|
+
!!getMacro(name) || isSubMacro(name) || !!getWidget(name),
|
|
75
|
+
macroNames: getMacroRegistry().map((m) => m.name),
|
|
76
|
+
only,
|
|
77
|
+
}).map(formatDiagnostic);
|
|
78
|
+
}
|
|
79
|
+
|
|
48
80
|
/**
|
|
49
81
|
* Boot Spindle in the current document: parse `<tw-storydata>`, install the
|
|
50
|
-
* `Story` API, run author JavaScript, validate `StoryVariables
|
|
51
|
-
* `StoryInit
|
|
82
|
+
* `Story` API, run author JavaScript, validate `StoryVariables` and
|
|
83
|
+
* `StoryInit`'s markup, run `StoryInit`, validate the other passages' markup
|
|
84
|
+
* (with the macros StoryInit defined), and render into `#root`. Dispatches `:storyready` when the
|
|
52
85
|
* first passage is shown. Call once per page (module state is global).
|
|
53
86
|
* The browser story format calls it from `main.tsx`.
|
|
54
87
|
*/
|
|
@@ -101,7 +134,18 @@ export function boot() {
|
|
|
101
134
|
const storeVarMacros = getMacroRegistry()
|
|
102
135
|
.filter((m) => m.storeVar)
|
|
103
136
|
.map((m) => m.name);
|
|
137
|
+
// Pass 1: Register the block widgets as block macros BEFORE any passage
|
|
138
|
+
// is parsed (validation and StoryInit included), so that passages
|
|
139
|
+
// invoking block widgets and widget bodies using other block widgets
|
|
140
|
+
// parse correctly regardless of passage or definition order.
|
|
141
|
+
for (const name of blockWidgetNames(storyData.passages.values())) {
|
|
142
|
+
registerBlockMacro(name);
|
|
143
|
+
}
|
|
144
|
+
|
|
104
145
|
const errors = validatePassages(storyData.passages, schema, storeVarMacros);
|
|
146
|
+
// StoryInit's markup must be valid before it runs; the other passages are
|
|
147
|
+
// validated once it has run, as it may define macros (see below).
|
|
148
|
+
errors.push(...markupErrors(storyData, (p) => p.name === 'StoryInit'));
|
|
105
149
|
|
|
106
150
|
// Parse StoryTransients (optional — no error if missing)
|
|
107
151
|
let transientDefaults: Record<string, unknown> = {};
|
|
@@ -124,13 +168,7 @@ export function boot() {
|
|
|
124
168
|
transientDefaults = extractDefaults(transientSchema);
|
|
125
169
|
}
|
|
126
170
|
|
|
127
|
-
if (errors.length > 0)
|
|
128
|
-
const root = document.getElementById('root');
|
|
129
|
-
if (root) renderErrors(root, errors);
|
|
130
|
-
throw new Error(
|
|
131
|
-
`spindle: ${errors.length} validation error(s):\n${errors.join('\n')}`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
171
|
+
if (errors.length > 0) stopWithErrors(errors);
|
|
134
172
|
|
|
135
173
|
defaults = extractDefaults(schema);
|
|
136
174
|
setDeclaredVariables(Object.keys(defaults), Object.keys(transientDefaults));
|
|
@@ -140,36 +178,18 @@ export function boot() {
|
|
|
140
178
|
// Enter runtime phase — handlers registered from here on are cleaned on restart
|
|
141
179
|
enterRuntimePhase();
|
|
142
180
|
|
|
143
|
-
// Pass 1: Pre-scan StoryInit and all widget passages to discover block
|
|
144
|
-
// widgets. Register them as block macros BEFORE any tokenize/buildAST
|
|
145
|
-
// calls (StoryInit's included), so that passages invoking block widgets
|
|
146
|
-
// and widget bodies using other block widgets parse correctly regardless
|
|
147
|
-
// of passage or definition order.
|
|
148
|
-
const blockWidgetPattern =
|
|
149
|
-
/\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
|
|
150
|
-
for (const [passageName, passage] of storyData.passages) {
|
|
151
|
-
if (passageName === 'StoryInit' || passage.tags.includes('widget')) {
|
|
152
|
-
let match;
|
|
153
|
-
while ((match = blockWidgetPattern.exec(passage.content)) !== null) {
|
|
154
|
-
const name = match[1]!;
|
|
155
|
-
const body = match[2]!;
|
|
156
|
-
if (/\{@children\}/.test(body)) {
|
|
157
|
-
registerBlockMacro(name);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
blockWidgetPattern.lastIndex = 0;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
181
|
// Run StoryInit, restore the session if the page was refreshed, and fire
|
|
165
182
|
// storyinit after all state is settled (defaults + StoryInit + session)
|
|
166
183
|
initializeStory(loadSession(storyData.ifid));
|
|
167
184
|
|
|
185
|
+
// Every other passage's markup, now that StoryInit may have defined macros
|
|
186
|
+
const markup = markupErrors(storyData, (p) => p.name !== 'StoryInit');
|
|
187
|
+
if (markup.length > 0) stopWithErrors(markup);
|
|
188
|
+
|
|
168
189
|
// Pass 2: Full parse and register widgets from passages tagged "widget"
|
|
169
190
|
for (const [, passage] of storyData.passages) {
|
|
170
191
|
if (passage.tags.includes('widget')) {
|
|
171
|
-
const
|
|
172
|
-
const widgetAST = buildAST(widgetTokens);
|
|
192
|
+
const widgetAST = parseMarkup(passage.content);
|
|
173
193
|
for (const node of widgetAST) {
|
|
174
194
|
if (node.type === 'macro' && node.name === 'widget' && node.rawArgs) {
|
|
175
195
|
// Read and registered as the {widget} macro does
|
package/src/interpolation.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* and macro labels resolve the inline markup of passage text (variables,
|
|
4
4
|
* expressions, macros, widgets) to a string.
|
|
5
5
|
*
|
|
6
|
-
* Such a value is parsed with the passage
|
|
6
|
+
* Such a value is parsed with the passage parser in text mode, so `{…}`
|
|
7
7
|
* means the same as in passage text, and its AST is evaluated directly to a
|
|
8
8
|
* string rather than rendered: the result is ready during the render that
|
|
9
9
|
* needs it (so it goes through the usual attribute handling, boolean
|
|
@@ -17,11 +17,10 @@ import { evaluate } from './expression';
|
|
|
17
17
|
import {
|
|
18
18
|
SCOPE_SIGILS,
|
|
19
19
|
SIGIL_SCOPES,
|
|
20
|
-
tokenize,
|
|
21
20
|
type VariableScope,
|
|
22
|
-
} from './markup/
|
|
21
|
+
} from './markup/tokens';
|
|
23
22
|
import type { Sigil } from './js-lexer';
|
|
24
|
-
import {
|
|
23
|
+
import { parseMarkup } from './markup/parse';
|
|
25
24
|
import type { ASTNode, MacroNode, TextNode } from './markup/ast';
|
|
26
25
|
import { getMacro, getMacroText, isSubMacro } from './registry';
|
|
27
26
|
import type { MacroTextContext } from './registry';
|
|
@@ -76,7 +75,7 @@ export function parseText(template: string): ParsedText {
|
|
|
76
75
|
let parsed = parseCache.get(template);
|
|
77
76
|
if (parsed === undefined) {
|
|
78
77
|
try {
|
|
79
|
-
parsed = { nodes:
|
|
78
|
+
parsed = { nodes: parseMarkup(template, { text: true }) };
|
|
80
79
|
} catch (error) {
|
|
81
80
|
parsed = { error };
|
|
82
81
|
}
|