@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.16.1",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -47,7 +47,7 @@
47
47
  "docs:dev": "vitepress dev docs",
48
48
  "docs:build": "vitepress build docs",
49
49
  "docs:preview": "vitepress preview docs",
50
- "prepublishOnly": "bun run test && bun run build",
50
+ "prepublishOnly": "bun run build && bun run test",
51
51
  "prepare": "husky"
52
52
  },
53
53
  "dependencies": {
@@ -12,7 +12,8 @@ export type ActionType =
12
12
  | 'forward'
13
13
  | 'restart'
14
14
  | 'save'
15
- | 'load';
15
+ | 'load'
16
+ | 'dialog';
16
17
 
17
18
  export interface StoryAction {
18
19
  id: string;
@@ -1,20 +1,36 @@
1
- import { useMemo } from 'preact/hooks';
1
+ import { useMemo, useEffect, useState } from 'preact/hooks';
2
2
  import { tokenize } from '../markup/tokenizer';
3
3
  import { buildAST } from '../markup/ast';
4
4
  import { renderNodes } from '../markup/render';
5
+ import { useStoryStore } from '../store';
5
6
  import type { Passage as PassageData } from '../parser';
6
7
  import { sourceLocationOf } from '../utils/source-location';
7
8
 
9
+ export function renderPassageContent(passage: PassageData) {
10
+ const tokens = tokenize(passage.content);
11
+ const ast = buildAST(tokens);
12
+ return renderNodes(ast);
13
+ }
14
+
8
15
  interface PassageProps {
9
16
  passage: PassageData;
10
17
  }
11
18
 
19
+ const CODE_PASSAGES = new Set([
20
+ 'PassageReady',
21
+ 'PassageHeader',
22
+ 'PassageFooter',
23
+ 'PassageDone',
24
+ ]);
25
+
12
26
  export function Passage({ passage }: PassageProps) {
27
+ const storyData = useStoryStore((s) => s.storyData);
28
+ const isCodePassage = CODE_PASSAGES.has(passage.name);
29
+ const [doneReady, setDoneReady] = useState(false);
30
+
13
31
  const content = useMemo(() => {
14
32
  try {
15
- const tokens = tokenize(passage.content);
16
- const ast = buildAST(tokens);
17
- return renderNodes(ast);
33
+ return renderPassageContent(passage);
18
34
  } catch (err) {
19
35
  return (
20
36
  <div class="error">
@@ -25,13 +41,62 @@ export function Passage({ passage }: PassageProps) {
25
41
  }
26
42
  }, [passage.content, passage.name]);
27
43
 
44
+ const headerPassage = isCodePassage
45
+ ? undefined
46
+ : storyData?.passages.get('PassageHeader');
47
+ const footerPassage = isCodePassage
48
+ ? undefined
49
+ : storyData?.passages.get('PassageFooter');
50
+ const donePassage = isCodePassage
51
+ ? undefined
52
+ : storyData?.passages.get('PassageDone');
53
+
54
+ const headerContent = useMemo(() => {
55
+ if (!headerPassage) return null;
56
+ try {
57
+ return renderPassageContent(headerPassage);
58
+ } catch (err) {
59
+ console.error('spindle: Error in PassageHeader:', err);
60
+ return null;
61
+ }
62
+ }, [headerPassage?.content]);
63
+
64
+ const footerContent = useMemo(() => {
65
+ if (!footerPassage) return null;
66
+ try {
67
+ return renderPassageContent(footerPassage);
68
+ } catch (err) {
69
+ console.error('spindle: Error in PassageFooter:', err);
70
+ return null;
71
+ }
72
+ }, [footerPassage?.content]);
73
+
74
+ // Defer PassageDone to after DOM commit
75
+ useEffect(() => {
76
+ if (donePassage) setDoneReady(true);
77
+ return () => setDoneReady(false);
78
+ }, [passage.name]);
79
+
80
+ const doneContent = useMemo(() => {
81
+ if (!doneReady || !donePassage) return null;
82
+ try {
83
+ return renderPassageContent(donePassage);
84
+ } catch (err) {
85
+ console.error('spindle: Error in PassageDone:', err);
86
+ return null;
87
+ }
88
+ }, [doneReady, donePassage?.content]);
89
+
28
90
  return (
29
91
  <div
30
92
  class="passage"
31
93
  data-passage={passage.name}
32
94
  data-tags={passage.tags.join(' ')}
33
95
  >
96
+ {headerContent && <div class="passage-header">{headerContent}</div>}
34
97
  {content}
98
+ {footerContent && <div class="passage-footer">{footerContent}</div>}
99
+ {doneContent && <div hidden>{doneContent}</div>}
35
100
  </div>
36
101
  );
37
102
  }
@@ -0,0 +1,69 @@
1
+ import { createContext } from 'preact';
2
+ import { useMemo } from 'preact/hooks';
3
+ import { tokenize } from '../markup/tokenizer';
4
+ import { buildAST } from '../markup/ast';
5
+ import { renderNodes } from '../markup/render';
6
+ import { useStoryStore } from '../store';
7
+
8
+ export const DialogCloseContext = createContext<(() => void) | null>(null);
9
+
10
+ interface PassageDialogProps {
11
+ passageName?: string;
12
+ fallbackMarkup?: string;
13
+ panelClass?: string;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export function PassageDialog({
18
+ passageName,
19
+ fallbackMarkup,
20
+ panelClass,
21
+ onClose,
22
+ }: PassageDialogProps) {
23
+ const storyData = useStoryStore((s) => s.storyData);
24
+
25
+ const passage = passageName
26
+ ? storyData?.passages.get(passageName)
27
+ : undefined;
28
+ const markup = passage?.content ?? fallbackMarkup;
29
+
30
+ const content = useMemo(() => {
31
+ if (!markup) {
32
+ return <div class="error">Dialog: no content available</div>;
33
+ }
34
+ try {
35
+ const tokens = tokenize(markup);
36
+ const ast = buildAST(tokens);
37
+ return renderNodes(ast);
38
+ } catch (err) {
39
+ return <div class="error">Error in dialog: {(err as Error).message}</div>;
40
+ }
41
+ }, [markup]);
42
+
43
+ const handleBackdrop = (e: MouseEvent) => {
44
+ if ((e.target as HTMLElement).classList.contains('dialog-overlay')) {
45
+ onClose();
46
+ }
47
+ };
48
+
49
+ const cls = panelClass ? `dialog-panel ${panelClass}` : 'dialog-panel';
50
+
51
+ return (
52
+ <DialogCloseContext.Provider value={onClose}>
53
+ <div
54
+ class="dialog-overlay"
55
+ onClick={handleBackdrop}
56
+ >
57
+ <div class={cls}>
58
+ <button
59
+ class="dialog-close"
60
+ onClick={onClose}
61
+ >
62
+ ✕
63
+ </button>
64
+ <div class="dialog-body">{content}</div>
65
+ </div>
66
+ </div>
67
+ </DialogCloseContext.Provider>
68
+ );
69
+ }
@@ -1,7 +1,7 @@
1
1
  import { useStoryStore } from '../store';
2
2
  import { tokenize } from '../markup/tokenizer';
3
3
  import { buildAST } from '../markup/ast';
4
- import { renderNodes } from '../markup/render';
4
+ import { renderInlineNodes } from '../markup/render';
5
5
 
6
6
  const DEFAULT_MARKUP =
7
7
  '<header class="story-menubar">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}';
@@ -16,7 +16,7 @@ export function StoryInterface() {
16
16
  try {
17
17
  const tokens = tokenize(markup);
18
18
  const ast = buildAST(tokens);
19
- return <>{renderNodes(ast)}</>;
19
+ return <>{renderInlineNodes(ast)}</>;
20
20
  } catch (err) {
21
21
  return (
22
22
  <span class="error">
@@ -1,49 +1,36 @@
1
- import { useContext } from 'preact/hooks';
2
- import { renderInlineNodes, LocalsUpdateContext } from '../../markup/render';
3
- import { collectText } from '../../utils/extract-text';
4
- import { useAction } from '../../hooks/use-action';
5
- import { useInterpolate } from '../../hooks/use-interpolate';
6
- import { executeMutation } from '../../execute-mutation';
7
- import { currentSourceLocation } from '../../utils/source-location';
8
- import { registerMacro } from '../../registry';
9
- import type { MacroProps } from '../../registry';
1
+ import { defineMacro } from '../../define-macro';
10
2
 
11
- export function Button({ rawArgs, children = [], className, id }: MacroProps) {
12
- const resolve = useInterpolate();
13
- className = resolve(className);
14
- id = resolve(id);
15
- const { update, getValues } = useContext(LocalsUpdateContext);
3
+ defineMacro({
4
+ name: 'button',
5
+ interpolate: true,
6
+ render({ rawArgs, children = [] }, ctx) {
7
+ const handleClick = () => {
8
+ try {
9
+ ctx.mutate(rawArgs);
10
+ } catch (err) {
11
+ console.error(
12
+ `spindle: Error in {button ${rawArgs}}${ctx.sourceLocation()}:`,
13
+ err,
14
+ );
15
+ }
16
+ };
16
17
 
17
- const handleClick = () => {
18
- try {
19
- executeMutation(rawArgs, getValues(), update);
20
- } catch (err) {
21
- console.error(
22
- `spindle: Error in {button ${rawArgs}}${currentSourceLocation()}:`,
23
- err,
24
- );
25
- }
26
- };
18
+ ctx.useAction({
19
+ type: 'button',
20
+ key: rawArgs,
21
+ authorId: ctx.id,
22
+ label: ctx.collectText(children) || rawArgs,
23
+ perform: handleClick,
24
+ });
27
25
 
28
- useAction({
29
- type: 'button',
30
- key: rawArgs,
31
- authorId: id,
32
- label: collectText(children) || rawArgs,
33
- perform: handleClick,
34
- });
35
-
36
- const cls = className ? `macro-button ${className}` : 'macro-button';
37
-
38
- return (
39
- <button
40
- id={id}
41
- class={cls}
42
- onClick={handleClick}
43
- >
44
- {renderInlineNodes(children)}
45
- </button>
46
- );
47
- }
48
-
49
- registerMacro('button', Button);
26
+ return (
27
+ <button
28
+ id={ctx.id}
29
+ class={ctx.cls}
30
+ onClick={handleClick}
31
+ >
32
+ {ctx.renderInlineNodes(children)}
33
+ </button>
34
+ );
35
+ },
36
+ });
@@ -1,7 +1,5 @@
1
1
  import { useStoryStore } from '../../store';
2
- import { useAction } from '../../hooks/use-action';
3
- import { registerMacro } from '../../registry';
4
- import type { MacroProps } from '../../registry';
2
+ import { defineMacro } from '../../define-macro';
5
3
 
6
4
  function parseArgs(rawArgs: string): { varName: string; label: string } {
7
5
  const match = rawArgs.match(/^\s*(["']?\$\w+["']?)\s+["']?(.+?)["']?\s*$/);
@@ -13,38 +11,37 @@ function parseArgs(rawArgs: string): { varName: string; label: string } {
13
11
  return { varName, label };
14
12
  }
15
13
 
16
- export function Checkbox({ rawArgs, className, id }: MacroProps) {
17
- const { varName, label } = parseArgs(rawArgs);
18
- const name = varName.startsWith('$') ? varName.slice(1) : varName;
14
+ defineMacro({
15
+ name: 'checkbox',
16
+ render({ rawArgs }, ctx) {
17
+ const { varName, label } = parseArgs(rawArgs);
18
+ const name = varName.startsWith('$') ? varName.slice(1) : varName;
19
19
 
20
- const value = useStoryStore((s) => s.variables[name]);
21
- const setVariable = useStoryStore((s) => s.setVariable);
20
+ const value = useStoryStore((s) => s.variables[name]);
21
+ const setVariable = useStoryStore((s) => s.setVariable);
22
22
 
23
- useAction({
24
- type: 'checkbox',
25
- key: `$${name}`,
26
- authorId: id,
27
- label: label || name,
28
- variable: name,
29
- value: !!value,
30
- perform: (v) => setVariable(name, v !== undefined ? !!v : !value),
31
- });
23
+ ctx.useAction({
24
+ type: 'checkbox',
25
+ key: `$${name}`,
26
+ authorId: ctx.id,
27
+ label: label || name,
28
+ variable: name,
29
+ value: !!value,
30
+ perform: (v) => setVariable(name, v !== undefined ? !!v : !value),
31
+ });
32
32
 
33
- const cls = className ? `macro-checkbox ${className}` : 'macro-checkbox';
34
-
35
- return (
36
- <label
37
- id={id}
38
- class={cls}
39
- >
40
- <input
41
- type="checkbox"
42
- checked={!!value}
43
- onChange={() => setVariable(name, !value)}
44
- />
45
- {label ? ` ${label}` : null}
46
- </label>
47
- );
48
- }
49
-
50
- registerMacro('checkbox', Checkbox);
33
+ return (
34
+ <label
35
+ id={ctx.id}
36
+ class={ctx.cls}
37
+ >
38
+ <input
39
+ type="checkbox"
40
+ checked={!!value}
41
+ onChange={() => setVariable(name, !value)}
42
+ />
43
+ {label ? ` ${label}` : null}
44
+ </label>
45
+ );
46
+ },
47
+ });
@@ -1,15 +1,12 @@
1
- import { useLayoutEffect, useRef } from 'preact/hooks';
2
1
  import { useStoryStore } from '../../store';
3
2
  import { evaluate } from '../../expression';
4
- import { useMergedLocals } from '../../hooks/use-merged-locals';
5
3
  import { currentSourceLocation } from '../../utils/source-location';
6
- import { registerMacro } from '../../registry';
7
- import type { MacroProps } from '../../registry';
4
+ import { defineMacro } from '../../define-macro';
5
+ import { MacroError } from './MacroError';
8
6
 
9
7
  function parseComputedArgs(rawArgs: string): { target: string; expr: string } {
10
8
  const trimmed = rawArgs.trim();
11
9
 
12
- // Find the first '=' that isn't part of '==' or '!='
13
10
  let depth = 0;
14
11
  for (let i = 0; i < trimmed.length; i++) {
15
12
  const ch = trimmed[i];
@@ -85,56 +82,54 @@ function computeAndApply(
85
82
  }
86
83
  }
87
84
 
88
- export function Computed({ rawArgs }: MacroProps) {
89
- const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
85
+ defineMacro({
86
+ name: 'computed',
87
+ merged: true,
88
+ render({ rawArgs }, ctx) {
89
+ const [mergedVars, mergedTemps, mergedLocals] = ctx.merged!;
90
90
 
91
- let target: string;
92
- let expr: string;
93
- try {
94
- ({ target, expr } = parseComputedArgs(rawArgs));
95
- } catch (err) {
96
- return (
97
- <span
98
- class="error"
99
- title={String(err)}
100
- >
101
- {`{computed error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
102
- </span>
103
- );
104
- }
105
- const isTemp = target.startsWith('_');
106
- const name = target.slice(1);
107
-
108
- // Synchronous first evaluation — sees preceding synchronous {set} mutations
109
- const ran = useRef(false);
110
- if (!ran.current) {
111
- ran.current = true;
112
- const state = useStoryStore.getState();
113
- computeAndApply(
114
- expr,
115
- name,
116
- isTemp,
117
- state.variables,
118
- state.temporary,
119
- mergedLocals,
120
- rawArgs,
121
- );
122
- }
91
+ let target: string;
92
+ let expr: string;
93
+ try {
94
+ ({ target, expr } = parseComputedArgs(rawArgs));
95
+ } catch (err) {
96
+ return (
97
+ <MacroError
98
+ macro="computed"
99
+ error={err}
100
+ />
101
+ );
102
+ }
103
+ const isTemp = target.startsWith('_');
104
+ const name = target.slice(1);
123
105
 
124
- // Reactive re-evaluation when dependencies change (via useMergedLocals subscription)
125
- useLayoutEffect(() => {
126
- computeAndApply(
127
- expr,
128
- name,
129
- isTemp,
130
- mergedVars,
131
- mergedTemps,
132
- mergedLocals,
133
- rawArgs,
134
- );
135
- }, [mergedVars, mergedTemps, mergedLocals]);
106
+ const ran = ctx.hooks.useRef(false);
107
+ if (!ran.current) {
108
+ ran.current = true;
109
+ const state = useStoryStore.getState();
110
+ computeAndApply(
111
+ expr,
112
+ name,
113
+ isTemp,
114
+ state.variables,
115
+ state.temporary,
116
+ mergedLocals,
117
+ rawArgs,
118
+ );
119
+ }
136
120
 
137
- return null;
138
- }
121
+ ctx.hooks.useLayoutEffect(() => {
122
+ computeAndApply(
123
+ expr,
124
+ name,
125
+ isTemp,
126
+ mergedVars,
127
+ mergedTemps,
128
+ mergedLocals,
129
+ rawArgs,
130
+ );
131
+ }, [mergedVars, mergedTemps, mergedLocals]);
139
132
 
140
- registerMacro('computed', Computed);
133
+ return null;
134
+ },
135
+ });
@@ -1,53 +1,43 @@
1
- import { useStoryStore } from '../../store';
2
- import { extractOptions } from './option-utils';
3
- import { useAction } from '../../hooks/use-action';
4
- import { registerMacro } from '../../registry';
5
- import type { MacroProps } from '../../registry';
6
-
7
- export function Cycle({ rawArgs, children = [], className, id }: MacroProps) {
8
- const varName = rawArgs.trim().replace(/["']/g, '');
9
- const name = varName.startsWith('$') ? varName.slice(1) : varName;
10
-
11
- const value = useStoryStore((s) => s.variables[name]);
12
- const setVariable = useStoryStore((s) => s.setVariable);
13
-
14
- const options = extractOptions(children);
15
-
16
- const handleClick = () => {
17
- if (options.length === 0) return;
18
- const currentIndex = options.indexOf(String(value));
19
- const nextIndex = (currentIndex + 1) % options.length;
20
- setVariable(name, options[nextIndex]);
21
- };
22
-
23
- useAction({
24
- type: 'cycle',
25
- key: `$${name}`,
26
- authorId: id,
27
- label: value == null ? options[0] || '' : String(value),
28
- variable: name,
29
- options,
30
- value,
31
- perform: (v) => {
32
- if (v !== undefined) {
33
- setVariable(name, v);
34
- } else {
35
- handleClick();
36
- }
37
- },
38
- });
39
-
40
- const cls = className ? `macro-cycle ${className}` : 'macro-cycle';
41
-
42
- return (
43
- <button
44
- id={id}
45
- class={cls}
46
- onClick={handleClick}
47
- >
48
- {value == null ? options[0] || '' : String(value)}
49
- </button>
50
- );
51
- }
52
-
53
- registerMacro('cycle', Cycle);
1
+ import { defineMacro } from '../../define-macro';
2
+
3
+ defineMacro({
4
+ name: 'cycle',
5
+ storeVar: true,
6
+ render({ children = [] }, ctx) {
7
+ const options = ctx.extractOptions(children);
8
+
9
+ const handleClick = () => {
10
+ if (options.length === 0) return;
11
+ const currentIndex = options.indexOf(String(ctx.value));
12
+ const nextIndex = (currentIndex + 1) % options.length;
13
+ ctx.setValue!(options[nextIndex]);
14
+ };
15
+
16
+ ctx.useAction({
17
+ type: 'cycle',
18
+ key: `$${ctx.varName}`,
19
+ authorId: ctx.id,
20
+ label: ctx.value == null ? options[0] || '' : String(ctx.value),
21
+ variable: ctx.varName,
22
+ options,
23
+ value: ctx.value,
24
+ perform: (v) => {
25
+ if (v !== undefined) {
26
+ ctx.setValue!(v);
27
+ } else {
28
+ handleClick();
29
+ }
30
+ },
31
+ });
32
+
33
+ return (
34
+ <button
35
+ id={ctx.id}
36
+ class={ctx.cls}
37
+ onClick={handleClick}
38
+ >
39
+ {ctx.value == null ? options[0] || '' : String(ctx.value)}
40
+ </button>
41
+ );
42
+ },
43
+ });
@@ -0,0 +1,40 @@
1
+ import { defineMacro } from '../../define-macro';
2
+ import { PassageDialog } from '../PassageDialog';
3
+
4
+ defineMacro({
5
+ name: 'dialog',
6
+ merged: true,
7
+ render({ rawArgs, children = [] }, ctx) {
8
+ const [open, setOpen] = ctx.hooks.useState(false);
9
+
10
+ let passageName: string;
11
+ try {
12
+ const result = ctx.evaluate!(rawArgs);
13
+ passageName = String(result);
14
+ } catch {
15
+ passageName = rawArgs.replace(/^["']|["']$/g, '').trim();
16
+ }
17
+
18
+ const label = children.length
19
+ ? ctx.renderInlineNodes(children)
20
+ : passageName;
21
+
22
+ return (
23
+ <>
24
+ <button
25
+ id={ctx.id}
26
+ class={ctx.cls}
27
+ onClick={() => setOpen(true)}
28
+ >
29
+ {label}
30
+ </button>
31
+ {open && (
32
+ <PassageDialog
33
+ passageName={passageName}
34
+ onClose={() => setOpen(false)}
35
+ />
36
+ )}
37
+ </>
38
+ );
39
+ },
40
+ });