@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
@@ -3,13 +3,9 @@ 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
-
7
- const SIGILS = {
8
- variable: '$',
9
- temporary: '_',
10
- local: '@',
11
- transient: '%',
12
- } as const;
6
+ import { SCOPE_SIGILS } from '../../markup/tokenizer';
7
+ import type { VariableNode } from '../../markup/ast';
8
+ import { display, wrapContent } from './display';
13
9
 
14
10
  const STORE_NAMESPACES = {
15
11
  variable: 'variables',
@@ -17,17 +13,12 @@ const STORE_NAMESPACES = {
17
13
  transient: 'transient',
18
14
  } as const;
19
15
 
20
- interface VarDisplayProps {
21
- name: string;
22
- scope: 'variable' | 'temporary' | 'local' | 'transient';
23
- className?: string;
24
- id?: string;
25
- }
26
-
27
- export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
16
+ /** A `{$name}` (`{_name}`, `{@name}`, `{%name}`) variable display. */
17
+ export function VarDisplay({ node }: { node: VariableNode }) {
18
+ const { name, scope } = node;
28
19
  const resolve = useInterpolate();
29
- className = resolve(className);
30
- id = resolve(id);
20
+ const className = resolve(node.className);
21
+ const id = resolve(node.id);
31
22
  const localsValues = useContext(LocalsValuesContext);
32
23
  const parts = name.split('.');
33
24
  const root = parts[0]!;
@@ -36,11 +27,10 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
36
27
  scope === 'local' ? undefined : ownValue(s[STORE_NAMESPACES[scope]], root),
37
28
  );
38
29
 
39
- const nameError = variableNameError(root, SIGILS[scope] + root);
30
+ const sigil = SCOPE_SIGILS[scope];
31
+ const nameError = variableNameError(root, sigil + root);
40
32
  if (nameError) {
41
- return (
42
- <span class="error">{`{${SIGILS[scope]}${name} error: ${nameError}}`}</span>
43
- );
33
+ return <span class="error">{`{${sigil}${name} error: ${nameError}}`}</span>;
44
34
  }
45
35
 
46
36
  let value: unknown;
@@ -61,15 +51,5 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
61
51
  value = (value as Record<string, unknown>)[part];
62
52
  }
63
53
 
64
- const display = value == null ? '' : String(value);
65
- if (className || id)
66
- return (
67
- <span
68
- id={id}
69
- class={className}
70
- >
71
- {display}
72
- </span>
73
- );
74
- return <>{display}</>;
54
+ return wrapContent(className, id, display(value));
75
55
  }
@@ -1,35 +1,69 @@
1
1
  import { registerWidget } from '../../widgets/widget-registry';
2
2
  import { astContainsChildren } from '../../widgets/ast-scanner';
3
- import { registerBlockMacro } from '../../markup/ast';
3
+ 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 { parseMacroArgs } from './macro-args';
8
+ import type { MacroArgs } from '../../registry';
9
+
10
+ /** A {widget} definition's name, then its `@` parameters. */
11
+ const WIDGET_PARAMETERS = [
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
+ }
7
20
 
8
21
  /**
9
- * Read the arguments of a {widget} definition: its name (quotes dropped)
10
- * and its parameters, the words after it that start with one of the
11
- * characters of `sigils`.
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.
12
25
  */
13
- export function parseWidgetDef(
14
- rawArgs: string,
15
- sigils = '@',
16
- ): { name: string; params: string[] } {
17
- const [first, ...rest] = rawArgs.trim().split(/\s+/);
26
+ function widgetDef({
27
+ name = '',
28
+ parameters = '',
29
+ }: MacroArgs<typeof WIDGET_PARAMETERS>): WidgetDef {
18
30
  return {
19
- name: first!.replace(/["']/g, ''),
20
- params: rest.filter((word) => sigils.includes(word[0]!)),
31
+ name,
32
+ params: parameters.split(/\s+/).filter((word) => word.startsWith('@')),
21
33
  };
22
34
  }
23
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
+ }
40
+
41
+ /**
42
+ * Register the widget a definition declares, with its body. Widgets whose
43
+ * body renders {@children} take a closing tag, so they are registered as
44
+ * block macros too: passages parsed later nest their content. A parameter
45
+ * that no namespace can hold throws (see registerWidget), registering
46
+ * nothing.
47
+ */
48
+ export function registerWidgetDef(
49
+ { name, params }: WidgetDef,
50
+ body: ASTNode[],
51
+ ): void {
52
+ const isBlock = astContainsChildren(body);
53
+ registerWidget(name, body, params, isBlock);
54
+ if (isBlock) registerBlockMacro(name);
55
+ }
56
+
24
57
  defineMacro({
25
58
  name: 'widget',
26
59
  block: true,
27
- render({ rawArgs, children = [] }, ctx) {
28
- let parsed: ReturnType<typeof parseWidgetDef> | undefined;
60
+ parameters: WIDGET_PARAMETERS,
61
+ render({ children = [] }, ctx) {
62
+ let parsed: WidgetDef | undefined;
29
63
  let error: unknown;
30
64
  try {
31
65
  // Refuse `@` parameters that no namespace can hold
32
- const def = parseWidgetDef(rawArgs);
66
+ const def = widgetDef(ctx.args);
33
67
  for (const param of def.params) checkVariableName(param.slice(1), param);
34
68
  parsed = def;
35
69
  } catch (err) {
@@ -41,12 +75,7 @@ defineMacro({
41
75
  const paramsKey = params.join(',');
42
76
 
43
77
  ctx.hooks.useLayoutEffect(() => {
44
- if (!parsed) return;
45
- // Widgets whose body renders {@children} take a closing tag; register
46
- // them as block macros so passages parsed later nest their content.
47
- const isBlock = astContainsChildren(children);
48
- registerWidget(name, children, params, isBlock);
49
- if (isBlock) registerBlockMacro(name);
78
+ if (parsed) registerWidgetDef(parsed, children);
50
79
  }, [name, childrenKey, paramsKey]);
51
80
 
52
81
  if (error) {
@@ -0,0 +1,18 @@
1
+ import type { Branch } from '../../markup/ast';
2
+
3
+ /**
4
+ * The branch a conditional block ({if}, {switch}) shows: the first of
5
+ * `branches` whose argument `matches`, else the last bare one ({else},
6
+ * {default}), else null. Throws what `matches` throws.
7
+ */
8
+ export function selectBranch(
9
+ branches: Branch[],
10
+ matches: (rawArgs: string) => boolean,
11
+ ): Branch | null {
12
+ let fallback: Branch | null = null;
13
+ for (const branch of branches) {
14
+ if (branch.rawArgs === '') fallback = branch;
15
+ else if (matches(branch.rawArgs)) return branch;
16
+ }
17
+ return fallback;
18
+ }
@@ -0,0 +1,19 @@
1
+ import { h, Fragment, type ComponentChildren, type VNode } from 'preact';
2
+
3
+ /** The text a value is shown as: nothing for null and undefined. */
4
+ export function display(value: unknown): string {
5
+ return value == null ? '' : String(value);
6
+ }
7
+
8
+ /**
9
+ * `content` in a `<span>` carrying `className` and `id`, or in a fragment if
10
+ * it has neither.
11
+ */
12
+ export function wrapContent(
13
+ className: string | undefined,
14
+ id: string | undefined,
15
+ content: ComponentChildren,
16
+ ): VNode<any> {
17
+ if (className || id) return h('span', { id, class: className }, content);
18
+ return h(Fragment, null, content);
19
+ }
@@ -1,5 +1,34 @@
1
1
  import { h } from 'preact';
2
- import { defineMacro } from '../../define-macro';
2
+ import { defineMacro, type MacroContext } from '../../define-macro';
3
+ import type { UseActionOptions } from '../../hooks/use-action';
4
+ import { display } from './display';
5
+
6
+ /** The story variable a `storeVar` macro binds: its first parameter. */
7
+ export const VARIABLE_PARAMETER = {
8
+ name: 'variable',
9
+ type: 'variable',
10
+ required: true,
11
+ } as const;
12
+
13
+ /**
14
+ * Register the automation action of a macro bound to a story variable: keyed
15
+ * by the variable (or `key`), with the variable's value unless `action`
16
+ * gives another.
17
+ */
18
+ export function useVariableAction(
19
+ ctx: Pick<MacroContext, 'useAction' | 'id' | 'varName' | 'value'>,
20
+ action: Omit<UseActionOptions, 'key' | 'authorId' | 'variable'> & {
21
+ key?: string;
22
+ },
23
+ ): string {
24
+ return ctx.useAction({
25
+ key: `$${ctx.varName}`,
26
+ authorId: ctx.id,
27
+ variable: ctx.varName,
28
+ value: ctx.value,
29
+ ...action,
30
+ });
31
+ }
3
32
 
4
33
  /**
5
34
  * Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
@@ -17,20 +46,13 @@ export function defineInputMacro(
17
46
  defineMacro({
18
47
  name,
19
48
  storeVar: true,
20
- parameters: [
21
- { name: 'variable', type: 'variable', required: true },
22
- { name: 'placeholder', type: 'string' },
23
- ],
49
+ parameters: [VARIABLE_PARAMETER, { name: 'placeholder', type: 'string' }],
24
50
  render(_props, ctx) {
25
51
  const placeholder = ctx.args.placeholder ?? '';
26
52
 
27
- ctx.useAction({
53
+ useVariableAction(ctx, {
28
54
  type: name,
29
- key: `$${ctx.varName}`,
30
- authorId: ctx.id,
31
55
  label: placeholder || ctx.varName!,
32
- variable: ctx.varName,
33
- value: ctx.value,
34
56
  perform: (v) => ctx.setValue!(toValue(v)),
35
57
  });
36
58
 
@@ -38,7 +60,7 @@ export function defineInputMacro(
38
60
  type,
39
61
  id: ctx.id,
40
62
  class: ctx.cls,
41
- value: ctx.value == null ? '' : String(ctx.value),
63
+ value: display(ctx.value),
42
64
  placeholder,
43
65
  onInput: (e: Event) =>
44
66
  ctx.setValue!(toValue((e.target as HTMLInputElement).value)),
@@ -232,6 +232,13 @@ function readGroup(
232
232
  : unset(rest);
233
233
  }
234
234
 
235
+ /** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
236
+ export const DELAY_PARAMETER = {
237
+ name: 'delay',
238
+ type: 'delay',
239
+ required: true,
240
+ } as const;
241
+
235
242
  /** Read `rawArgs` into the declared `parameters` (see above). */
236
243
  export function parseMacroArgs<const P extends readonly ParameterDef[]>(
237
244
  rawArgs: string,
@@ -1,5 +1,4 @@
1
1
  import type { ASTNode } from '../../markup/ast';
2
- import { readWholeQuoted } from './arg-utils';
3
2
  import { parseMacroArgs } from './macro-args';
4
3
 
5
4
  /** `$var "placeholder"`, the arguments of the input macros. */
@@ -14,18 +13,22 @@ export function parseVarArgs(rawArgs: string): {
14
13
  return { varName: variable.replace(/["']/g, ''), placeholder };
15
14
  }
16
15
 
16
+ /** The parameters of an `{option}` sub-macro: its value. */
17
+ const OPTION_PARAMETERS = [{ name: 'value', type: 'string' }] as const;
18
+
17
19
  /**
18
- * Walk AST children to find {option} macro nodes, returning their rawArgs as values.
20
+ * Walk AST children to find {option} macro nodes, returning their values.
19
21
  */
20
22
  export function extractOptions(children: ASTNode[]): string[] {
21
23
  const options: string[] = [];
22
24
  for (const node of children) {
23
25
  if (node.type === 'macro' && node.name === 'option') {
24
- const raw = node.rawArgs.trim();
25
- // Strip surrounding quotes so {option "Long Sword"} gives "Long Sword"
26
- const stripped =
27
- readWholeQuoted(raw) ?? raw.replace(/^(["'])(.+)\1$/s, '$2');
28
- options.push(stripped);
26
+ // {option "Long Sword"} gives `Long Sword`. A value that isn't one
27
+ // quoted string loses a matching pair of quotes around it, if any.
28
+ const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
29
+ options.push(
30
+ value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, '$2'),
31
+ );
29
32
  }
30
33
  }
31
34
  return options;
@@ -1,4 +1,4 @@
1
- import { h, Fragment } from 'preact';
1
+ import { h } from 'preact';
2
2
  import type { VNode, ComponentChildren } from 'preact';
3
3
  import {
4
4
  useContext,
@@ -28,6 +28,7 @@ import type { UseActionOptions } from './hooks/use-action';
28
28
  import { collectText } from './utils/extract-text';
29
29
  import { currentSourceLocation } from './utils/source-location';
30
30
  import { parseVarArgs, extractOptions } from './components/macros/option-utils';
31
+ import { wrapContent } from './components/macros/display';
31
32
  import {
32
33
  parseMacroArgs,
33
34
  readBoundVariable,
@@ -177,11 +178,8 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
177
178
  mutate: (code: string) => executeMutation(code, getValues(), update),
178
179
  update,
179
180
  getValues,
180
- wrap: (content: ComponentChildren): VNode<any> => {
181
- if (className || id)
182
- return h('span', { id, class: className }, content);
183
- return h(Fragment, null, content);
184
- },
181
+ wrap: (content: ComponentChildren): VNode<any> =>
182
+ wrapContent(className, id, content),
185
183
  };
186
184
 
187
185
  if (config.merged) {
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
- export function countQueries(counts: () => Counts) {
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
- currentPassage,
166
- previousPassage,
167
- visited: visits.count,
168
- hasVisited: visits.has,
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
- * Evaluate an expression and return its value.
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: Namespace = EMPTY_NAMESPACE,
207
- transient: Namespace = EMPTY_NAMESPACE,
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
- const fn = getOrCompile('exec:' + transformed, transformed);
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
  /**
@@ -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
- export interface UseActionOptions {
11
- type: ActionType;
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
- if (opts.target !== undefined) action.target = opts.target;
43
- if (opts.variable !== undefined) action.variable = opts.variable;
44
- if (opts.options !== undefined) action.options = opts.options;
45
- if (opts.value !== undefined) action.value = opts.value;
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 { registerWidget } from './widgets/widget-registry';
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
- const { name: widgetName, params } = parseWidgetDef(
178
- node.rawArgs,
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
- registerWidget(widgetName, children, params, isBlock);
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 "${widgetName}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`,
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
  }