@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
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useRef,
|
|
6
6
|
useContext,
|
|
7
7
|
} from 'preact/hooks';
|
|
8
|
-
import { useStoryStore } from '../../store';
|
|
8
|
+
import { useStoryStore, resolvePlaythroughId } from '../../store';
|
|
9
9
|
import { isSaveExport, type SaveRecord } from '../../saves/types';
|
|
10
10
|
import {
|
|
11
11
|
getSavesGrouped,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '../../saves/save-manager';
|
|
23
23
|
import { DialogCloseContext } from '../PassageDialog';
|
|
24
24
|
import { defineMacro } from '../../define-macro';
|
|
25
|
+
import { errorMessage } from '../../utils/error-message';
|
|
25
26
|
|
|
26
27
|
function relativeTime(iso: string): string {
|
|
27
28
|
const diff = Date.now() - new Date(iso).getTime();
|
|
@@ -84,15 +85,25 @@ export function SaveManagerContent() {
|
|
|
84
85
|
}
|
|
85
86
|
}, [renamingId]);
|
|
86
87
|
|
|
88
|
+
// Actions are async and may finish after the manager closed: once
|
|
89
|
+
// unmounted, no status is shown and no timer is left behind.
|
|
90
|
+
const mounted = useRef(true);
|
|
87
91
|
const statusTimer = useRef<number>();
|
|
92
|
+
const closeTimer = useRef<number>();
|
|
88
93
|
const showStatus = (text: string, type: 'success' | 'error' = 'success') => {
|
|
94
|
+
if (!mounted.current) return;
|
|
89
95
|
clearTimeout(statusTimer.current);
|
|
90
96
|
setStatus({ text, type });
|
|
91
97
|
statusTimer.current = window.setTimeout(() => setStatus(null), 3000);
|
|
92
98
|
};
|
|
93
99
|
|
|
94
100
|
useEffect(() => {
|
|
95
|
-
|
|
101
|
+
mounted.current = true;
|
|
102
|
+
return () => {
|
|
103
|
+
mounted.current = false;
|
|
104
|
+
clearTimeout(statusTimer.current);
|
|
105
|
+
clearTimeout(closeTimer.current);
|
|
106
|
+
};
|
|
96
107
|
}, []);
|
|
97
108
|
|
|
98
109
|
const toggleCollapse = (id: string) => {
|
|
@@ -106,11 +117,16 @@ export function SaveManagerContent() {
|
|
|
106
117
|
|
|
107
118
|
// Dialog saves are not slot saves, so the save hooks get no slot.
|
|
108
119
|
const handleNewSave = async () => {
|
|
109
|
-
if (!ifid
|
|
120
|
+
if (!ifid) return;
|
|
121
|
+
// The playthrough current at the click, which a restart may have
|
|
122
|
+
// switched since this render
|
|
123
|
+
const playthrough = resolvePlaythroughId();
|
|
110
124
|
try {
|
|
111
|
-
await saveWithHooks(undefined, undefined, beginSave, (payload) =>
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
|
|
126
|
+
const ptId = await playthrough;
|
|
127
|
+
if (!ptId) throw new Error('No playthrough');
|
|
128
|
+
return createSave(ifid, ptId, payload);
|
|
129
|
+
});
|
|
114
130
|
showStatus('Save created');
|
|
115
131
|
await refresh();
|
|
116
132
|
} catch {
|
|
@@ -119,9 +135,12 @@ export function SaveManagerContent() {
|
|
|
119
135
|
};
|
|
120
136
|
|
|
121
137
|
const handleOverwrite = async (saveId: string) => {
|
|
138
|
+
const playthrough = resolvePlaythroughId();
|
|
122
139
|
try {
|
|
123
140
|
await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
|
|
124
|
-
|
|
141
|
+
// Only the current playthrough's saves offer "Save Here"
|
|
142
|
+
const ptId = (await playthrough) || undefined;
|
|
143
|
+
if (!(await overwriteSave(saveId, payload, undefined, ptId))) {
|
|
125
144
|
throw new Error('Save not found');
|
|
126
145
|
}
|
|
127
146
|
});
|
|
@@ -134,10 +153,19 @@ export function SaveManagerContent() {
|
|
|
134
153
|
|
|
135
154
|
const handleLoad = async (save: SaveRecord) => {
|
|
136
155
|
try {
|
|
137
|
-
// Stored records hold serialized variables; the store expects live
|
|
138
|
-
|
|
156
|
+
// Stored records hold serialized variables; the store expects live
|
|
157
|
+
// ones. The game moves to the save's playthrough.
|
|
158
|
+
loadFromPayload(
|
|
159
|
+
deserializePayload(save.payload),
|
|
160
|
+
undefined,
|
|
161
|
+
save.meta.playthroughId,
|
|
162
|
+
);
|
|
139
163
|
showStatus('Game loaded');
|
|
140
|
-
if (closeDialog)
|
|
164
|
+
if (closeDialog) {
|
|
165
|
+
closeTimer.current = window.setTimeout(closeDialog, 500);
|
|
166
|
+
}
|
|
167
|
+
// A playthrough deleted since the list was read is recorded again
|
|
168
|
+
await refresh();
|
|
141
169
|
} catch {
|
|
142
170
|
showStatus('Failed to load save', 'error');
|
|
143
171
|
}
|
|
@@ -221,10 +249,7 @@ export function SaveManagerContent() {
|
|
|
221
249
|
showStatus('Save imported');
|
|
222
250
|
await refresh();
|
|
223
251
|
} catch (err) {
|
|
224
|
-
showStatus(
|
|
225
|
-
err instanceof Error ? err.message : 'Failed to import save',
|
|
226
|
-
'error',
|
|
227
|
-
);
|
|
252
|
+
showStatus(errorMessage(err, 'Failed to import save'), 'error');
|
|
228
253
|
}
|
|
229
254
|
};
|
|
230
255
|
|
|
@@ -47,4 +47,17 @@ defineMacro({
|
|
|
47
47
|
|
|
48
48
|
return null;
|
|
49
49
|
},
|
|
50
|
+
text({ rawArgs, branches = [] }, ctx) {
|
|
51
|
+
const switchValue = ctx.evaluate(rawArgs);
|
|
52
|
+
let defaultBranch: (typeof branches)[number] | null = null;
|
|
53
|
+
for (let i = 1; i < branches.length; i++) {
|
|
54
|
+
const branch = branches[i]!;
|
|
55
|
+
if (branch.rawArgs === '') {
|
|
56
|
+
defaultBranch = branch;
|
|
57
|
+
} else if (switchValue === ctx.evaluate(branch.rawArgs)) {
|
|
58
|
+
return ctx.renderText(branch.children);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return defaultBranch ? ctx.renderText(defaultBranch.children) : '';
|
|
62
|
+
},
|
|
50
63
|
});
|
|
@@ -1,31 +1,51 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { defineMacro } from '../../define-macro';
|
|
3
|
+
import { MacroError } from './MacroError';
|
|
3
4
|
|
|
4
5
|
defineMacro({
|
|
5
6
|
name: 'unset',
|
|
6
7
|
render({ rawArgs }, ctx) {
|
|
7
8
|
const ran = ctx.hooks.useRef(false);
|
|
9
|
+
const error = ctx.hooks.useRef<unknown>(null);
|
|
8
10
|
|
|
9
11
|
if (!ran.current) {
|
|
10
12
|
ran.current = true;
|
|
11
13
|
const state = useStoryStore.getState();
|
|
12
14
|
const name = rawArgs.trim();
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
try {
|
|
17
|
+
if (name.startsWith('$')) {
|
|
18
|
+
state.deleteVariable(name.slice(1));
|
|
19
|
+
} else if (name.startsWith('_')) {
|
|
20
|
+
state.deleteTemporary(name.slice(1));
|
|
21
|
+
} else if (name.startsWith('%')) {
|
|
22
|
+
state.deleteTransient(name.slice(1));
|
|
23
|
+
} else if (name.startsWith('@')) {
|
|
24
|
+
// Throws outside a locals scope ({for}, widget, {link}, {button})
|
|
25
|
+
ctx.update(name.slice(1), undefined);
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`{unset} expects a variable ($name, _name, %name, or @name), got "${name}"`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
error.current = err;
|
|
23
33
|
console.error(
|
|
24
|
-
`spindle: {unset
|
|
34
|
+
`spindle: Error in {unset ${rawArgs}}${ctx.sourceLocation()}:`,
|
|
35
|
+
err,
|
|
25
36
|
);
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
if (error.current) {
|
|
41
|
+
return (
|
|
42
|
+
<MacroError
|
|
43
|
+
macro="unset"
|
|
44
|
+
error={error.current}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
return null;
|
|
30
50
|
},
|
|
31
51
|
});
|
|
@@ -2,6 +2,14 @@ import { useStoryStore } from '../../store';
|
|
|
2
2
|
import { useContext } from 'preact/hooks';
|
|
3
3
|
import { LocalsValuesContext } from '../../markup/render';
|
|
4
4
|
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
5
|
+
import { RESERVED_NAME, ownValue } from '../../utils/namespace';
|
|
6
|
+
|
|
7
|
+
const SIGILS = {
|
|
8
|
+
variable: '$',
|
|
9
|
+
temporary: '_',
|
|
10
|
+
local: '@',
|
|
11
|
+
transient: '%',
|
|
12
|
+
} as const;
|
|
5
13
|
|
|
6
14
|
interface VarDisplayProps {
|
|
7
15
|
name: string;
|
|
@@ -17,19 +25,28 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
|
17
25
|
const localsValues = useContext(LocalsValuesContext);
|
|
18
26
|
const parts = name.split('.');
|
|
19
27
|
const root = parts[0]!;
|
|
28
|
+
// Own entries only, as the expression engine reads them
|
|
20
29
|
const storeValue = useStoryStore((s) =>
|
|
21
30
|
scope === 'variable'
|
|
22
|
-
? s.variables
|
|
31
|
+
? ownValue(s.variables, root)
|
|
23
32
|
: scope === 'temporary'
|
|
24
|
-
? s.temporary
|
|
33
|
+
? ownValue(s.temporary, root)
|
|
25
34
|
: scope === 'transient'
|
|
26
|
-
? s.transient
|
|
35
|
+
? ownValue(s.transient, root)
|
|
27
36
|
: undefined,
|
|
28
37
|
);
|
|
29
38
|
|
|
39
|
+
if (root === RESERVED_NAME) {
|
|
40
|
+
return (
|
|
41
|
+
<span class="error">
|
|
42
|
+
{`{${SIGILS[scope]}${name} error: "${SIGILS[scope]}${root}" cannot be used as a variable name (${RESERVED_NAME} is reserved)}`}
|
|
43
|
+
</span>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
let value: unknown;
|
|
31
48
|
if (scope === 'local') {
|
|
32
|
-
value =
|
|
49
|
+
value = ownValue(localsValues, root);
|
|
33
50
|
} else {
|
|
34
51
|
value = storeValue;
|
|
35
52
|
}
|
|
@@ -1,51 +1,100 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { addMacroTrigger, removeTrigger } from '../../triggers';
|
|
3
3
|
import type { WatchOptions } from '../../triggers';
|
|
4
|
+
import { isWhitespace, readQuoted, readWholeQuoted } from './arg-utils';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
render(props, ctx) {
|
|
8
|
-
const { hooks } = ctx;
|
|
6
|
+
const WORD_RE = /\w+/y;
|
|
7
|
+
const DIGITS_RE = /\d+/y;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
10
|
+
function matchAt(re: RegExp, src: string, pos: number): string | null {
|
|
11
|
+
re.lastIndex = pos;
|
|
12
|
+
return re.exec(src)?.[0] ?? null;
|
|
13
|
+
}
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Parse `{watch "condition" keyword value …}`: a quoted condition followed
|
|
17
|
+
* by keyword options (`goto "X"`, `dialog "X"`, `run "X"`, `name "X"`,
|
|
18
|
+
* `priority N`, `once`). Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
19
|
+
* Returns `null` when the arguments do not start with a quoted condition.
|
|
20
|
+
*/
|
|
21
|
+
export function parseWatchArgs(
|
|
22
|
+
rawArgs: string,
|
|
23
|
+
): { condition: string; options: WatchOptions } | null {
|
|
24
|
+
const raw = rawArgs.trim();
|
|
25
|
+
const cond = readQuoted(raw, 0);
|
|
26
|
+
if (!cond) return null;
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
case 'once':
|
|
44
|
-
options.once = true;
|
|
45
|
-
break;
|
|
28
|
+
const options: WatchOptions = {};
|
|
29
|
+
let i = cond.end;
|
|
30
|
+
while (i < raw.length) {
|
|
31
|
+
const key = matchAt(WORD_RE, raw, i);
|
|
32
|
+
if (!key) {
|
|
33
|
+
// Skip whitespace and stray characters, or a stray quoted string.
|
|
34
|
+
i = readQuoted(raw, i)?.end ?? i + 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
i += key.length;
|
|
38
|
+
|
|
39
|
+
// A value is a quoted string or a digit run, after whitespace.
|
|
40
|
+
let val: string | undefined;
|
|
41
|
+
let j = i;
|
|
42
|
+
while (j < raw.length && isWhitespace(raw[j]!)) j++;
|
|
43
|
+
if (j > i) {
|
|
44
|
+
const quoted = readQuoted(raw, j);
|
|
45
|
+
const digits = quoted ? null : matchAt(DIGITS_RE, raw, j);
|
|
46
|
+
if (quoted) {
|
|
47
|
+
val = quoted.value;
|
|
48
|
+
i = quoted.end;
|
|
49
|
+
} else if (digits) {
|
|
50
|
+
val = digits;
|
|
51
|
+
i = j + digits.length;
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
switch (key) {
|
|
56
|
+
case 'goto':
|
|
57
|
+
options.goto = val;
|
|
58
|
+
break;
|
|
59
|
+
case 'dialog':
|
|
60
|
+
options.dialog = val;
|
|
61
|
+
break;
|
|
62
|
+
case 'run':
|
|
63
|
+
options.run = val;
|
|
64
|
+
break;
|
|
65
|
+
case 'name':
|
|
66
|
+
options.name = val;
|
|
67
|
+
break;
|
|
68
|
+
case 'priority':
|
|
69
|
+
options.priority = val ? Number(val) : 0;
|
|
70
|
+
break;
|
|
71
|
+
case 'once':
|
|
72
|
+
options.once = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { condition: cond.value, options };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse the watcher name of `{unwatch "name"}`: a quoted string, with the
|
|
82
|
+
* same escapes as `{watch}`, or a bare name.
|
|
83
|
+
*/
|
|
84
|
+
export function parseUnwatchName(rawArgs: string): string {
|
|
85
|
+
const raw = rawArgs.trim();
|
|
86
|
+
return readWholeQuoted(raw) ?? raw.replace(/^['"]|['"]$/g, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
defineMacro({
|
|
90
|
+
name: 'watch',
|
|
91
|
+
render(props, ctx) {
|
|
92
|
+
const { hooks } = ctx;
|
|
93
|
+
|
|
94
|
+
const parsed = parseWatchArgs(props.rawArgs);
|
|
95
|
+
if (!parsed) return null;
|
|
96
|
+
const { condition, options } = parsed;
|
|
97
|
+
|
|
49
98
|
// Register during render (like {set}) — triggers survive navigation
|
|
50
99
|
// and are cleaned up via resetTriggers() on restart or {unwatch}.
|
|
51
100
|
// Remounts (revisits, re-rendered branches) keep the registered watcher.
|
|
@@ -62,8 +111,7 @@ defineMacro({
|
|
|
62
111
|
defineMacro({
|
|
63
112
|
name: 'unwatch',
|
|
64
113
|
render(props, ctx) {
|
|
65
|
-
const
|
|
66
|
-
const name = raw.replace(/^['"]|['"]$/g, '');
|
|
114
|
+
const name = parseUnwatchName(props.rawArgs);
|
|
67
115
|
|
|
68
116
|
const ran = ctx.hooks.useRef(false);
|
|
69
117
|
if (!ran.current) {
|
|
@@ -2,11 +2,14 @@ import { registerWidget } from '../../widgets/widget-registry';
|
|
|
2
2
|
import { astContainsChildren } from '../../widgets/ast-scanner';
|
|
3
3
|
import { registerBlockMacro } from '../../markup/ast';
|
|
4
4
|
import { defineMacro } from '../../define-macro';
|
|
5
|
+
import { checkVariableName } from '../../utils/namespace';
|
|
6
|
+
import { MacroError } from './MacroError';
|
|
5
7
|
|
|
6
8
|
function parseWidgetDef(rawArgs: string): { name: string; params: string[] } {
|
|
7
9
|
const tokens = rawArgs.trim().split(/\s+/);
|
|
8
10
|
const name = tokens[0]!.replace(/["']/g, '');
|
|
9
11
|
const params = tokens.slice(1).filter((t) => t.startsWith('@'));
|
|
12
|
+
for (const param of params) checkVariableName(param.slice(1), param);
|
|
10
13
|
return { name, params };
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -14,12 +17,20 @@ defineMacro({
|
|
|
14
17
|
name: 'widget',
|
|
15
18
|
block: true,
|
|
16
19
|
render({ rawArgs, children = [] }, ctx) {
|
|
17
|
-
|
|
20
|
+
let parsed: ReturnType<typeof parseWidgetDef> | undefined;
|
|
21
|
+
let error: unknown;
|
|
22
|
+
try {
|
|
23
|
+
parsed = parseWidgetDef(rawArgs);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
error = err;
|
|
26
|
+
}
|
|
27
|
+
const { name, params } = parsed ?? { name: '', params: [] };
|
|
18
28
|
|
|
19
29
|
const childrenKey = JSON.stringify(children);
|
|
20
30
|
const paramsKey = params.join(',');
|
|
21
31
|
|
|
22
32
|
ctx.hooks.useLayoutEffect(() => {
|
|
33
|
+
if (!parsed) return;
|
|
23
34
|
// Widgets whose body renders {@children} take a closing tag; register
|
|
24
35
|
// them as block macros so passages parsed later nest their content.
|
|
25
36
|
const isBlock = astContainsChildren(children);
|
|
@@ -27,6 +38,14 @@ defineMacro({
|
|
|
27
38
|
if (isBlock) registerBlockMacro(name);
|
|
28
39
|
}, [name, childrenKey, paramsKey]);
|
|
29
40
|
|
|
41
|
+
if (error) {
|
|
42
|
+
return (
|
|
43
|
+
<MacroError
|
|
44
|
+
macro="widget"
|
|
45
|
+
error={error}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
30
49
|
return null;
|
|
31
50
|
},
|
|
32
51
|
});
|