@rohal12/spindle 0.16.1 → 0.17.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.
Files changed (52) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/package.json +1 -1
  3. package/src/action-registry.ts +2 -1
  4. package/src/components/PassageDialog.tsx +69 -0
  5. package/src/components/macros/Button.tsx +33 -46
  6. package/src/components/macros/Checkbox.tsx +32 -35
  7. package/src/components/macros/Computed.tsx +49 -54
  8. package/src/components/macros/Cycle.tsx +43 -53
  9. package/src/components/macros/Dialog.tsx +40 -0
  10. package/src/components/macros/Do.tsx +15 -21
  11. package/src/components/macros/For.tsx +57 -75
  12. package/src/components/macros/Goto.tsx +18 -22
  13. package/src/components/macros/If.tsx +39 -53
  14. package/src/components/macros/Include.tsx +30 -46
  15. package/src/components/macros/Listbox.tsx +38 -48
  16. package/src/components/macros/MacroError.tsx +18 -0
  17. package/src/components/macros/MacroLink.tsx +36 -51
  18. package/src/components/macros/MenubarButtons.tsx +158 -0
  19. package/src/components/macros/Meter.tsx +35 -52
  20. package/src/components/macros/Numberbox.tsx +30 -47
  21. package/src/components/macros/PassageDisplay.tsx +26 -29
  22. package/src/components/macros/Print.tsx +19 -36
  23. package/src/components/macros/Radiobutton.tsx +33 -38
  24. package/src/components/macros/Repeat.tsx +49 -54
  25. package/src/components/macros/SaveManager.tsx +400 -0
  26. package/src/components/macros/Set.tsx +19 -24
  27. package/src/components/{SettingsDialog.tsx → macros/SettingsControls.tsx} +18 -36
  28. package/src/components/macros/Stop.tsx +11 -12
  29. package/src/components/macros/StoryTitle.tsx +16 -16
  30. package/src/components/macros/Switch.tsx +41 -59
  31. package/src/components/macros/Textarea.tsx +26 -45
  32. package/src/components/macros/Textbox.tsx +27 -44
  33. package/src/components/macros/Timed.tsx +52 -55
  34. package/src/components/macros/Type.tsx +61 -62
  35. package/src/components/macros/Unset.tsx +21 -22
  36. package/src/components/macros/Widget.tsx +11 -15
  37. package/src/components/macros/option-utils.ts +13 -0
  38. package/src/define-macro.ts +154 -0
  39. package/src/macros/register-builtins.ts +4 -7
  40. package/src/markup/ast.ts +10 -16
  41. package/src/markup/render.tsx +0 -13
  42. package/src/story-api.ts +7 -0
  43. package/src/styles.css +37 -68
  44. package/src/components/PassageLink.tsx +0 -49
  45. package/src/components/SaveLoadDialog.tsx +0 -408
  46. package/src/components/macros/Back.tsx +0 -32
  47. package/src/components/macros/Forward.tsx +0 -34
  48. package/src/components/macros/QuickLoad.tsx +0 -42
  49. package/src/components/macros/QuickSave.tsx +0 -30
  50. package/src/components/macros/Restart.tsx +0 -35
  51. package/src/components/macros/Saves.tsx +0 -24
  52. package/src/components/macros/SettingsButton.tsx +0 -28
@@ -5,17 +5,13 @@ import {
5
5
  useRef,
6
6
  useMemo,
7
7
  } from 'preact/hooks';
8
- import { evaluate } from '../../expression';
9
8
  import {
10
9
  LocalsValuesContext,
11
10
  LocalsUpdateContext,
12
11
  renderNodes,
13
12
  } from '../../markup/render';
14
- import { useMergedLocals } from '../../hooks/use-merged-locals';
15
- import { useInterpolate } from '../../hooks/use-interpolate';
16
- import { currentSourceLocation } from '../../utils/source-location';
17
- import { registerMacro } from '../../registry';
18
- import type { MacroProps } from '../../registry';
13
+ import { defineMacro } from '../../define-macro';
14
+ import { MacroError } from './MacroError';
19
15
  import type { ASTNode } from '../../markup/ast';
20
16
 
21
17
  /**
@@ -89,78 +85,64 @@ function ForIteration({
89
85
  );
90
86
  }
91
87
 
92
- export function For({ rawArgs, children = [], className, id }: MacroProps) {
93
- const resolve = useInterpolate();
94
- className = resolve(className);
95
- id = resolve(id);
96
- const parentValues = useContext(LocalsValuesContext);
97
- const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
98
-
99
- let parsed: ReturnType<typeof parseForArgs>;
100
- try {
101
- parsed = parseForArgs(rawArgs);
102
- } catch (err) {
103
- return (
104
- <span
105
- class="error"
106
- title={String(err)}
107
- >
108
- {`{for error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
109
- </span>
110
- );
111
- }
112
-
113
- const { itemVar, indexVar, listExpr } = parsed;
88
+ defineMacro({
89
+ name: 'for',
90
+ interpolate: true,
91
+ merged: true,
92
+ render({ rawArgs, children = [] }, ctx) {
93
+ const parentValues = useContext(LocalsValuesContext);
94
+
95
+ let parsed: ReturnType<typeof parseForArgs>;
96
+ try {
97
+ parsed = parseForArgs(rawArgs);
98
+ } catch (err) {
99
+ return (
100
+ <MacroError
101
+ macro="for"
102
+ error={err}
103
+ />
104
+ );
105
+ }
114
106
 
115
- let list: unknown[];
116
- try {
117
- const result = evaluate(listExpr, mergedVars, mergedTemps, mergedLocals);
118
- if (!Array.isArray(result)) {
107
+ const { itemVar, indexVar, listExpr } = parsed;
108
+
109
+ let list: unknown[];
110
+ try {
111
+ const result = ctx.evaluate!(listExpr);
112
+ if (!Array.isArray(result)) {
113
+ return (
114
+ <span class="error">
115
+ {`{for error: expression did not evaluate to an array}`}
116
+ </span>
117
+ );
118
+ }
119
+ list = result;
120
+ } catch (err) {
119
121
  return (
120
- <span class="error">
121
- {`{for error${currentSourceLocation()}: expression did not evaluate to an array}`}
122
- </span>
122
+ <MacroError
123
+ macro="for"
124
+ error={err}
125
+ />
123
126
  );
124
127
  }
125
- list = result;
126
- } catch (err) {
127
- return (
128
- <span
129
- class="error"
130
- title={String(err)}
131
- >
132
- {`{for error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
133
- </span>
134
- );
135
- }
136
128
 
137
- const content = list.map((item, i) => {
138
- const ownKeys: Record<string, unknown> = {
139
- [itemVar]: item,
140
- ...(indexVar ? { [indexVar]: i } : undefined),
141
- };
142
-
143
- return (
144
- <ForIteration
145
- key={i}
146
- parentValues={parentValues}
147
- ownKeys={ownKeys}
148
- initialValues={{}}
149
- children={children}
150
- />
151
- );
152
- });
153
-
154
- if (className || id)
155
- return (
156
- <span
157
- id={id}
158
- class={className}
159
- >
160
- {content}
161
- </span>
162
- );
163
- return <>{content}</>;
164
- }
129
+ const content = list.map((item, i) => {
130
+ const ownKeys: Record<string, unknown> = {
131
+ [itemVar]: item,
132
+ ...(indexVar ? { [indexVar]: i } : undefined),
133
+ };
134
+
135
+ return (
136
+ <ForIteration
137
+ key={i}
138
+ parentValues={parentValues}
139
+ ownKeys={ownKeys}
140
+ initialValues={{}}
141
+ children={children}
142
+ />
143
+ );
144
+ });
165
145
 
166
- registerMacro('for', For);
146
+ return ctx.wrap(content);
147
+ },
148
+ });
@@ -1,25 +1,21 @@
1
- import { useLayoutEffect } from 'preact/hooks';
2
1
  import { useStoryStore } from '../../store';
3
- import { evaluate } from '../../expression';
4
- import { useMergedLocals } from '../../hooks/use-merged-locals';
5
- import { registerMacro } from '../../registry';
6
- import type { MacroProps } from '../../registry';
2
+ import { defineMacro } from '../../define-macro';
7
3
 
8
- export function Goto({ rawArgs }: MacroProps) {
9
- const [variables, temporary, locals] = useMergedLocals();
4
+ defineMacro({
5
+ name: 'goto',
6
+ merged: true,
7
+ render({ rawArgs }, ctx) {
8
+ ctx.hooks.useLayoutEffect(() => {
9
+ let passageName: string;
10
+ try {
11
+ const result = ctx.evaluate!(rawArgs);
12
+ passageName = String(result);
13
+ } catch {
14
+ passageName = rawArgs.replace(/^["']|["']$/g, '');
15
+ }
16
+ useStoryStore.getState().navigate(passageName);
17
+ }, []);
10
18
 
11
- useLayoutEffect(() => {
12
- let passageName: string;
13
- try {
14
- const result = evaluate(rawArgs, variables, temporary, locals);
15
- passageName = String(result);
16
- } catch {
17
- passageName = rawArgs.replace(/^["']|["']$/g, '');
18
- }
19
- useStoryStore.getState().navigate(passageName);
20
- }, []);
21
-
22
- return null;
23
- }
24
-
25
- registerMacro('goto', Goto);
19
+ return null;
20
+ },
21
+ });
@@ -1,61 +1,47 @@
1
- import { evaluate } from '../../expression';
2
- import { renderNodes } from '../../markup/render';
3
- import { useMergedLocals } from '../../hooks/use-merged-locals';
4
- import { useInterpolate } from '../../hooks/use-interpolate';
5
- import { currentSourceLocation } from '../../utils/source-location';
6
- import { registerMacro } from '../../registry';
7
- import type { MacroProps } from '../../registry';
1
+ import { defineMacro } from '../../define-macro';
2
+ import { MacroError } from './MacroError';
8
3
  import type { Branch } from '../../markup/ast';
9
4
 
10
- export function If({ branches = [] }: MacroProps) {
11
- const resolve = useInterpolate();
12
- const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
13
-
14
- function renderBranch(branch: Branch) {
15
- const children = renderNodes(branch.children);
16
- const cls = resolve(branch.className);
17
- const branchId = resolve(branch.id);
18
- if (cls || branchId)
19
- return (
20
- <span
21
- id={branchId}
22
- class={cls}
23
- >
24
- {children}
25
- </span>
26
- );
27
- return <>{children}</>;
28
- }
29
-
30
- for (const branch of branches) {
31
- // {else} has empty rawArgs — always truthy
32
- if (branch.rawArgs === '') {
33
- return renderBranch(branch);
5
+ defineMacro({
6
+ name: 'if',
7
+ interpolate: true,
8
+ merged: true,
9
+ render({ branches = [] }, ctx) {
10
+ function renderBranch(branch: Branch) {
11
+ const children = ctx.renderNodes(branch.children);
12
+ const cls = ctx.resolve!(branch.className);
13
+ const branchId = ctx.resolve!(branch.id);
14
+ if (cls || branchId)
15
+ return (
16
+ <span
17
+ id={branchId}
18
+ class={cls}
19
+ >
20
+ {children}
21
+ </span>
22
+ );
23
+ return <>{children}</>;
34
24
  }
35
25
 
36
- try {
37
- const result = evaluate(
38
- branch.rawArgs,
39
- mergedVars,
40
- mergedTemps,
41
- mergedLocals,
42
- );
43
- if (result) {
26
+ for (const branch of branches) {
27
+ if (branch.rawArgs === '') {
44
28
  return renderBranch(branch);
45
29
  }
46
- } catch (err) {
47
- return (
48
- <span
49
- class="error"
50
- title={String(err)}
51
- >
52
- {`{if error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
53
- </span>
54
- );
55
- }
56
- }
57
30
 
58
- return null;
59
- }
31
+ try {
32
+ if (ctx.evaluate!(branch.rawArgs)) {
33
+ return renderBranch(branch);
34
+ }
35
+ } catch (err) {
36
+ return (
37
+ <MacroError
38
+ macro="if"
39
+ error={err}
40
+ />
41
+ );
42
+ }
43
+ }
60
44
 
61
- registerMacro('if', If);
45
+ return null;
46
+ },
47
+ });
@@ -1,55 +1,39 @@
1
1
  import { useStoryStore } from '../../store';
2
- import { evaluate } from '../../expression';
3
2
  import { tokenize } from '../../markup/tokenizer';
4
3
  import { buildAST } from '../../markup/ast';
5
- import { renderNodes } from '../../markup/render';
6
- import { useMergedLocals } from '../../hooks/use-merged-locals';
7
- import { useInterpolate } from '../../hooks/use-interpolate';
8
- import { currentSourceLocation } from '../../utils/source-location';
9
- import { registerMacro } from '../../registry';
10
- import type { MacroProps } from '../../registry';
4
+ import { defineMacro } from '../../define-macro';
11
5
 
12
- export function Include({ rawArgs, className, id }: MacroProps) {
13
- const resolve = useInterpolate();
14
- className = resolve(className);
15
- id = resolve(id);
16
- const storyData = useStoryStore((s) => s.storyData);
17
- const [variables, temporary, locals] = useMergedLocals();
6
+ defineMacro({
7
+ name: 'include',
8
+ interpolate: true,
9
+ merged: true,
10
+ render({ rawArgs }, ctx) {
11
+ const storyData = useStoryStore((s) => s.storyData);
18
12
 
19
- if (!storyData) return null;
13
+ if (!storyData) return null;
20
14
 
21
- let passageName: string;
22
- try {
23
- const result = evaluate(rawArgs, variables, temporary, locals);
24
- passageName = String(result);
25
- } catch {
26
- passageName = rawArgs.replace(/^["']|["']$/g, '');
27
- }
15
+ let passageName: string;
16
+ try {
17
+ const result = ctx.evaluate!(rawArgs);
18
+ passageName = String(result);
19
+ } catch {
20
+ passageName = rawArgs.replace(/^["']|["']$/g, '');
21
+ }
28
22
 
29
- const passage = storyData.passages.get(passageName);
30
- if (passage) {
31
- useStoryStore.getState().trackRender(passageName);
32
- }
33
- if (!passage) {
34
- return (
35
- <span class="error">{`{include${currentSourceLocation()}: passage "${passageName}" not found}`}</span>
36
- );
37
- }
23
+ const passage = storyData.passages.get(passageName);
24
+ if (passage) {
25
+ useStoryStore.getState().trackRender(passageName);
26
+ }
27
+ if (!passage) {
28
+ return (
29
+ <span class="error">{`{include${ctx.sourceLocation()}: passage "${passageName}" not found}`}</span>
30
+ );
31
+ }
38
32
 
39
- const tokens = tokenize(passage.content);
40
- const ast = buildAST(tokens);
41
- const content = renderNodes(ast);
33
+ const tokens = tokenize(passage.content);
34
+ const ast = buildAST(tokens);
35
+ const content = ctx.renderNodes(ast);
42
36
 
43
- if (className || id)
44
- return (
45
- <span
46
- id={id}
47
- class={className}
48
- >
49
- {content}
50
- </span>
51
- );
52
- return <>{content}</>;
53
- }
54
-
55
- registerMacro('include', Include);
37
+ return ctx.wrap(content);
38
+ },
39
+ });
@@ -1,51 +1,41 @@
1
- import { useStoryStore } from '../../store';
2
- import { extractOptions } from './option-utils';
3
- import { useAction } from '../../hooks/use-action';
4
- import { registerMacro, registerSubMacro } from '../../registry';
5
- import type { MacroProps } from '../../registry';
1
+ import { defineMacro } from '../../define-macro';
6
2
 
7
- export function Listbox({ rawArgs, children = [], className, id }: MacroProps) {
8
- const varName = rawArgs.trim().replace(/["']/g, '');
9
- const name = varName.startsWith('$') ? varName.slice(1) : varName;
3
+ defineMacro({
4
+ name: 'listbox',
5
+ subMacros: ['option'],
6
+ storeVar: true,
7
+ render({ children = [] }, ctx) {
8
+ const options = ctx.extractOptions(children);
10
9
 
11
- const value = useStoryStore((s) => s.variables[name]);
12
- const setVariable = useStoryStore((s) => s.setVariable);
10
+ ctx.useAction({
11
+ type: 'listbox',
12
+ key: `$${ctx.varName}`,
13
+ authorId: ctx.id,
14
+ label: ctx.varName!,
15
+ variable: ctx.varName,
16
+ options,
17
+ value: ctx.value,
18
+ perform: (v) => {
19
+ if (v !== undefined) ctx.setValue!(String(v));
20
+ },
21
+ });
13
22
 
14
- const options = extractOptions(children);
15
-
16
- useAction({
17
- type: 'listbox',
18
- key: `$${name}`,
19
- authorId: id,
20
- label: name,
21
- variable: name,
22
- options,
23
- value,
24
- perform: (v) => {
25
- if (v !== undefined) setVariable(name, String(v));
26
- },
27
- });
28
-
29
- const cls = className ? `macro-listbox ${className}` : 'macro-listbox';
30
-
31
- return (
32
- <select
33
- id={id}
34
- class={cls}
35
- value={value == null ? '' : String(value)}
36
- onChange={(e) => setVariable(name, (e.target as HTMLSelectElement).value)}
37
- >
38
- {options.map((opt) => (
39
- <option
40
- key={opt}
41
- value={opt}
42
- >
43
- {opt}
44
- </option>
45
- ))}
46
- </select>
47
- );
48
- }
49
-
50
- registerMacro('listbox', Listbox);
51
- registerSubMacro('option');
23
+ return (
24
+ <select
25
+ id={ctx.id}
26
+ class={ctx.cls}
27
+ value={ctx.value == null ? '' : String(ctx.value)}
28
+ onChange={(e) => ctx.setValue!((e.target as HTMLSelectElement).value)}
29
+ >
30
+ {options.map((opt) => (
31
+ <option
32
+ key={opt}
33
+ value={opt}
34
+ >
35
+ {opt}
36
+ </option>
37
+ ))}
38
+ </select>
39
+ );
40
+ },
41
+ });
@@ -0,0 +1,18 @@
1
+ import { currentSourceLocation } from '../../utils/source-location';
2
+
3
+ export function MacroError({
4
+ macro,
5
+ error,
6
+ }: {
7
+ macro: string;
8
+ error: unknown;
9
+ }) {
10
+ return (
11
+ <span
12
+ class="error"
13
+ title={String(error)}
14
+ >
15
+ {`{${macro} error${currentSourceLocation()}: ${error instanceof Error ? error.message : String(error)}}`}
16
+ </span>
17
+ );
18
+ }
@@ -1,12 +1,9 @@
1
- import { useContext } from 'preact/hooks';
2
1
  import { useStoryStore } from '../../store';
3
2
  import type { ASTNode } from '../../markup/ast';
4
- import { LocalsUpdateContext } from '../../markup/render';
5
- import { useInterpolate } from '../../hooks/use-interpolate';
6
3
  import { executeMutation } from '../../execute-mutation';
4
+ import { collectText } from '../../utils/extract-text';
7
5
  import { currentSourceLocation } from '../../utils/source-location';
8
- import { registerMacro } from '../../registry';
9
- import type { MacroProps } from '../../registry';
6
+ import { defineMacro } from '../../define-macro';
10
7
 
11
8
  function parseArgs(rawArgs: string): {
12
9
  display: string;
@@ -29,9 +26,6 @@ function parseArgs(rawArgs: string): {
29
26
  return { display: rawArgs.trim(), passage: null };
30
27
  }
31
28
 
32
- import { collectText } from '../../utils/extract-text';
33
- import { useAction } from '../../hooks/use-action';
34
-
35
29
  /**
36
30
  * Execute the children imperatively: walk AST for {set} and {do} macros.
37
31
  */
@@ -65,52 +59,43 @@ function executeChildren(
65
59
  }
66
60
  }
67
61
 
68
- export function MacroLink({
69
- rawArgs,
70
- children = [],
71
- className,
72
- id,
73
- }: MacroProps) {
74
- const resolve = useInterpolate();
75
- className = resolve(className);
76
- id = resolve(id);
77
- const { display, passage } = parseArgs(rawArgs);
78
- const { update, getValues } = useContext(LocalsUpdateContext);
62
+ defineMacro({
63
+ name: 'link',
64
+ interpolate: true,
65
+ render({ rawArgs, children = [] }, ctx) {
66
+ const { display, passage } = parseArgs(rawArgs);
79
67
 
80
- const handleClick = (e: Event) => {
81
- e.preventDefault();
82
- executeChildren(children, getValues(), update);
83
- if (passage) {
84
- useStoryStore.getState().navigate(passage);
85
- }
86
- };
87
-
88
- useAction({
89
- type: 'link',
90
- key: passage || display,
91
- authorId: id,
92
- label: display,
93
- target: passage ?? undefined,
94
- perform: () => {
95
- executeChildren(children, getValues(), update);
68
+ const handleClick = (e: Event) => {
69
+ e.preventDefault();
70
+ executeChildren(children, ctx.getValues(), ctx.update);
96
71
  if (passage) {
97
72
  useStoryStore.getState().navigate(passage);
98
73
  }
99
- },
100
- });
101
-
102
- const cls = className ? `macro-link ${className}` : 'macro-link';
74
+ };
103
75
 
104
- return (
105
- <a
106
- id={id}
107
- class={cls}
108
- href="#"
109
- onClick={handleClick}
110
- >
111
- {display}
112
- </a>
113
- );
114
- }
76
+ ctx.useAction({
77
+ type: 'link',
78
+ key: passage || display,
79
+ authorId: ctx.id,
80
+ label: display,
81
+ target: passage ?? undefined,
82
+ perform: () => {
83
+ executeChildren(children, ctx.getValues(), ctx.update);
84
+ if (passage) {
85
+ useStoryStore.getState().navigate(passage);
86
+ }
87
+ },
88
+ });
115
89
 
116
- registerMacro('link', MacroLink);
90
+ return (
91
+ <a
92
+ id={ctx.id}
93
+ class={ctx.cls}
94
+ href="#"
95
+ onClick={handleClick}
96
+ >
97
+ {display}
98
+ </a>
99
+ );
100
+ },
101
+ });