@rohal12/spindle 0.52.7 → 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.
Files changed (46) hide show
  1. package/README.md +26 -0
  2. package/dist/pkg/format.js +1 -1
  3. package/dist/pkg/headless.js +710 -703
  4. package/dist/pkg/macro-registry.json +18 -0
  5. package/package.json +1 -1
  6. package/src/action-registry.ts +9 -0
  7. package/src/class-registry.ts +1 -5
  8. package/src/components/Passage.tsx +35 -39
  9. package/src/components/PassageDialog.tsx +2 -2
  10. package/src/components/StoryInterface.tsx +2 -2
  11. package/src/components/macros/Checkbox.tsx +3 -8
  12. package/src/components/macros/Computed.tsx +1 -1
  13. package/src/components/macros/Cycle.tsx +3 -6
  14. package/src/components/macros/ExprDisplay.tsx +12 -29
  15. package/src/components/macros/If.tsx +35 -41
  16. package/src/components/macros/Include.tsx +2 -1
  17. package/src/components/macros/Listbox.tsx +5 -7
  18. package/src/components/macros/Print.tsx +3 -5
  19. package/src/components/macros/Radiobutton.tsx +3 -5
  20. package/src/components/macros/Repeat.tsx +2 -1
  21. package/src/components/macros/Switch.tsx +6 -13
  22. package/src/components/macros/Timed.tsx +14 -18
  23. package/src/components/macros/Type.tsx +2 -1
  24. package/src/components/macros/VarDisplay.tsx +12 -32
  25. package/src/components/macros/Widget.tsx +49 -20
  26. package/src/components/macros/branches.ts +18 -0
  27. package/src/components/macros/display.ts +19 -0
  28. package/src/components/macros/input-macro.ts +33 -11
  29. package/src/components/macros/macro-args.ts +7 -0
  30. package/src/components/macros/option-utils.ts +10 -7
  31. package/src/define-macro.ts +4 -6
  32. package/src/expression.ts +58 -40
  33. package/src/hooks/use-action.ts +7 -15
  34. package/src/index.tsx +5 -15
  35. package/src/interpolation.ts +34 -41
  36. package/src/markup/ast.ts +4 -3
  37. package/src/markup/render.tsx +4 -12
  38. package/src/registry.ts +13 -12
  39. package/src/saves/save-manager.ts +18 -13
  40. package/src/saves/storage.ts +19 -20
  41. package/src/story-api.ts +30 -73
  42. package/src/structural.ts +1 -18
  43. package/src/types-drift-check.ts +37 -50
  44. package/src/utils/macro-names.ts +40 -0
  45. package/src/utils/namespace.ts +17 -0
  46. package/src/widgets/widget-registry.ts +5 -4
@@ -182,6 +182,13 @@
182
182
  "next"
183
183
  ],
184
184
  "interpolate": true,
185
+ "parameters": [
186
+ {
187
+ "name": "delay",
188
+ "type": "delay",
189
+ "required": true
190
+ }
191
+ ],
185
192
  "source": "builtin"
186
193
  },
187
194
  {
@@ -208,6 +215,17 @@
208
215
  "name": "widget",
209
216
  "block": true,
210
217
  "subMacros": [],
218
+ "parameters": [
219
+ {
220
+ "name": "name",
221
+ "type": "text",
222
+ "required": true
223
+ },
224
+ {
225
+ "name": "parameters",
226
+ "type": "expression"
227
+ }
228
+ ],
211
229
  "source": "builtin"
212
230
  },
213
231
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.52.7",
3
+ "version": "0.52.8",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -29,6 +29,15 @@ export interface StoryAction {
29
29
  perform: (value?: unknown) => void;
30
30
  }
31
31
 
32
+ /** The fields of a StoryAction that are left out when unset. */
33
+ export const OPTIONAL_ACTION_FIELDS = [
34
+ 'target',
35
+ 'variable',
36
+ 'options',
37
+ 'value',
38
+ 'disabled',
39
+ ] as const;
40
+
32
41
  /** A mounted control's hold on its action ID. */
33
42
  export interface ActionRegistration {
34
43
  /**
@@ -1,10 +1,6 @@
1
1
  // Class registry for preserving class instances across clone/save/load cycles.
2
2
 
3
- import { setOwn } from './structural';
4
- import { hasOwn } from './utils/namespace';
5
-
6
- // Copying and comparing story values live with the structural diff
7
- export { deepClone, deepEqual, type DeepCloneOptions } from './structural';
3
+ import { hasOwn, setOwn } from './utils/namespace';
8
4
 
9
5
  type Constructor = new (...args: any[]) => any;
10
6
 
@@ -8,7 +8,7 @@ import {
8
8
  import { tokenize } from '../markup/tokenizer';
9
9
  import { buildAST, type ASTNode } from '../markup/ast';
10
10
  import { renderNodes, NobrContext } from '../markup/render';
11
- import { useStoryStore } from '../store';
11
+ import { useStoryFields } from '../hooks/use-story-fields';
12
12
  import type { Passage as PassageData } from '../parser';
13
13
  import { sourceLocationOf } from '../utils/source-location';
14
14
  import { emitFromRender } from '../event-emitter';
@@ -40,6 +40,21 @@ export function renderPassageContent(passage: PassageData) {
40
40
  return renderNodes(ast, nobr ? { nobr: true } : undefined);
41
41
  }
42
42
 
43
+ /**
44
+ * The content of a special passage shown with every passage (PassageHeader,
45
+ * PassageFooter, PassageDone), or null. One that fails to render is logged
46
+ * and left out, so it can't take the passage down with it.
47
+ */
48
+ function renderSpecialPassage(passage: PassageData | undefined) {
49
+ if (!passage) return null;
50
+ try {
51
+ return renderPassageContent(passage);
52
+ } catch (err) {
53
+ console.error(`spindle: Error in ${passage.name}:`, err);
54
+ return null;
55
+ }
56
+ }
57
+
43
58
  interface PassageProps {
44
59
  passage: PassageData;
45
60
  dataTransition?: string;
@@ -59,7 +74,7 @@ export function Passage({
59
74
  dataTransition,
60
75
  navigationId,
61
76
  }: PassageProps) {
62
- const storyData = useStoryStore((s) => s.storyData);
77
+ const { storyData } = useStoryFields('storyData');
63
78
  const isCodePassage = CODE_PASSAGES.has(passage.name);
64
79
  const [doneReady, setDoneReady] = useState(false);
65
80
 
@@ -76,35 +91,21 @@ export function Passage({
76
91
  }
77
92
  }, [passage.content, passage.name]);
78
93
 
79
- const headerPassage = isCodePassage
80
- ? undefined
81
- : storyData?.passages.get('PassageHeader');
82
- const footerPassage = isCodePassage
83
- ? undefined
84
- : storyData?.passages.get('PassageFooter');
85
- const donePassage = isCodePassage
86
- ? undefined
87
- : storyData?.passages.get('PassageDone');
88
-
89
- const headerContent = useMemo(() => {
90
- if (!headerPassage) return null;
91
- try {
92
- return renderPassageContent(headerPassage);
93
- } catch (err) {
94
- console.error('spindle: Error in PassageHeader:', err);
95
- return null;
96
- }
97
- }, [headerPassage?.content]);
94
+ // Code passages are shown without the special passages
95
+ const special = (name: string) =>
96
+ isCodePassage ? undefined : storyData?.passages.get(name);
97
+ const headerPassage = special('PassageHeader');
98
+ const footerPassage = special('PassageFooter');
99
+ const donePassage = special('PassageDone');
98
100
 
99
- const footerContent = useMemo(() => {
100
- if (!footerPassage) return null;
101
- try {
102
- return renderPassageContent(footerPassage);
103
- } catch (err) {
104
- console.error('spindle: Error in PassageFooter:', err);
105
- return null;
106
- }
107
- }, [footerPassage?.content]);
101
+ const headerContent = useMemo(
102
+ () => renderSpecialPassage(headerPassage),
103
+ [headerPassage?.content],
104
+ );
105
+ const footerContent = useMemo(
106
+ () => renderSpecialPassage(footerPassage),
107
+ [footerPassage?.content],
108
+ );
108
109
 
109
110
  // Defer PassageDone to after DOM commit
110
111
  useEffect(() => {
@@ -112,15 +113,10 @@ export function Passage({
112
113
  return () => setDoneReady(false);
113
114
  }, [passage.name]);
114
115
 
115
- const doneContent = useMemo(() => {
116
- if (!doneReady || !donePassage) return null;
117
- try {
118
- return renderPassageContent(donePassage);
119
- } catch (err) {
120
- console.error('spindle: Error in PassageDone:', err);
121
- return null;
122
- }
123
- }, [doneReady, donePassage?.content]);
116
+ const doneContent = useMemo(
117
+ () => (doneReady ? renderSpecialPassage(donePassage) : null),
118
+ [doneReady, donePassage?.content],
119
+ );
124
120
 
125
121
  // Signal that this passage's DOM is committed (after descendants' layout
126
122
  // effects, before paint). Runs once per mount: PassageDisplay keys the
@@ -3,7 +3,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef } from 'preact/hooks';
3
3
  import { tokenize } from '../markup/tokenizer';
4
4
  import { buildAST } from '../markup/ast';
5
5
  import { renderNodes, NobrContext } from '../markup/render';
6
- import { useStoryStore } from '../store';
6
+ import { useStoryFields } from '../hooks/use-story-fields';
7
7
  import { emitFromRender } from '../event-emitter';
8
8
  import { useModalFocus } from '../hooks/use-modal-focus';
9
9
  import { errorMessage } from '../utils/error-message';
@@ -38,7 +38,7 @@ export function PassageDialog({
38
38
  onCloseRef.current = onClose;
39
39
  const stableOnClose = useCallback(() => onCloseRef.current(), []);
40
40
 
41
- const storyData = useStoryStore((s) => s.storyData);
41
+ const { storyData } = useStoryFields('storyData');
42
42
 
43
43
  const passage = passageName
44
44
  ? storyData?.passages.get(passageName)
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from 'preact/hooks';
2
- import { useStoryStore } from '../store';
2
+ import { useStoryFields } from '../hooks/use-story-fields';
3
3
  import { tokenize } from '../markup/tokenizer';
4
4
  import { buildAST } from '../markup/ast';
5
5
  import { renderInlineNodes, NobrContext } from '../markup/render';
@@ -9,7 +9,7 @@ const DEFAULT_MARKUP =
9
9
  '<header class="story-menubar">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}';
10
10
 
11
11
  export function StoryInterface() {
12
- const storyData = useStoryStore((s) => s.storyData);
12
+ const { storyData } = useStoryFields('storyData');
13
13
 
14
14
  const overridePassage = storyData?.passages.get('StoryInterface');
15
15
  const markup =
@@ -1,21 +1,16 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
2
3
 
3
4
  defineMacro({
4
5
  name: 'checkbox',
5
6
  storeVar: true,
6
- parameters: [
7
- { name: 'variable', type: 'variable', required: true },
8
- { name: 'label', type: 'text' },
9
- ],
7
+ parameters: [VARIABLE_PARAMETER, { name: 'label', type: 'text' }],
10
8
  render(_props, ctx) {
11
9
  const label = ctx.args.label ?? '';
12
10
 
13
- ctx.useAction({
11
+ useVariableAction(ctx, {
14
12
  type: 'checkbox',
15
- key: `$${ctx.varName}`,
16
- authorId: ctx.id,
17
13
  label: label || ctx.varName || '',
18
- variable: ctx.varName,
19
14
  value: !!ctx.value,
20
15
  perform: (v) => ctx.setValue!(v !== undefined ? !!v : !ctx.value),
21
16
  });
@@ -1,7 +1,7 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { evaluate } from '../../expression';
3
3
  import { readState } from '../../execute-mutation';
4
- import { deepEqual } from '../../class-registry';
4
+ import { deepEqual } from '../../structural';
5
5
  import { defineMacro } from '../../define-macro';
6
6
  import { MacroError, logMacroError } from './MacroError';
7
7
  import { checkVariableName } from '../../utils/namespace';
@@ -1,10 +1,11 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
2
3
 
3
4
  defineMacro({
4
5
  name: 'cycle',
5
6
  block: true,
6
7
  storeVar: true,
7
- parameters: [{ name: 'variable', type: 'variable', required: true }],
8
+ parameters: [VARIABLE_PARAMETER],
8
9
  render({ children = [] }, ctx) {
9
10
  const options = ctx.extractOptions(children);
10
11
 
@@ -16,14 +17,10 @@ defineMacro({
16
17
  ctx.setValue!(options[nextIndex]);
17
18
  };
18
19
 
19
- ctx.useAction({
20
+ useVariableAction(ctx, {
20
21
  type: 'cycle',
21
- key: `$${ctx.varName}`,
22
- authorId: ctx.id,
23
22
  label: ctx.value == null ? options[0] || '' : String(ctx.value),
24
- variable: ctx.varName,
25
23
  options,
26
- value: ctx.value,
27
24
  perform: (v) => {
28
25
  if (v !== undefined) {
29
26
  ctx.setValue!(v);
@@ -1,41 +1,24 @@
1
1
  import { evaluate } from '../../expression';
2
2
  import { useInterpolate } from '../../hooks/use-interpolate';
3
3
  import { useMergedLocals } from '../../hooks/use-merged-locals';
4
+ import { display, wrapContent } from './display';
5
+ import type { ExpressionNode } from '../../markup/ast';
4
6
 
5
- interface ExprDisplayProps {
6
- expression: string;
7
- className?: string;
8
- id?: string;
9
- }
10
-
11
- export function ExprDisplay({ expression, className, id }: ExprDisplayProps) {
7
+ /** An expression display: `{$hp + 1}`. */
8
+ export function ExprDisplay({ node }: { node: ExpressionNode }) {
9
+ const { expression } = node;
12
10
  const resolve = useInterpolate();
13
- className = resolve(className);
14
- id = resolve(id);
11
+ const className = resolve(node.className);
12
+ const id = resolve(node.id);
15
13
  const [variables, temporary, localsValues, transient] = useMergedLocals();
16
14
 
17
- let display: string;
15
+ let text: string;
18
16
  try {
19
- const value = evaluate(
20
- expression,
21
- variables,
22
- temporary,
23
- localsValues,
24
- transient,
17
+ text = display(
18
+ evaluate(expression, variables, temporary, localsValues, transient),
25
19
  );
26
- display = value == null ? '' : String(value);
27
20
  } catch {
28
- display = `{error: ${expression}}`;
21
+ text = `{error: ${expression}}`;
29
22
  }
30
-
31
- if (className || id)
32
- return (
33
- <span
34
- id={id}
35
- class={className}
36
- >
37
- {display}
38
- </span>
39
- );
40
- return <>{display}</>;
23
+ return wrapContent(className, id, text);
41
24
  }
@@ -1,6 +1,23 @@
1
1
  import { defineMacro } from '../../define-macro';
2
2
  import { MacroError } from './MacroError';
3
3
  import type { Branch } from '../../markup/ast';
4
+ import { selectBranch } from './branches';
5
+ import { wrapContent } from './display';
6
+
7
+ /**
8
+ * The {if}/{elseif}/{else} branch whose condition holds, else the {else}
9
+ * branch, else null: the chain ends at its first bare branch. Throws what a
10
+ * condition throws.
11
+ */
12
+ function selectIfBranch(
13
+ branches: Branch[],
14
+ evaluate: (expr: string) => unknown,
15
+ ): Branch | null {
16
+ const end = branches.findIndex((branch) => branch.rawArgs === '');
17
+ return selectBranch(end < 0 ? branches : branches.slice(0, end + 1), (expr) =>
18
+ Boolean(evaluate(expr)),
19
+ );
20
+ }
4
21
 
5
22
  defineMacro({
6
23
  name: 'if',
@@ -8,49 +25,26 @@ defineMacro({
8
25
  interpolate: true,
9
26
  merged: true,
10
27
  render({ branches = [] }, ctx) {
11
- function renderBranch(branch: Branch) {
12
- const children = ctx.renderNodes(branch.children);
13
- const cls = ctx.resolve!(branch.className);
14
- const branchId = ctx.resolve!(branch.id);
15
- if (cls || branchId)
16
- return (
17
- <span
18
- id={branchId}
19
- class={cls}
20
- >
21
- {children}
22
- </span>
23
- );
24
- return <>{children}</>;
25
- }
26
-
27
- for (const branch of branches) {
28
- if (branch.rawArgs === '') {
29
- return renderBranch(branch);
30
- }
31
-
32
- try {
33
- if (ctx.evaluate!(branch.rawArgs)) {
34
- return renderBranch(branch);
35
- }
36
- } catch (err) {
37
- return (
38
- <MacroError
39
- macro="if"
40
- error={err}
41
- />
42
- );
43
- }
28
+ let branch: Branch | null;
29
+ try {
30
+ branch = selectIfBranch(branches, ctx.evaluate!);
31
+ } catch (err) {
32
+ return (
33
+ <MacroError
34
+ macro="if"
35
+ error={err}
36
+ />
37
+ );
44
38
  }
45
-
46
- return null;
39
+ if (!branch) return null;
40
+ return wrapContent(
41
+ ctx.resolve!(branch.className),
42
+ ctx.resolve!(branch.id),
43
+ ctx.renderNodes(branch.children),
44
+ );
47
45
  },
48
46
  text({ branches = [] }, ctx) {
49
- for (const branch of branches) {
50
- if (branch.rawArgs === '' || ctx.evaluate(branch.rawArgs)) {
51
- return ctx.renderText(branch.children);
52
- }
53
- }
54
- return '';
47
+ const branch = selectIfBranch(branches, ctx.evaluate);
48
+ return branch ? ctx.renderText(branch.children) : '';
55
49
  },
56
50
  });
@@ -1,4 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
+ import { useStoryFields } from '../../hooks/use-story-fields';
2
3
  import { tokenize } from '../../markup/tokenizer';
3
4
  import { buildAST } from '../../markup/ast';
4
5
  import { NobrContext } from '../../markup/render';
@@ -16,7 +17,7 @@ defineMacro({
16
17
  { name: 'passage', type: 'expression', required: true },
17
18
  ],
18
19
  render(_props, ctx) {
19
- const storyData = useStoryStore((s) => s.storyData);
20
+ const { storyData } = useStoryFields('storyData');
20
21
 
21
22
  const { inline } = ctx.args;
22
23
  const passageName = evaluatePassageName(ctx.args.passage, ctx.evaluate!);
@@ -1,21 +1,19 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
3
+ import { display } from './display';
2
4
 
3
5
  defineMacro({
4
6
  name: 'listbox',
5
7
  subMacros: ['option'],
6
8
  storeVar: true,
7
- parameters: [{ name: 'variable', type: 'variable', required: true }],
9
+ parameters: [VARIABLE_PARAMETER],
8
10
  render({ children = [] }, ctx) {
9
11
  const options = ctx.extractOptions(children);
10
12
 
11
- ctx.useAction({
13
+ useVariableAction(ctx, {
12
14
  type: 'listbox',
13
- key: `$${ctx.varName}`,
14
- authorId: ctx.id,
15
15
  label: ctx.varName!,
16
- variable: ctx.varName,
17
16
  options,
18
- value: ctx.value,
19
17
  perform: (v) => {
20
18
  if (v !== undefined) ctx.setValue!(String(v));
21
19
  },
@@ -25,7 +23,7 @@ defineMacro({
25
23
  <select
26
24
  id={ctx.id}
27
25
  class={ctx.cls}
28
- value={ctx.value == null ? '' : String(ctx.value)}
26
+ value={display(ctx.value)}
29
27
  onChange={(e) => ctx.setValue!((e.target as HTMLSelectElement).value)}
30
28
  >
31
29
  {options.map((opt) => (
@@ -1,5 +1,6 @@
1
1
  import { defineMacro } from '../../define-macro';
2
2
  import { MacroError } from './MacroError';
3
+ import { display } from './display';
3
4
 
4
5
  defineMacro({
5
6
  name: 'print',
@@ -8,9 +9,7 @@ defineMacro({
8
9
  merged: true,
9
10
  render({ rawArgs }, ctx) {
10
11
  try {
11
- const result = ctx.evaluate!(rawArgs);
12
- const display = result == null ? '' : String(result);
13
- return ctx.wrap(display);
12
+ return ctx.wrap(display(ctx.evaluate!(rawArgs)));
14
13
  } catch (err) {
15
14
  return (
16
15
  <MacroError
@@ -21,7 +20,6 @@ defineMacro({
21
20
  }
22
21
  },
23
22
  text({ rawArgs }, ctx) {
24
- const result = ctx.evaluate(rawArgs);
25
- return result == null ? '' : String(result);
23
+ return display(ctx.evaluate(rawArgs));
26
24
  },
27
25
  });
@@ -1,10 +1,11 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
2
3
 
3
4
  defineMacro({
4
5
  name: 'radiobutton',
5
6
  storeVar: true,
6
7
  parameters: [
7
- { name: 'variable', type: 'variable', required: true },
8
+ VARIABLE_PARAMETER,
8
9
  { name: 'value', type: 'text', required: true },
9
10
  { name: 'label', type: 'text' },
10
11
  ],
@@ -12,13 +13,10 @@ defineMacro({
12
13
  const radioValue = ctx.args.value ?? '';
13
14
  const label = ctx.args.label ?? '';
14
15
 
15
- ctx.useAction({
16
+ useVariableAction(ctx, {
16
17
  type: 'radiobutton',
17
18
  key: `$${ctx.varName}:${radioValue}`,
18
- authorId: ctx.id,
19
19
  label: label || radioValue,
20
- variable: ctx.varName,
21
- value: ctx.value,
22
20
  perform: () => ctx.setValue!(radioValue),
23
21
  });
24
22
 
@@ -1,5 +1,6 @@
1
1
  import { createContext } from 'preact';
2
2
  import { defineMacro } from '../../define-macro';
3
+ import { DELAY_PARAMETER } from './macro-args';
3
4
 
4
5
  export const RepeatContext = createContext<{ stop: () => void }>({
5
6
  stop: () => {},
@@ -9,7 +10,7 @@ defineMacro({
9
10
  name: 'repeat',
10
11
  block: true,
11
12
  interpolate: true,
12
- parameters: [{ name: 'delay', type: 'delay', required: true }],
13
+ parameters: [DELAY_PARAMETER],
13
14
  render({ children = [] }, ctx) {
14
15
  const { useState, useEffect, useCallback } = ctx.hooks;
15
16
 
@@ -1,26 +1,19 @@
1
1
  import { defineMacro } from '../../define-macro';
2
2
  import { MacroError } from './MacroError';
3
3
  import type { Branch } from '../../markup/ast';
4
+ import { selectBranch } from './branches';
4
5
 
5
6
  /**
6
7
  * The first {case} branch whose value equals `value`, else the {default}
7
8
  * branch, else null. Throws what a case expression throws.
8
9
  */
9
- function selectBranch(
10
+ function selectCase(
10
11
  value: unknown,
11
12
  branches: Branch[],
12
13
  evaluate: (expr: string) => unknown,
13
14
  ): Branch | null {
14
- let defaultBranch: Branch | null = null;
15
- for (let i = 1; i < branches.length; i++) {
16
- const branch = branches[i]!;
17
- if (branch.rawArgs === '') {
18
- defaultBranch = branch;
19
- } else if (value === evaluate(branch.rawArgs)) {
20
- return branch;
21
- }
22
- }
23
- return defaultBranch;
15
+ // The first branch holds the content before the first {case}
16
+ return selectBranch(branches.slice(1), (expr) => value === evaluate(expr));
24
17
  }
25
18
 
26
19
  defineMacro({
@@ -43,7 +36,7 @@ defineMacro({
43
36
 
44
37
  let branch: Branch | null;
45
38
  try {
46
- branch = selectBranch(switchValue, branches, ctx.evaluate!);
39
+ branch = selectCase(switchValue, branches, ctx.evaluate!);
47
40
  } catch (err) {
48
41
  return (
49
42
  <MacroError
@@ -56,7 +49,7 @@ defineMacro({
56
49
  return branch && <>{ctx.renderNodes(branch.children)}</>;
57
50
  },
58
51
  text({ rawArgs, branches = [] }, ctx) {
59
- const branch = selectBranch(ctx.evaluate(rawArgs), branches, ctx.evaluate);
52
+ const branch = selectCase(ctx.evaluate(rawArgs), branches, ctx.evaluate);
60
53
  return branch ? ctx.renderText(branch.children) : '';
61
54
  },
62
55
  });
@@ -1,16 +1,21 @@
1
- import { parseDelay } from '../../utils/parse-delay';
2
1
  import { defineMacro } from '../../define-macro';
2
+ import { DELAY_PARAMETER, parseMacroArgs } from './macro-args';
3
+ import { wrapContent } from './display';
4
+
5
+ /** The parameters of {timed} and of each {next}: the delay before it shows. */
6
+ const TIMED_PARAMETERS = [DELAY_PARAMETER] as const;
3
7
 
4
8
  defineMacro({
5
9
  name: 'timed',
6
10
  subMacros: ['next'],
7
11
  interpolate: true,
12
+ parameters: TIMED_PARAMETERS,
8
13
  render({ branches = [] }, ctx) {
9
14
  const { useState, useEffect, useMemo } = ctx.hooks;
10
15
 
11
16
  const sections = useMemo(() => {
12
17
  return branches.map((branch) => ({
13
- delay: branch.rawArgs ? parseDelay(branch.rawArgs) : 0,
18
+ delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
14
19
  nodes: branch.children,
15
20
  className: branch.className,
16
21
  id: branch.id,
@@ -35,21 +40,12 @@ defineMacro({
35
40
  if (visibleIndex < 0) return ctx.wrap(null);
36
41
 
37
42
  const section = sections[visibleIndex]!;
38
- const content = ctx.renderNodes(section.nodes);
39
- const sectionClass = ctx.resolve!(section.className);
40
- const sectionId = ctx.resolve!(section.id);
41
-
42
- const inner =
43
- sectionClass || sectionId ? (
44
- <span
45
- id={sectionId}
46
- class={sectionClass}
47
- >
48
- {content}
49
- </span>
50
- ) : (
51
- content
52
- );
53
- return ctx.wrap(inner);
43
+ return ctx.wrap(
44
+ wrapContent(
45
+ ctx.resolve!(section.className),
46
+ ctx.resolve!(section.id),
47
+ ctx.renderNodes(section.nodes),
48
+ ),
49
+ );
54
50
  },
55
51
  });
@@ -1,10 +1,11 @@
1
1
  import { defineMacro } from '../../define-macro';
2
+ import { DELAY_PARAMETER } from './macro-args';
2
3
 
3
4
  defineMacro({
4
5
  name: 'type',
5
6
  block: true,
6
7
  interpolate: true,
7
- parameters: [{ name: 'delay', type: 'delay', required: true }],
8
+ parameters: [DELAY_PARAMETER],
8
9
  render({ children = [] }, ctx) {
9
10
  const { useState, useEffect, useRef } = ctx.hooks;
10
11
  const speed = ctx.args.delay ?? 0;