@rohal12/spindle 0.16.1 → 0.18.1

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 (60) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/package.json +2 -2
  3. package/src/action-registry.ts +2 -1
  4. package/src/components/Passage.tsx +69 -4
  5. package/src/components/PassageDialog.tsx +69 -0
  6. package/src/components/StoryInterface.tsx +2 -2
  7. package/src/components/macros/Button.tsx +33 -46
  8. package/src/components/macros/Checkbox.tsx +32 -35
  9. package/src/components/macros/Computed.tsx +49 -54
  10. package/src/components/macros/Cycle.tsx +43 -53
  11. package/src/components/macros/Dialog.tsx +40 -0
  12. package/src/components/macros/Do.tsx +15 -21
  13. package/src/components/macros/For.tsx +57 -75
  14. package/src/components/macros/Goto.tsx +18 -22
  15. package/src/components/macros/If.tsx +39 -53
  16. package/src/components/macros/Include.tsx +30 -46
  17. package/src/components/macros/Listbox.tsx +38 -48
  18. package/src/components/macros/MacroError.tsx +18 -0
  19. package/src/components/macros/MacroLink.tsx +38 -53
  20. package/src/components/macros/MenubarButtons.tsx +158 -0
  21. package/src/components/macros/Meter.tsx +35 -52
  22. package/src/components/macros/Numberbox.tsx +30 -47
  23. package/src/components/macros/PassageDisplay.tsx +37 -30
  24. package/src/components/macros/Print.tsx +19 -36
  25. package/src/components/macros/Radiobutton.tsx +33 -38
  26. package/src/components/macros/Repeat.tsx +49 -54
  27. package/src/components/macros/SaveManager.tsx +400 -0
  28. package/src/components/macros/Set.tsx +19 -24
  29. package/src/components/{SettingsDialog.tsx → macros/SettingsControls.tsx} +18 -36
  30. package/src/components/macros/Span.tsx +8 -0
  31. package/src/components/macros/Stop.tsx +11 -12
  32. package/src/components/macros/StoryTitle.tsx +16 -16
  33. package/src/components/macros/Switch.tsx +41 -59
  34. package/src/components/macros/Textarea.tsx +26 -45
  35. package/src/components/macros/Textbox.tsx +27 -44
  36. package/src/components/macros/Timed.tsx +54 -55
  37. package/src/components/macros/Type.tsx +61 -62
  38. package/src/components/macros/Unset.tsx +21 -22
  39. package/src/components/macros/Widget.tsx +11 -15
  40. package/src/components/macros/option-utils.ts +13 -0
  41. package/src/define-macro.ts +154 -0
  42. package/src/expression.ts +10 -8
  43. package/src/index.tsx +7 -0
  44. package/src/macros/register-builtins.ts +5 -7
  45. package/src/markup/ast.ts +11 -16
  46. package/src/markup/render.tsx +0 -13
  47. package/src/saves/save-manager.ts +43 -0
  48. package/src/saves/types.ts +9 -2
  49. package/src/store.ts +283 -52
  50. package/src/story-api.ts +33 -12
  51. package/src/styles.css +37 -68
  52. package/src/components/PassageLink.tsx +0 -49
  53. package/src/components/SaveLoadDialog.tsx +0 -408
  54. package/src/components/macros/Back.tsx +0 -32
  55. package/src/components/macros/Forward.tsx +0 -34
  56. package/src/components/macros/QuickLoad.tsx +0 -42
  57. package/src/components/macros/QuickSave.tsx +0 -30
  58. package/src/components/macros/Restart.tsx +0 -35
  59. package/src/components/macros/Saves.tsx +0 -24
  60. package/src/components/macros/SettingsButton.tsx +0 -28
@@ -1,68 +1,50 @@
1
- import { evaluate } from '../../expression';
2
- import { renderNodes } from '../../markup/render';
3
- import { useMergedLocals } from '../../hooks/use-merged-locals';
4
- import { currentSourceLocation } from '../../utils/source-location';
5
- import { registerMacro, registerSubMacro } from '../../registry';
6
- import type { MacroProps } from '../../registry';
7
- import type { Branch } from '../../markup/ast';
8
-
9
- export function Switch({ rawArgs, branches = [] }: MacroProps) {
10
- const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
11
-
12
- let switchValue: unknown;
13
- try {
14
- switchValue = evaluate(rawArgs, mergedVars, mergedTemps, mergedLocals);
15
- } catch (err) {
16
- return (
17
- <span
18
- class="error"
19
- title={String(err)}
20
- >
21
- {`{switch error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
22
- </span>
23
- );
24
- }
25
-
26
- // Find matching {case} branch or {default}
27
- // Skip first branch (index 0) — it holds the switch expression, not a case
28
- let defaultBranch: Branch | null = null;
29
- for (let i = 1; i < branches.length; i++) {
30
- const branch = branches[i]!;
31
- // {default} has empty rawArgs
32
- if (branch.rawArgs === '') {
33
- defaultBranch = branch;
34
- continue;
35
- }
1
+ import { defineMacro } from '../../define-macro';
2
+ import { MacroError } from './MacroError';
36
3
 
4
+ defineMacro({
5
+ name: 'switch',
6
+ subMacros: ['case', 'default'],
7
+ merged: true,
8
+ render({ rawArgs, branches = [] }, ctx) {
9
+ let switchValue: unknown;
37
10
  try {
38
- const caseValue = evaluate(
39
- branch.rawArgs,
40
- mergedVars,
41
- mergedTemps,
42
- mergedLocals,
43
- );
44
- if (switchValue === caseValue) {
45
- return <>{renderNodes(branch.children)}</>;
46
- }
11
+ switchValue = ctx.evaluate!(rawArgs);
47
12
  } catch (err) {
48
13
  return (
49
- <span
50
- class="error"
51
- title={String(err)}
52
- >
53
- {`{case error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
54
- </span>
14
+ <MacroError
15
+ macro="switch"
16
+ error={err}
17
+ />
55
18
  );
56
19
  }
57
- }
58
20
 
59
- if (defaultBranch) {
60
- return <>{renderNodes(defaultBranch.children)}</>;
61
- }
21
+ let defaultBranch: (typeof branches)[number] | null = null;
22
+ for (let i = 1; i < branches.length; i++) {
23
+ const branch = branches[i]!;
24
+ if (branch.rawArgs === '') {
25
+ defaultBranch = branch;
26
+ continue;
27
+ }
62
28
 
63
- return null;
64
- }
29
+ try {
30
+ const caseValue = ctx.evaluate!(branch.rawArgs);
31
+ if (switchValue === caseValue) {
32
+ return <>{ctx.renderNodes(branch.children)}</>;
33
+ }
34
+ } catch (err) {
35
+ return (
36
+ <MacroError
37
+ macro="case"
38
+ error={err}
39
+ />
40
+ );
41
+ }
42
+ }
43
+
44
+ if (defaultBranch) {
45
+ return <>{ctx.renderNodes(defaultBranch.children)}</>;
46
+ }
65
47
 
66
- registerMacro('switch', Switch);
67
- registerSubMacro('case');
68
- registerSubMacro('default');
48
+ return null;
49
+ },
50
+ });
@@ -1,48 +1,29 @@
1
- import { useStoryStore } from '../../store';
2
- import { useAction } from '../../hooks/use-action';
3
- import { registerMacro } from '../../registry';
4
- import type { MacroProps } from '../../registry';
1
+ import { defineMacro } from '../../define-macro';
5
2
 
6
- function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
7
- const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/);
8
- if (!match) {
9
- return { varName: rawArgs.trim(), placeholder: '' };
10
- }
11
- const varName = match[1]!.replace(/["']/g, '');
12
- const placeholder = match[2] || '';
13
- return { varName, placeholder };
14
- }
3
+ defineMacro({
4
+ name: 'textarea',
5
+ storeVar: true,
6
+ render({ rawArgs }, ctx) {
7
+ const { placeholder } = ctx.parseVarArgs(rawArgs);
15
8
 
16
- export function Textarea({ rawArgs, className, id }: MacroProps) {
17
- const { varName, placeholder } = parseArgs(rawArgs);
18
- const name = varName.startsWith('$') ? varName.slice(1) : varName;
9
+ ctx.useAction({
10
+ type: 'textarea',
11
+ key: `$${ctx.varName}`,
12
+ authorId: ctx.id,
13
+ label: placeholder || ctx.varName!,
14
+ variable: ctx.varName,
15
+ value: ctx.value,
16
+ perform: (v) => ctx.setValue!(v !== undefined ? String(v) : ''),
17
+ });
19
18
 
20
- const value = useStoryStore((s) => s.variables[name]);
21
- const setVariable = useStoryStore((s) => s.setVariable);
22
-
23
- useAction({
24
- type: 'textarea',
25
- key: `$${name}`,
26
- authorId: id,
27
- label: placeholder || name,
28
- variable: name,
29
- value,
30
- perform: (v) => setVariable(name, v !== undefined ? String(v) : ''),
31
- });
32
-
33
- const cls = className ? `macro-textarea ${className}` : 'macro-textarea';
34
-
35
- return (
36
- <textarea
37
- id={id}
38
- class={cls}
39
- value={value == null ? '' : String(value)}
40
- placeholder={placeholder}
41
- onInput={(e) =>
42
- setVariable(name, (e.target as HTMLTextAreaElement).value)
43
- }
44
- />
45
- );
46
- }
47
-
48
- registerMacro('textarea', Textarea);
19
+ return (
20
+ <textarea
21
+ id={ctx.id}
22
+ class={ctx.cls}
23
+ value={ctx.value == null ? '' : String(ctx.value)}
24
+ placeholder={placeholder}
25
+ onInput={(e) => ctx.setValue!((e.target as HTMLTextAreaElement).value)}
26
+ />
27
+ );
28
+ },
29
+ });
@@ -1,47 +1,30 @@
1
- import { useStoryStore } from '../../store';
2
- import { useAction } from '../../hooks/use-action';
3
- import { registerMacro } from '../../registry';
4
- import type { MacroProps } from '../../registry';
1
+ import { defineMacro } from '../../define-macro';
5
2
 
6
- function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
7
- const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/);
8
- if (!match) {
9
- return { varName: rawArgs.trim(), placeholder: '' };
10
- }
11
- const varName = match[1]!.replace(/["']/g, '');
12
- const placeholder = match[2] || '';
13
- return { varName, placeholder };
14
- }
3
+ defineMacro({
4
+ name: 'textbox',
5
+ storeVar: true,
6
+ render({ rawArgs }, ctx) {
7
+ const { placeholder } = ctx.parseVarArgs(rawArgs);
15
8
 
16
- export function Textbox({ rawArgs, className, id }: MacroProps) {
17
- const { varName, placeholder } = parseArgs(rawArgs);
18
- const name = varName.startsWith('$') ? varName.slice(1) : varName;
9
+ ctx.useAction({
10
+ type: 'textbox',
11
+ key: `$${ctx.varName}`,
12
+ authorId: ctx.id,
13
+ label: placeholder || ctx.varName!,
14
+ variable: ctx.varName,
15
+ value: ctx.value,
16
+ perform: (v) => ctx.setValue!(v !== undefined ? String(v) : ''),
17
+ });
19
18
 
20
- const value = useStoryStore((s) => s.variables[name]);
21
- const setVariable = useStoryStore((s) => s.setVariable);
22
-
23
- useAction({
24
- type: 'textbox',
25
- key: `$${name}`,
26
- authorId: id,
27
- label: placeholder || name,
28
- variable: name,
29
- value,
30
- perform: (v) => setVariable(name, v !== undefined ? String(v) : ''),
31
- });
32
-
33
- const cls = className ? `macro-textbox ${className}` : 'macro-textbox';
34
-
35
- return (
36
- <input
37
- type="text"
38
- id={id}
39
- class={cls}
40
- value={value == null ? '' : String(value)}
41
- placeholder={placeholder}
42
- onInput={(e) => setVariable(name, (e.target as HTMLInputElement).value)}
43
- />
44
- );
45
- }
46
-
47
- registerMacro('textbox', Textbox);
19
+ return (
20
+ <input
21
+ type="text"
22
+ id={ctx.id}
23
+ class={ctx.cls}
24
+ value={ctx.value == null ? '' : String(ctx.value)}
25
+ placeholder={placeholder}
26
+ onInput={(e) => ctx.setValue!((e.target as HTMLInputElement).value)}
27
+ />
28
+ );
29
+ },
30
+ });
@@ -1,56 +1,55 @@
1
- import { useState, useEffect, useMemo } from 'preact/hooks';
2
- import { renderNodes } from '../../markup/render';
3
1
  import { parseDelay } from '../../utils/parse-delay';
4
- import { useInterpolate } from '../../hooks/use-interpolate';
5
- import { registerMacro, registerSubMacro } from '../../registry';
6
- import type { MacroProps } from '../../registry';
7
-
8
- export function Timed({ branches = [] }: MacroProps) {
9
- const resolve = useInterpolate();
10
- // For branching blocks, className/id from the opening tag goes on branches[0],
11
- // so the component-level className/id is unused. Each branch carries its own.
12
- const sections = useMemo(() => {
13
- return branches.map((branch) => ({
14
- delay: branch.rawArgs ? parseDelay(branch.rawArgs) : 0,
15
- nodes: branch.children,
16
- className: branch.className,
17
- id: branch.id,
18
- }));
19
- }, [branches]);
20
-
21
- const [visibleIndex, setVisibleIndex] = useState(-1);
22
-
23
- useEffect(() => {
24
- if (visibleIndex >= sections.length - 1) return;
25
-
26
- const nextIndex = visibleIndex + 1;
27
- const delay = sections[nextIndex]!.delay;
28
-
29
- const timer = setTimeout(() => {
30
- setVisibleIndex(nextIndex);
31
- }, delay);
32
-
33
- return () => clearTimeout(timer);
34
- }, [visibleIndex, sections]);
35
-
36
- if (visibleIndex < 0) return null;
37
-
38
- const section = sections[visibleIndex]!;
39
- const content = renderNodes(section.nodes);
40
- const sectionClass = resolve(section.className);
41
- const sectionId = resolve(section.id);
42
-
43
- if (sectionClass || sectionId)
44
- return (
45
- <span
46
- id={sectionId}
47
- class={sectionClass}
48
- >
49
- {content}
50
- </span>
51
- );
52
- return <>{content}</>;
53
- }
54
-
55
- registerMacro('timed', Timed);
56
- registerSubMacro('next');
2
+ import { defineMacro } from '../../define-macro';
3
+
4
+ defineMacro({
5
+ name: 'timed',
6
+ subMacros: ['next'],
7
+ interpolate: true,
8
+ render({ branches = [] }, ctx) {
9
+ const { useState, useEffect, useMemo } = ctx.hooks;
10
+
11
+ const sections = useMemo(() => {
12
+ return branches.map((branch) => ({
13
+ delay: branch.rawArgs ? parseDelay(branch.rawArgs) : 0,
14
+ nodes: branch.children,
15
+ className: branch.className,
16
+ id: branch.id,
17
+ }));
18
+ }, [branches]);
19
+
20
+ const [visibleIndex, setVisibleIndex] = useState(-1);
21
+
22
+ useEffect(() => {
23
+ if (visibleIndex >= sections.length - 1) return;
24
+
25
+ const nextIndex = visibleIndex + 1;
26
+ const delay = sections[nextIndex]!.delay;
27
+
28
+ const timer = setTimeout(() => {
29
+ setVisibleIndex(nextIndex);
30
+ }, delay);
31
+
32
+ return () => clearTimeout(timer);
33
+ }, [visibleIndex, sections]);
34
+
35
+ if (visibleIndex < 0) return ctx.wrap(null);
36
+
37
+ 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);
54
+ },
55
+ });
@@ -1,76 +1,75 @@
1
- import { useState, useEffect, useRef } from 'preact/hooks';
2
- import { renderInlineNodes } from '../../markup/render';
3
1
  import { parseDelay } from '../../utils/parse-delay';
4
- import { useInterpolate } from '../../hooks/use-interpolate';
5
- import { registerMacro } from '../../registry';
6
- import type { MacroProps } from '../../registry';
2
+ import { defineMacro } from '../../define-macro';
7
3
 
8
- export function Type({ rawArgs, children = [], className, id }: MacroProps) {
9
- const resolve = useInterpolate();
10
- className = resolve(className);
11
- id = resolve(id);
12
- const speed = parseDelay(rawArgs);
13
- const containerRef = useRef<HTMLSpanElement>(null);
14
- const [totalChars, setTotalChars] = useState(0);
15
- const [visibleChars, setVisibleChars] = useState(0);
4
+ defineMacro({
5
+ name: 'type',
6
+ interpolate: true,
7
+ render({ rawArgs, children = [] }, ctx) {
8
+ const { useState, useEffect, useRef } = ctx.hooks;
9
+ const speed = parseDelay(rawArgs);
10
+ const containerRef = useRef<HTMLSpanElement>(null);
11
+ const [totalChars, setTotalChars] = useState(0);
12
+ const [visibleChars, setVisibleChars] = useState(0);
16
13
 
17
- // After first render, measure total text length
18
- useEffect(() => {
19
- if (containerRef.current) {
20
- const text = containerRef.current.textContent || '';
21
- setTotalChars(text.length);
22
- }
23
- }, []);
14
+ useEffect(() => {
15
+ if (containerRef.current) {
16
+ const text = containerRef.current.textContent || '';
17
+ setTotalChars(text.length);
18
+ }
19
+ }, []);
24
20
 
25
- // Typewriter interval
26
- useEffect(() => {
27
- if (totalChars === 0) return;
28
- if (visibleChars >= totalChars) return;
21
+ // Typewriter interval
22
+ useEffect(() => {
23
+ if (totalChars === 0) return;
24
+ if (visibleChars >= totalChars) return;
29
25
 
30
- const timer = setInterval(() => {
31
- setVisibleChars((c) => {
32
- if (c >= totalChars) {
33
- clearInterval(timer);
34
- return c;
35
- }
36
- return c + 1;
37
- });
38
- }, speed);
26
+ const timer = setInterval(() => {
27
+ setVisibleChars((c) => {
28
+ if (c >= totalChars) {
29
+ clearInterval(timer);
30
+ return c;
31
+ }
32
+ return c + 1;
33
+ });
34
+ }, speed);
39
35
 
40
- return () => clearInterval(timer);
41
- }, [totalChars, speed]);
36
+ return () => clearInterval(timer);
37
+ }, [totalChars, speed]);
42
38
 
43
- const done = visibleChars >= totalChars && totalChars > 0;
39
+ const done = visibleChars >= totalChars && totalChars > 0;
44
40
 
45
- const cls = ['macro-type', done ? 'macro-type-done' : '', className || '']
46
- .filter(Boolean)
47
- .join(' ');
41
+ const cls = [
42
+ 'macro-type',
43
+ done ? 'macro-type-done' : '',
44
+ ctx.className || '',
45
+ ]
46
+ .filter(Boolean)
47
+ .join(' ');
48
48
 
49
- return (
50
- <span
51
- id={id}
52
- class={cls}
53
- ref={containerRef}
54
- style={{
55
- clipPath: totalChars > 0 && !done ? undefined : undefined,
56
- }}
57
- >
49
+ return (
58
50
  <span
59
- class="macro-type-inner"
51
+ id={ctx.id}
52
+ class={cls}
53
+ ref={containerRef}
60
54
  style={{
61
- display: 'inline',
62
- visibility: totalChars === 0 ? 'hidden' : 'visible',
63
- clipPath:
64
- totalChars > 0 && !done
65
- ? `inset(0 ${((totalChars - visibleChars) / totalChars) * 100}% 0 0)`
66
- : undefined,
55
+ clipPath: totalChars > 0 && !done ? undefined : undefined,
67
56
  }}
68
57
  >
69
- {renderInlineNodes(children)}
58
+ <span
59
+ class="macro-type-inner"
60
+ style={{
61
+ display: 'inline',
62
+ visibility: totalChars === 0 ? 'hidden' : 'visible',
63
+ clipPath:
64
+ totalChars > 0 && !done
65
+ ? `inset(0 ${((totalChars - visibleChars) / totalChars) * 100}% 0 0)`
66
+ : undefined,
67
+ }}
68
+ >
69
+ {ctx.renderInlineNodes(children)}
70
+ </span>
71
+ {!done && totalChars > 0 && <span class="macro-type-cursor" />}
70
72
  </span>
71
- {!done && totalChars > 0 && <span class="macro-type-cursor" />}
72
- </span>
73
- );
74
- }
75
-
76
- registerMacro('type', Type);
73
+ );
74
+ },
75
+ });
@@ -1,28 +1,27 @@
1
- import { useRef } from 'preact/hooks';
2
1
  import { useStoryStore } from '../../store';
3
- import { registerMacro } from '../../registry';
4
- import type { MacroProps } from '../../registry';
2
+ import { defineMacro } from '../../define-macro';
5
3
 
6
- export function Unset({ rawArgs }: MacroProps) {
7
- const ran = useRef(false);
4
+ defineMacro({
5
+ name: 'unset',
6
+ render({ rawArgs }, ctx) {
7
+ const ran = ctx.hooks.useRef(false);
8
8
 
9
- if (!ran.current) {
10
- ran.current = true;
11
- const state = useStoryStore.getState();
12
- const name = rawArgs.trim();
9
+ if (!ran.current) {
10
+ ran.current = true;
11
+ const state = useStoryStore.getState();
12
+ const name = rawArgs.trim();
13
13
 
14
- if (name.startsWith('$')) {
15
- state.deleteVariable(name.slice(1));
16
- } else if (name.startsWith('_')) {
17
- state.deleteTemporary(name.slice(1));
18
- } else {
19
- console.error(
20
- `spindle: {unset} expects a variable ($name or _name), got "${name}"`,
21
- );
14
+ if (name.startsWith('$')) {
15
+ state.deleteVariable(name.slice(1));
16
+ } else if (name.startsWith('_')) {
17
+ state.deleteTemporary(name.slice(1));
18
+ } else {
19
+ console.error(
20
+ `spindle: {unset} expects a variable ($name or _name), got "${name}"`,
21
+ );
22
+ }
22
23
  }
23
- }
24
24
 
25
- return null;
26
- }
27
-
28
- registerMacro('unset', Unset);
25
+ return null;
26
+ },
27
+ });
@@ -1,11 +1,6 @@
1
- import { useLayoutEffect } from 'preact/hooks';
2
1
  import { registerWidget } from '../../widgets/widget-registry';
3
- import { registerMacro } from '../../registry';
4
- import type { MacroProps } from '../../registry';
2
+ import { defineMacro } from '../../define-macro';
5
3
 
6
- /**
7
- * Parse widget definition args: "WidgetName" or "WidgetName" $param1 $param2
8
- */
9
4
  function parseWidgetDef(rawArgs: string): { name: string; params: string[] } {
10
5
  const tokens = rawArgs.trim().split(/\s+/);
11
6
  const name = tokens[0]!.replace(/["']/g, '');
@@ -13,14 +8,15 @@ function parseWidgetDef(rawArgs: string): { name: string; params: string[] } {
13
8
  return { name, params };
14
9
  }
15
10
 
16
- export function Widget({ rawArgs, children = [] }: MacroProps) {
17
- const { name, params } = parseWidgetDef(rawArgs);
11
+ defineMacro({
12
+ name: 'widget',
13
+ render({ rawArgs, children = [] }, ctx) {
14
+ const { name, params } = parseWidgetDef(rawArgs);
18
15
 
19
- useLayoutEffect(() => {
20
- registerWidget(name, children, params);
21
- }, []);
16
+ ctx.hooks.useLayoutEffect(() => {
17
+ registerWidget(name, children, params);
18
+ }, []);
22
19
 
23
- return null;
24
- }
25
-
26
- registerMacro('widget', Widget);
20
+ return null;
21
+ },
22
+ });
@@ -1,5 +1,18 @@
1
1
  import type { ASTNode } from '../../markup/ast';
2
2
 
3
+ export function parseVarArgs(rawArgs: string): {
4
+ varName: string;
5
+ placeholder: string;
6
+ } {
7
+ const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s*(?:["'](.*)["'])?\s*$/);
8
+ if (!match) {
9
+ return { varName: rawArgs.trim(), placeholder: '' };
10
+ }
11
+ const varName = match[1]!.replace(/["']/g, '');
12
+ const placeholder = match[2] || '';
13
+ return { varName, placeholder };
14
+ }
15
+
3
16
  /**
4
17
  * Walk AST children to find {option} macro nodes, returning their rawArgs as values.
5
18
  */