@rohal12/spindle 0.3.2 → 0.4.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.
- package/README.md +9 -0
- package/dist/pkg/format.js +1 -1
- package/package.json +8 -2
- package/src/automation/runner.ts +1 -1
- package/src/components/SaveLoadDialog.tsx +8 -3
- package/src/components/macros/Checkbox.tsx +2 -2
- package/src/components/macros/Computed.tsx +22 -17
- package/src/components/macros/For.tsx +12 -19
- package/src/components/macros/If.tsx +3 -15
- package/src/components/macros/MacroLink.tsx +3 -3
- package/src/components/macros/Meter.tsx +12 -22
- package/src/components/macros/Numberbox.tsx +1 -1
- package/src/components/macros/Print.tsx +3 -15
- package/src/components/macros/Radiobutton.tsx +5 -5
- package/src/components/macros/Switch.tsx +5 -15
- package/src/components/macros/Textarea.tsx +1 -1
- package/src/components/macros/Textbox.tsx +1 -1
- package/src/components/macros/Timed.tsx +13 -14
- package/src/components/macros/VarDisplay.tsx +3 -2
- package/src/expression.ts +82 -10
- package/src/hooks/use-merged-locals.ts +26 -0
- package/src/markup/ast.ts +12 -7
- package/src/markup/render.tsx +13 -6
- package/src/markup/tokenizer.ts +12 -12
- package/src/parser.ts +16 -1
- package/src/prng.ts +128 -0
- package/src/saves/save-manager.ts +25 -10
- package/src/saves/types.ts +31 -0
- package/src/settings.ts +26 -1
- package/src/store.ts +101 -35
- package/src/story-api.ts +63 -0
- package/src/story-variables.ts +18 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rohal12/spindle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A Preact-based story format for Twine 2.",
|
|
6
6
|
"license": "Unlicense",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"docs:dev": "vitepress dev docs",
|
|
47
47
|
"docs:build": "vitepress build docs",
|
|
48
48
|
"docs:preview": "vitepress preview docs",
|
|
49
|
-
"prepublishOnly": "bun run test && bun run build"
|
|
49
|
+
"prepublishOnly": "bun run test && bun run build",
|
|
50
|
+
"prepare": "husky"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"immer": "^11.1.4",
|
|
@@ -56,13 +57,18 @@
|
|
|
56
57
|
"preact": "^10.28.4",
|
|
57
58
|
"zustand": "^5.0.11"
|
|
58
59
|
},
|
|
60
|
+
"lint-staged": {
|
|
61
|
+
"*": "prettier --write --ignore-unknown"
|
|
62
|
+
},
|
|
59
63
|
"devDependencies": {
|
|
60
64
|
"@preact/preset-vite": "^2.10.3",
|
|
61
65
|
"@rohal12/twee-ts": "^1.1.2",
|
|
62
66
|
"@types/js-yaml": "^4.0.9",
|
|
63
67
|
"@vitest/coverage-v8": "^4.0.18",
|
|
64
68
|
"happy-dom": "^20.8.3",
|
|
69
|
+
"husky": "^9.1.7",
|
|
65
70
|
"js-yaml": "^4.1.1",
|
|
71
|
+
"lint-staged": "^16.3.2",
|
|
66
72
|
"playwright": "^1.58.2",
|
|
67
73
|
"prettier": "^3.8.1",
|
|
68
74
|
"typescript": "^5.9.3",
|
package/src/automation/runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
2
2
|
import { useStoryStore } from '../store';
|
|
3
|
-
import type
|
|
3
|
+
import { isSaveExport, type SaveRecord } from '../saves/types';
|
|
4
4
|
import {
|
|
5
5
|
getSavesGrouped,
|
|
6
6
|
createSave,
|
|
@@ -45,7 +45,7 @@ export function SaveLoadDialog({ onClose }: SaveLoadDialogProps) {
|
|
|
45
45
|
text: string;
|
|
46
46
|
type: 'success' | 'error';
|
|
47
47
|
} | null>(null);
|
|
48
|
-
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
|
48
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set());
|
|
49
49
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
50
50
|
const [renameValue, setRenameValue] = useState('');
|
|
51
51
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -75,9 +75,11 @@ export function SaveLoadDialog({ onClose }: SaveLoadDialogProps) {
|
|
|
75
75
|
}
|
|
76
76
|
}, [renamingId]);
|
|
77
77
|
|
|
78
|
+
const statusTimer = useRef<number>();
|
|
78
79
|
const showStatus = (text: string, type: 'success' | 'error' = 'success') => {
|
|
80
|
+
clearTimeout(statusTimer.current);
|
|
79
81
|
setStatus({ text, type });
|
|
80
|
-
setTimeout(() => setStatus(null), 3000);
|
|
82
|
+
statusTimer.current = window.setTimeout(() => setStatus(null), 3000);
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
const toggleCollapse = (id: string) => {
|
|
@@ -187,6 +189,9 @@ export function SaveLoadDialog({ onClose }: SaveLoadDialogProps) {
|
|
|
187
189
|
try {
|
|
188
190
|
const text = await file.text();
|
|
189
191
|
const data = JSON.parse(text);
|
|
192
|
+
if (!isSaveExport(data)) {
|
|
193
|
+
throw new Error('Invalid save file format');
|
|
194
|
+
}
|
|
190
195
|
await importSave(data, ifid);
|
|
191
196
|
showStatus('Save imported');
|
|
192
197
|
await refresh();
|
|
@@ -12,8 +12,8 @@ function parseArgs(rawArgs: string): { varName: string; label: string } {
|
|
|
12
12
|
if (!match) {
|
|
13
13
|
return { varName: rawArgs.trim(), label: '' };
|
|
14
14
|
}
|
|
15
|
-
const varName = match[1]
|
|
16
|
-
const label = match[2]
|
|
15
|
+
const varName = match[1]!.replace(/["']/g, '');
|
|
16
|
+
const label = match[2]!;
|
|
17
17
|
return { varName, label };
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useLayoutEffect } from 'preact/hooks';
|
|
2
|
-
import { useContext } from 'preact/hooks';
|
|
3
2
|
import { useStoryStore } from '../../store';
|
|
4
3
|
import { evaluate } from '../../expression';
|
|
5
|
-
import {
|
|
4
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
5
|
|
|
7
6
|
interface ComputedProps {
|
|
8
7
|
rawArgs: string;
|
|
@@ -50,18 +49,32 @@ function valuesEqual(a: unknown, b: unknown): boolean {
|
|
|
50
49
|
typeof b === 'object' &&
|
|
51
50
|
b !== null
|
|
52
51
|
) {
|
|
53
|
-
|
|
52
|
+
try {
|
|
53
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
return false;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
export function Computed({ rawArgs }: ComputedProps) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const [mergedVars, mergedTemps] = useMergedLocals();
|
|
63
|
+
|
|
64
|
+
let target: string;
|
|
65
|
+
let expr: string;
|
|
66
|
+
try {
|
|
67
|
+
({ target, expr } = parseComputedArgs(rawArgs));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return (
|
|
70
|
+
<span
|
|
71
|
+
class="error"
|
|
72
|
+
title={String(err)}
|
|
73
|
+
>
|
|
74
|
+
{`{computed error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
65
78
|
const isTemp = target.startsWith('_');
|
|
66
79
|
const name = target.slice(1);
|
|
67
80
|
|
|
@@ -69,14 +82,6 @@ export function Computed({ rawArgs }: ComputedProps) {
|
|
|
69
82
|
useLayoutEffect(() => {
|
|
70
83
|
const state = useStoryStore.getState();
|
|
71
84
|
|
|
72
|
-
// Merge locals from for-loops
|
|
73
|
-
const mergedVars = { ...state.variables };
|
|
74
|
-
const mergedTemps = { ...state.temporary };
|
|
75
|
-
for (const [key, val] of Object.entries(locals)) {
|
|
76
|
-
if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
|
|
77
|
-
else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
85
|
let newValue: unknown;
|
|
81
86
|
try {
|
|
82
87
|
newValue = evaluate(expr, mergedVars, mergedTemps);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { useStoryStore } from '../../store';
|
|
2
1
|
import { useContext } from 'preact/hooks';
|
|
3
2
|
import { evaluate } from '../../expression';
|
|
4
|
-
import { LocalsContext } from '../../markup/render';
|
|
5
|
-
import {
|
|
3
|
+
import { LocalsContext, renderNodes } from '../../markup/render';
|
|
4
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
5
|
import type { ASTNode } from '../../markup/ast';
|
|
7
6
|
|
|
8
7
|
interface ForProps {
|
|
@@ -29,24 +28,15 @@ function parseForArgs(rawArgs: string): {
|
|
|
29
28
|
const listExpr = rawArgs.slice(ofIdx + 4).trim();
|
|
30
29
|
|
|
31
30
|
const vars = varsPart.split(',').map((v) => v.trim());
|
|
32
|
-
const itemVar = vars[0]
|
|
33
|
-
const indexVar = vars.length > 1 ? vars[1] : null;
|
|
31
|
+
const itemVar = vars[0]!;
|
|
32
|
+
const indexVar = vars.length > 1 ? vars[1]! : null;
|
|
34
33
|
|
|
35
34
|
return { itemVar, indexVar, listExpr };
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export function For({ rawArgs, children, className, id }: ForProps) {
|
|
39
|
-
const variables = useStoryStore((s) => s.variables);
|
|
40
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
41
38
|
const parentLocals = useContext(LocalsContext);
|
|
42
|
-
|
|
43
|
-
// Merge parent locals for expression evaluation
|
|
44
|
-
const mergedVars = { ...variables };
|
|
45
|
-
const mergedTemps = { ...temporary };
|
|
46
|
-
for (const [key, val] of Object.entries(parentLocals)) {
|
|
47
|
-
if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
|
|
48
|
-
else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
|
|
49
|
-
}
|
|
39
|
+
const [mergedVars, mergedTemps] = useMergedLocals();
|
|
50
40
|
|
|
51
41
|
let parsed: ReturnType<typeof parseForArgs>;
|
|
52
42
|
try {
|
|
@@ -57,7 +47,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
|
|
|
57
47
|
class="error"
|
|
58
48
|
title={String(err)}
|
|
59
49
|
>
|
|
60
|
-
{`{for error: ${
|
|
50
|
+
{`{for error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
61
51
|
</span>
|
|
62
52
|
);
|
|
63
53
|
}
|
|
@@ -81,14 +71,17 @@ export function For({ rawArgs, children, className, id }: ForProps) {
|
|
|
81
71
|
class="error"
|
|
82
72
|
title={String(err)}
|
|
83
73
|
>
|
|
84
|
-
{`{for error: ${
|
|
74
|
+
{`{for error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
85
75
|
</span>
|
|
86
76
|
);
|
|
87
77
|
}
|
|
88
78
|
|
|
89
79
|
const content = list.map((item, i) => {
|
|
90
|
-
const locals = {
|
|
91
|
-
|
|
80
|
+
const locals = {
|
|
81
|
+
...parentLocals,
|
|
82
|
+
[itemVar]: item,
|
|
83
|
+
...(indexVar ? { [indexVar]: i } : undefined),
|
|
84
|
+
};
|
|
92
85
|
|
|
93
86
|
return (
|
|
94
87
|
<LocalsContext.Provider
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { useStoryStore } from '../../store';
|
|
2
|
-
import { useContext } from 'preact/hooks';
|
|
3
1
|
import { evaluate } from '../../expression';
|
|
4
|
-
import { LocalsContext } from '../../markup/render';
|
|
5
2
|
import { renderNodes } from '../../markup/render';
|
|
3
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
4
|
import type { Branch } from '../../markup/ast';
|
|
7
5
|
|
|
8
6
|
interface IfProps {
|
|
@@ -10,17 +8,7 @@ interface IfProps {
|
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export function If({ branches }: IfProps) {
|
|
13
|
-
const
|
|
14
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
15
|
-
const locals = useContext(LocalsContext);
|
|
16
|
-
|
|
17
|
-
// Merge locals for expression evaluation
|
|
18
|
-
const mergedVars = { ...variables };
|
|
19
|
-
const mergedTemps = { ...temporary };
|
|
20
|
-
for (const [key, val] of Object.entries(locals)) {
|
|
21
|
-
if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
|
|
22
|
-
else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
|
|
23
|
-
}
|
|
11
|
+
const [mergedVars, mergedTemps] = useMergedLocals();
|
|
24
12
|
|
|
25
13
|
function renderBranch(branch: Branch) {
|
|
26
14
|
const children = renderNodes(branch.children);
|
|
@@ -53,7 +41,7 @@ export function If({ branches }: IfProps) {
|
|
|
53
41
|
class="error"
|
|
54
42
|
title={String(err)}
|
|
55
43
|
>
|
|
56
|
-
{`{if error: ${
|
|
44
|
+
{`{if error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
57
45
|
</span>
|
|
58
46
|
);
|
|
59
47
|
}
|
|
@@ -19,13 +19,13 @@ function parseArgs(rawArgs: string): {
|
|
|
19
19
|
const re = /["']([^"']+)["']/g;
|
|
20
20
|
let m;
|
|
21
21
|
while ((m = re.exec(rawArgs)) !== null) {
|
|
22
|
-
parts.push(m[1]);
|
|
22
|
+
parts.push(m[1]!);
|
|
23
23
|
}
|
|
24
24
|
if (parts.length >= 2) {
|
|
25
|
-
return { display: parts[0]
|
|
25
|
+
return { display: parts[0]!, passage: parts[1]! };
|
|
26
26
|
}
|
|
27
27
|
if (parts.length === 1) {
|
|
28
|
-
return { display: parts[0]
|
|
28
|
+
return { display: parts[0]!, passage: null };
|
|
29
29
|
}
|
|
30
30
|
// Fallback: treat entire rawArgs as display text
|
|
31
31
|
return { display: rawArgs.trim(), passage: null };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { useStoryStore } from '../../store';
|
|
2
|
-
import { useContext } from 'preact/hooks';
|
|
3
1
|
import { evaluate } from '../../expression';
|
|
4
|
-
import {
|
|
2
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
5
3
|
|
|
6
4
|
interface MeterProps {
|
|
7
5
|
rawArgs: string;
|
|
@@ -25,8 +23,8 @@ function parseArgs(rawArgs: string): {
|
|
|
25
23
|
let rest = trimmed;
|
|
26
24
|
const quoteMatch = rest.match(/\s+(?:"([^"]*)"|'([^']*)')$/);
|
|
27
25
|
if (quoteMatch) {
|
|
28
|
-
labelMode = quoteMatch[1] ?? quoteMatch[2];
|
|
29
|
-
rest = rest.slice(0, quoteMatch.index
|
|
26
|
+
labelMode = quoteMatch[1] ?? quoteMatch[2] ?? '';
|
|
27
|
+
rest = rest.slice(0, quoteMatch.index ?? rest.length).trim();
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// Split remaining into two expressions.
|
|
@@ -40,7 +38,7 @@ function parseArgs(rawArgs: string): {
|
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
return {
|
|
43
|
-
currentExpr: exprs[0]
|
|
41
|
+
currentExpr: exprs[0]!,
|
|
44
42
|
maxExpr: exprs.slice(1).join(' '),
|
|
45
43
|
labelMode,
|
|
46
44
|
};
|
|
@@ -52,7 +50,7 @@ function splitExpressions(input: string): string[] {
|
|
|
52
50
|
let depth = 0;
|
|
53
51
|
|
|
54
52
|
for (let i = 0; i < input.length; i++) {
|
|
55
|
-
const ch = input[i]
|
|
53
|
+
const ch = input[i]!;
|
|
56
54
|
if (ch === '(' || ch === '[') {
|
|
57
55
|
depth++;
|
|
58
56
|
current += ch;
|
|
@@ -63,7 +61,7 @@ function splitExpressions(input: string): string[] {
|
|
|
63
61
|
result.push(current);
|
|
64
62
|
current = '';
|
|
65
63
|
// Skip additional whitespace
|
|
66
|
-
while (i + 1 < input.length && /\s/.test(input[i + 1])) i++;
|
|
64
|
+
while (i + 1 < input.length && /\s/.test(input[i + 1]!)) i++;
|
|
67
65
|
} else {
|
|
68
66
|
current += ch;
|
|
69
67
|
}
|
|
@@ -78,29 +76,21 @@ function formatLabel(
|
|
|
78
76
|
labelMode: string,
|
|
79
77
|
): string | null {
|
|
80
78
|
if (labelMode === 'none') return null;
|
|
81
|
-
if (labelMode === '%')
|
|
79
|
+
if (labelMode === '%')
|
|
80
|
+
return `${max === 0 ? 0 : Math.round((current / max) * 100)}%`;
|
|
82
81
|
if (labelMode) return `${current} ${labelMode} / ${max} ${labelMode}`;
|
|
83
82
|
return `${current} / ${max}`;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
export function Meter({ rawArgs, className, id }: MeterProps) {
|
|
87
|
-
const
|
|
88
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
89
|
-
const locals = useContext(LocalsContext);
|
|
90
|
-
|
|
91
|
-
// Merge locals into variables for expression evaluation
|
|
92
|
-
const mergedVars = { ...variables };
|
|
93
|
-
const mergedTemps = { ...temporary };
|
|
94
|
-
for (const [key, val] of Object.entries(locals)) {
|
|
95
|
-
if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
|
|
96
|
-
else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
|
|
97
|
-
}
|
|
86
|
+
const [mergedVars, mergedTemps] = useMergedLocals();
|
|
98
87
|
|
|
99
88
|
try {
|
|
100
89
|
const { currentExpr, maxExpr, labelMode } = parseArgs(rawArgs);
|
|
101
90
|
const current = Number(evaluate(currentExpr, mergedVars, mergedTemps));
|
|
102
91
|
const max = Number(evaluate(maxExpr, mergedVars, mergedTemps));
|
|
103
|
-
const pct =
|
|
92
|
+
const pct =
|
|
93
|
+
max === 0 ? 0 : Math.max(0, Math.min(100, (current / max) * 100));
|
|
104
94
|
const label = formatLabel(current, max, labelMode);
|
|
105
95
|
|
|
106
96
|
const classes = ['macro-meter', className].filter(Boolean).join(' ');
|
|
@@ -123,7 +113,7 @@ export function Meter({ rawArgs, className, id }: MeterProps) {
|
|
|
123
113
|
class="error"
|
|
124
114
|
title={String(err)}
|
|
125
115
|
>
|
|
126
|
-
{`{meter error: ${
|
|
116
|
+
{`{meter error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
127
117
|
</span>
|
|
128
118
|
);
|
|
129
119
|
}
|
|
@@ -12,7 +12,7 @@ function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
|
|
|
12
12
|
if (!match) {
|
|
13
13
|
return { varName: rawArgs.trim(), placeholder: '' };
|
|
14
14
|
}
|
|
15
|
-
const varName = match[1]
|
|
15
|
+
const varName = match[1]!.replace(/["']/g, '');
|
|
16
16
|
const placeholder = match[2] || '';
|
|
17
17
|
return { varName, placeholder };
|
|
18
18
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { useStoryStore } from '../../store';
|
|
2
|
-
import { useContext } from 'preact/hooks';
|
|
3
1
|
import { evaluate } from '../../expression';
|
|
4
|
-
import {
|
|
2
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
5
3
|
|
|
6
4
|
interface PrintProps {
|
|
7
5
|
rawArgs: string;
|
|
@@ -10,17 +8,7 @@ interface PrintProps {
|
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export function Print({ rawArgs, className, id }: PrintProps) {
|
|
13
|
-
const
|
|
14
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
15
|
-
const locals = useContext(LocalsContext);
|
|
16
|
-
|
|
17
|
-
// Merge locals into variables for expression evaluation
|
|
18
|
-
const mergedVars = { ...variables };
|
|
19
|
-
const mergedTemps = { ...temporary };
|
|
20
|
-
for (const [key, val] of Object.entries(locals)) {
|
|
21
|
-
if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
|
|
22
|
-
else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
|
|
23
|
-
}
|
|
11
|
+
const [mergedVars, mergedTemps] = useMergedLocals();
|
|
24
12
|
|
|
25
13
|
try {
|
|
26
14
|
const result = evaluate(rawArgs, mergedVars, mergedTemps);
|
|
@@ -41,7 +29,7 @@ export function Print({ rawArgs, className, id }: PrintProps) {
|
|
|
41
29
|
class="error"
|
|
42
30
|
title={String(err)}
|
|
43
31
|
>
|
|
44
|
-
{`{print error: ${
|
|
32
|
+
{`{print error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
45
33
|
</span>
|
|
46
34
|
);
|
|
47
35
|
}
|
|
@@ -20,15 +20,15 @@ function parseArgs(rawArgs: string): {
|
|
|
20
20
|
// Try simpler: $var value label
|
|
21
21
|
const parts = rawArgs.trim().split(/\s+/);
|
|
22
22
|
return {
|
|
23
|
-
varName: (parts[0]
|
|
24
|
-
value: parts[1]
|
|
23
|
+
varName: (parts[0] ?? '').replace(/["']/g, ''),
|
|
24
|
+
value: parts[1] ?? '',
|
|
25
25
|
label: parts.slice(2).join(' '),
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
return {
|
|
29
|
-
varName: match[1]
|
|
30
|
-
value: match[2]
|
|
31
|
-
label: match[3]
|
|
29
|
+
varName: match[1]!.replace(/["']/g, ''),
|
|
30
|
+
value: match[2]!,
|
|
31
|
+
label: match[3]!,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { useStoryStore } from '../../store';
|
|
2
|
-
import { useContext } from 'preact/hooks';
|
|
3
1
|
import { evaluate } from '../../expression';
|
|
4
|
-
import {
|
|
2
|
+
import { renderNodes } from '../../markup/render';
|
|
3
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
5
4
|
import type { Branch } from '../../markup/ast';
|
|
6
5
|
|
|
7
6
|
interface SwitchProps {
|
|
@@ -10,16 +9,7 @@ interface SwitchProps {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export function Switch({ rawArgs, branches }: SwitchProps) {
|
|
13
|
-
const
|
|
14
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
15
|
-
const locals = useContext(LocalsContext);
|
|
16
|
-
|
|
17
|
-
const mergedVars = { ...variables };
|
|
18
|
-
const mergedTemps = { ...temporary };
|
|
19
|
-
for (const [key, val] of Object.entries(locals)) {
|
|
20
|
-
if (key.startsWith('$')) mergedVars[key.slice(1)] = val;
|
|
21
|
-
else if (key.startsWith('_')) mergedTemps[key.slice(1)] = val;
|
|
22
|
-
}
|
|
12
|
+
const [mergedVars, mergedTemps] = useMergedLocals();
|
|
23
13
|
|
|
24
14
|
let switchValue: unknown;
|
|
25
15
|
try {
|
|
@@ -30,7 +20,7 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
|
|
|
30
20
|
class="error"
|
|
31
21
|
title={String(err)}
|
|
32
22
|
>
|
|
33
|
-
{`{switch error: ${
|
|
23
|
+
{`{switch error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
34
24
|
</span>
|
|
35
25
|
);
|
|
36
26
|
}
|
|
@@ -55,7 +45,7 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
|
|
|
55
45
|
class="error"
|
|
56
46
|
title={String(err)}
|
|
57
47
|
>
|
|
58
|
-
{`{case error: ${
|
|
48
|
+
{`{case error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
59
49
|
</span>
|
|
60
50
|
);
|
|
61
51
|
}
|
|
@@ -12,7 +12,7 @@ function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
|
|
|
12
12
|
if (!match) {
|
|
13
13
|
return { varName: rawArgs.trim(), placeholder: '' };
|
|
14
14
|
}
|
|
15
|
-
const varName = match[1]
|
|
15
|
+
const varName = match[1]!.replace(/["']/g, '');
|
|
16
16
|
const placeholder = match[2] || '';
|
|
17
17
|
return { varName, placeholder };
|
|
18
18
|
}
|
|
@@ -12,7 +12,7 @@ function parseArgs(rawArgs: string): { varName: string; placeholder: string } {
|
|
|
12
12
|
if (!match) {
|
|
13
13
|
return { varName: rawArgs.trim(), placeholder: '' };
|
|
14
14
|
}
|
|
15
|
-
const varName = match[1]
|
|
15
|
+
const varName = match[1]!.replace(/["']/g, '');
|
|
16
16
|
const placeholder = match[2] || '';
|
|
17
17
|
return { varName, placeholder };
|
|
18
18
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'preact/hooks';
|
|
1
|
+
import { useState, useEffect, useMemo } from 'preact/hooks';
|
|
2
2
|
import { renderNodes } from '../../markup/render';
|
|
3
3
|
import { parseDelay } from '../../utils/parse-delay';
|
|
4
4
|
import type { ASTNode, Branch } from '../../markup/ast';
|
|
@@ -20,16 +20,15 @@ export function Timed({
|
|
|
20
20
|
}: TimedProps) {
|
|
21
21
|
// Section 0 = initial children, sections 1..N = {next} branches
|
|
22
22
|
// Each section has its own delay
|
|
23
|
-
const sections
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
23
|
+
const sections = useMemo(() => {
|
|
24
|
+
const result: { delay: number; nodes: ASTNode[] }[] = [];
|
|
25
|
+
result.push({ delay: parseDelay(rawArgs), nodes: children });
|
|
26
|
+
for (const branch of branches) {
|
|
27
|
+
const delay = branch.rawArgs ? parseDelay(branch.rawArgs) : 0;
|
|
28
|
+
result.push({ delay, nodes: branch.children });
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}, [rawArgs, children, branches]);
|
|
33
32
|
|
|
34
33
|
const [visibleIndex, setVisibleIndex] = useState(-1);
|
|
35
34
|
|
|
@@ -37,18 +36,18 @@ export function Timed({
|
|
|
37
36
|
if (visibleIndex >= sections.length - 1) return;
|
|
38
37
|
|
|
39
38
|
const nextIndex = visibleIndex + 1;
|
|
40
|
-
const delay = sections[nextIndex]
|
|
39
|
+
const delay = sections[nextIndex]!.delay;
|
|
41
40
|
|
|
42
41
|
const timer = setTimeout(() => {
|
|
43
42
|
setVisibleIndex(nextIndex);
|
|
44
43
|
}, delay);
|
|
45
44
|
|
|
46
45
|
return () => clearTimeout(timer);
|
|
47
|
-
}, [visibleIndex, sections
|
|
46
|
+
}, [visibleIndex, sections]);
|
|
48
47
|
|
|
49
48
|
if (visibleIndex < 0) return null;
|
|
50
49
|
|
|
51
|
-
const content = renderNodes(sections[visibleIndex]
|
|
50
|
+
const content = renderNodes(sections[visibleIndex]!.nodes);
|
|
52
51
|
|
|
53
52
|
if (className || id)
|
|
54
53
|
return (
|
|
@@ -12,7 +12,7 @@ interface VarDisplayProps {
|
|
|
12
12
|
export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
13
13
|
const locals = useContext(LocalsContext);
|
|
14
14
|
const parts = name.split('.');
|
|
15
|
-
const root = parts[0]
|
|
15
|
+
const root = parts[0]!;
|
|
16
16
|
const storeValue = useStoryStore((s) =>
|
|
17
17
|
scope === 'variable' ? s.variables[root] : s.temporary[root],
|
|
18
18
|
);
|
|
@@ -27,7 +27,8 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
|
27
27
|
value = undefined;
|
|
28
28
|
break;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
const part = parts[i] as string;
|
|
31
|
+
value = (value as Record<string, unknown>)[part];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const display = value == null ? '' : String(value);
|