@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.
- package/dist/pkg/format.js +1 -1
- package/package.json +2 -2
- package/src/action-registry.ts +2 -1
- package/src/components/Passage.tsx +69 -4
- package/src/components/PassageDialog.tsx +69 -0
- package/src/components/StoryInterface.tsx +2 -2
- package/src/components/macros/Button.tsx +33 -46
- package/src/components/macros/Checkbox.tsx +32 -35
- package/src/components/macros/Computed.tsx +49 -54
- package/src/components/macros/Cycle.tsx +43 -53
- package/src/components/macros/Dialog.tsx +40 -0
- package/src/components/macros/Do.tsx +15 -21
- package/src/components/macros/For.tsx +57 -75
- package/src/components/macros/Goto.tsx +18 -22
- package/src/components/macros/If.tsx +39 -53
- package/src/components/macros/Include.tsx +30 -46
- package/src/components/macros/Listbox.tsx +38 -48
- package/src/components/macros/MacroError.tsx +18 -0
- package/src/components/macros/MacroLink.tsx +38 -53
- package/src/components/macros/MenubarButtons.tsx +158 -0
- package/src/components/macros/Meter.tsx +35 -52
- package/src/components/macros/Numberbox.tsx +30 -47
- package/src/components/macros/PassageDisplay.tsx +37 -30
- package/src/components/macros/Print.tsx +19 -36
- package/src/components/macros/Radiobutton.tsx +33 -38
- package/src/components/macros/Repeat.tsx +49 -54
- package/src/components/macros/SaveManager.tsx +400 -0
- package/src/components/macros/Set.tsx +19 -24
- package/src/components/{SettingsDialog.tsx → macros/SettingsControls.tsx} +18 -36
- package/src/components/macros/Span.tsx +8 -0
- package/src/components/macros/Stop.tsx +11 -12
- package/src/components/macros/StoryTitle.tsx +16 -16
- package/src/components/macros/Switch.tsx +41 -59
- package/src/components/macros/Textarea.tsx +26 -45
- package/src/components/macros/Textbox.tsx +27 -44
- package/src/components/macros/Timed.tsx +54 -55
- package/src/components/macros/Type.tsx +61 -62
- package/src/components/macros/Unset.tsx +21 -22
- package/src/components/macros/Widget.tsx +11 -15
- package/src/components/macros/option-utils.ts +13 -0
- package/src/define-macro.ts +154 -0
- package/src/expression.ts +10 -8
- package/src/index.tsx +7 -0
- package/src/macros/register-builtins.ts +5 -7
- package/src/markup/ast.ts +11 -16
- package/src/markup/render.tsx +0 -13
- package/src/saves/save-manager.ts +43 -0
- package/src/saves/types.ts +9 -2
- package/src/store.ts +283 -52
- package/src/story-api.ts +33 -12
- package/src/styles.css +37 -68
- package/src/components/PassageLink.tsx +0 -49
- package/src/components/SaveLoadDialog.tsx +0 -408
- package/src/components/macros/Back.tsx +0 -32
- package/src/components/macros/Forward.tsx +0 -34
- package/src/components/macros/QuickLoad.tsx +0 -42
- package/src/components/macros/QuickSave.tsx +0 -30
- package/src/components/macros/Restart.tsx +0 -35
- package/src/components/macros/Saves.tsx +0 -24
- 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.
|
|
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
|
|
50
|
+
"prepublishOnly": "bun run build && bun run test",
|
|
51
51
|
"prepare": "husky"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
package/src/action-registry.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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 <>{
|
|
19
|
+
return <>{renderInlineNodes(ast)}</>;
|
|
20
20
|
} catch (err) {
|
|
21
21
|
return (
|
|
22
22
|
<span class="error">
|
|
@@ -1,49 +1,36 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
20
|
+
const value = useStoryStore((s) => s.variables[name]);
|
|
21
|
+
const setVariable = useStoryStore((s) => s.setVariable);
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 {
|
|
7
|
-
import
|
|
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
|
-
|
|
89
|
-
|
|
85
|
+
defineMacro({
|
|
86
|
+
name: 'computed',
|
|
87
|
+
merged: true,
|
|
88
|
+
render({ rawArgs }, ctx) {
|
|
89
|
+
const [mergedVars, mergedTemps, mergedLocals] = ctx.merged!;
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
return null;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
@@ -1,53 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
});
|