@rohal12/spindle 0.9.0 → 0.11.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/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/Passage.tsx +3 -2
- package/src/components/PassageLink.tsx +4 -0
- package/src/components/macros/Button.tsx +9 -1
- package/src/components/macros/Computed.tsx +57 -22
- package/src/components/macros/Do.tsx +2 -1
- package/src/components/macros/For.tsx +8 -3
- package/src/components/macros/If.tsx +9 -4
- package/src/components/macros/Include.tsx +6 -1
- package/src/components/macros/MacroLink.tsx +13 -2
- package/src/components/macros/Meter.tsx +4 -0
- package/src/components/macros/PassageDisplay.tsx +4 -0
- package/src/components/macros/Print.tsx +6 -1
- package/src/components/macros/Repeat.tsx +19 -3
- package/src/components/macros/Set.tsx +11 -5
- package/src/components/macros/Switch.tsx +3 -2
- package/src/components/macros/Timed.tsx +4 -0
- package/src/components/macros/Type.tsx +4 -0
- package/src/components/macros/Unset.tsx +6 -3
- package/src/components/macros/VarDisplay.tsx +4 -0
- package/src/components/macros/option-utils.ts +4 -1
- package/src/expression.ts +106 -2
- package/src/hooks/use-interpolate.ts +17 -0
- package/src/index.tsx +4 -1
- package/src/interpolation.ts +44 -0
- package/src/markup/render.tsx +55 -9
- package/src/markup/tokenizer.ts +35 -4
- package/src/utils/source-location.ts +19 -0
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { tokenize } from '../markup/tokenizer';
|
|
|
3
3
|
import { buildAST } from '../markup/ast';
|
|
4
4
|
import { renderNodes } from '../markup/render';
|
|
5
5
|
import type { Passage as PassageData } from '../parser';
|
|
6
|
+
import { sourceLocationOf } from '../utils/source-location';
|
|
6
7
|
|
|
7
8
|
interface PassageProps {
|
|
8
9
|
passage: PassageData;
|
|
@@ -17,8 +18,8 @@ export function Passage({ passage }: PassageProps) {
|
|
|
17
18
|
} catch (err) {
|
|
18
19
|
return (
|
|
19
20
|
<div class="error">
|
|
20
|
-
Error parsing passage “{passage.name}&rdquo
|
|
21
|
-
{(err as Error).message}
|
|
21
|
+
Error parsing passage “{passage.name}”
|
|
22
|
+
{sourceLocationOf(passage)}: {(err as Error).message}
|
|
22
23
|
</div>
|
|
23
24
|
);
|
|
24
25
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useStoryStore } from '../store';
|
|
2
2
|
import { useAction } from '../hooks/use-action';
|
|
3
|
+
import { useInterpolate } from '../hooks/use-interpolate';
|
|
3
4
|
|
|
4
5
|
interface PassageLinkProps {
|
|
5
6
|
target: string;
|
|
@@ -15,6 +16,9 @@ export function PassageLink({
|
|
|
15
16
|
children,
|
|
16
17
|
}: PassageLinkProps) {
|
|
17
18
|
const navigate = useStoryStore((s) => s.navigate);
|
|
19
|
+
const resolve = useInterpolate();
|
|
20
|
+
className = resolve(className);
|
|
21
|
+
id = resolve(id);
|
|
18
22
|
|
|
19
23
|
const handleClick = (e: MouseEvent) => {
|
|
20
24
|
e.preventDefault();
|
|
@@ -6,6 +6,8 @@ import { deepClone } from '../../class-registry';
|
|
|
6
6
|
import { collectText } from '../../utils/extract-text';
|
|
7
7
|
import { useAction } from '../../hooks/use-action';
|
|
8
8
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
9
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
10
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
9
11
|
import type { ASTNode } from '../../markup/ast';
|
|
10
12
|
|
|
11
13
|
interface ButtonProps {
|
|
@@ -16,6 +18,9 @@ interface ButtonProps {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function Button({ rawArgs, children, className, id }: ButtonProps) {
|
|
21
|
+
const resolve = useInterpolate();
|
|
22
|
+
className = resolve(className);
|
|
23
|
+
id = resolve(id);
|
|
19
24
|
const scope = useContext(LocalsContext);
|
|
20
25
|
const [, , mergedLocals] = useMergedLocals();
|
|
21
26
|
|
|
@@ -28,7 +33,10 @@ export function Button({ rawArgs, children, className, id }: ButtonProps) {
|
|
|
28
33
|
try {
|
|
29
34
|
execute(rawArgs, vars, temps, localsClone);
|
|
30
35
|
} catch (err) {
|
|
31
|
-
console.error(
|
|
36
|
+
console.error(
|
|
37
|
+
`spindle: Error in {button ${rawArgs}}${currentSourceLocation()}:`,
|
|
38
|
+
err,
|
|
39
|
+
);
|
|
32
40
|
return;
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useLayoutEffect } from 'preact/hooks';
|
|
1
|
+
import { useLayoutEffect, useRef } from 'preact/hooks';
|
|
2
2
|
import { useStoryStore } from '../../store';
|
|
3
3
|
import { evaluate } from '../../expression';
|
|
4
4
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
5
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
5
6
|
|
|
6
7
|
interface ComputedProps {
|
|
7
8
|
rawArgs: string;
|
|
@@ -58,6 +59,34 @@ function valuesEqual(a: unknown, b: unknown): boolean {
|
|
|
58
59
|
return false;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function computeAndApply(
|
|
63
|
+
expr: string,
|
|
64
|
+
name: string,
|
|
65
|
+
isTemp: boolean,
|
|
66
|
+
variables: Record<string, unknown>,
|
|
67
|
+
temporary: Record<string, unknown>,
|
|
68
|
+
locals: Record<string, unknown>,
|
|
69
|
+
rawArgs: string,
|
|
70
|
+
): void {
|
|
71
|
+
const state = useStoryStore.getState();
|
|
72
|
+
let newValue: unknown;
|
|
73
|
+
try {
|
|
74
|
+
newValue = evaluate(expr, variables, temporary, locals);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(
|
|
77
|
+
`spindle: Error in {computed ${rawArgs}}${currentSourceLocation()}:`,
|
|
78
|
+
err,
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const current = isTemp ? state.temporary[name] : state.variables[name];
|
|
84
|
+
if (!valuesEqual(current, newValue)) {
|
|
85
|
+
if (isTemp) state.setTemporary(name, newValue);
|
|
86
|
+
else state.setVariable(name, newValue);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
export function Computed({ rawArgs }: ComputedProps) {
|
|
62
91
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
63
92
|
|
|
@@ -71,35 +100,41 @@ export function Computed({ rawArgs }: ComputedProps) {
|
|
|
71
100
|
class="error"
|
|
72
101
|
title={String(err)}
|
|
73
102
|
>
|
|
74
|
-
{`{computed error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
103
|
+
{`{computed error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
75
104
|
</span>
|
|
76
105
|
);
|
|
77
106
|
}
|
|
78
107
|
const isTemp = target.startsWith('_');
|
|
79
108
|
const name = target.slice(1);
|
|
80
109
|
|
|
81
|
-
//
|
|
82
|
-
|
|
110
|
+
// Synchronous first evaluation — sees preceding synchronous {set} mutations
|
|
111
|
+
const ran = useRef(false);
|
|
112
|
+
if (!ran.current) {
|
|
113
|
+
ran.current = true;
|
|
83
114
|
const state = useStoryStore.getState();
|
|
115
|
+
computeAndApply(
|
|
116
|
+
expr,
|
|
117
|
+
name,
|
|
118
|
+
isTemp,
|
|
119
|
+
state.variables,
|
|
120
|
+
state.temporary,
|
|
121
|
+
mergedLocals,
|
|
122
|
+
rawArgs,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
state.setTemporary(name, newValue);
|
|
98
|
-
} else {
|
|
99
|
-
state.setVariable(name, newValue);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
126
|
+
// Reactive re-evaluation when dependencies change (via useMergedLocals subscription)
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
computeAndApply(
|
|
129
|
+
expr,
|
|
130
|
+
name,
|
|
131
|
+
isTemp,
|
|
132
|
+
mergedVars,
|
|
133
|
+
mergedTemps,
|
|
134
|
+
mergedLocals,
|
|
135
|
+
rawArgs,
|
|
136
|
+
);
|
|
137
|
+
}, [mergedVars, mergedTemps, mergedLocals]);
|
|
103
138
|
|
|
104
139
|
return null;
|
|
105
140
|
}
|
|
@@ -5,6 +5,7 @@ import type { ASTNode } from '../../markup/ast';
|
|
|
5
5
|
import { deepClone } from '../../class-registry';
|
|
6
6
|
import { LocalsContext } from '../../markup/render';
|
|
7
7
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
8
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
8
9
|
|
|
9
10
|
interface DoProps {
|
|
10
11
|
children: ASTNode[];
|
|
@@ -31,7 +32,7 @@ export function Do({ children }: DoProps) {
|
|
|
31
32
|
try {
|
|
32
33
|
execute(code, vars, temps, localsClone);
|
|
33
34
|
} catch (err) {
|
|
34
|
-
console.error(`spindle: Error in {do}:`, err);
|
|
35
|
+
console.error(`spindle: Error in {do}${currentSourceLocation()}:`, err);
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -3,6 +3,8 @@ import { evaluate } from '../../expression';
|
|
|
3
3
|
import { LocalsContext, renderNodes } from '../../markup/render';
|
|
4
4
|
import type { LocalsScope } from '../../markup/render';
|
|
5
5
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
7
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
6
8
|
import type { ASTNode } from '../../markup/ast';
|
|
7
9
|
|
|
8
10
|
interface ForProps {
|
|
@@ -75,6 +77,9 @@ function ForIteration({
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export function For({ rawArgs, children, className, id }: ForProps) {
|
|
80
|
+
const resolve = useInterpolate();
|
|
81
|
+
className = resolve(className);
|
|
82
|
+
id = resolve(id);
|
|
78
83
|
const parentScope = useContext(LocalsContext);
|
|
79
84
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
80
85
|
|
|
@@ -87,7 +92,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
|
|
|
87
92
|
class="error"
|
|
88
93
|
title={String(err)}
|
|
89
94
|
>
|
|
90
|
-
{`{for error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
95
|
+
{`{for error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
91
96
|
</span>
|
|
92
97
|
);
|
|
93
98
|
}
|
|
@@ -100,7 +105,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
|
|
|
100
105
|
if (!Array.isArray(result)) {
|
|
101
106
|
return (
|
|
102
107
|
<span class="error">
|
|
103
|
-
{`{for error: expression did not evaluate to an array}`}
|
|
108
|
+
{`{for error${currentSourceLocation()}: expression did not evaluate to an array}`}
|
|
104
109
|
</span>
|
|
105
110
|
);
|
|
106
111
|
}
|
|
@@ -111,7 +116,7 @@ export function For({ rawArgs, children, className, id }: ForProps) {
|
|
|
111
116
|
class="error"
|
|
112
117
|
title={String(err)}
|
|
113
118
|
>
|
|
114
|
-
{`{for error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
119
|
+
{`{for error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
115
120
|
</span>
|
|
116
121
|
);
|
|
117
122
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { evaluate } from '../../expression';
|
|
2
2
|
import { renderNodes } from '../../markup/render';
|
|
3
3
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
4
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
5
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
4
6
|
import type { Branch } from '../../markup/ast';
|
|
5
7
|
|
|
6
8
|
interface IfProps {
|
|
@@ -8,15 +10,18 @@ interface IfProps {
|
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export function If({ branches }: IfProps) {
|
|
13
|
+
const resolve = useInterpolate();
|
|
11
14
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
12
15
|
|
|
13
16
|
function renderBranch(branch: Branch) {
|
|
14
17
|
const children = renderNodes(branch.children);
|
|
15
|
-
|
|
18
|
+
const cls = resolve(branch.className);
|
|
19
|
+
const branchId = resolve(branch.id);
|
|
20
|
+
if (cls || branchId)
|
|
16
21
|
return (
|
|
17
22
|
<span
|
|
18
|
-
id={
|
|
19
|
-
class={
|
|
23
|
+
id={branchId}
|
|
24
|
+
class={cls}
|
|
20
25
|
>
|
|
21
26
|
{children}
|
|
22
27
|
</span>
|
|
@@ -46,7 +51,7 @@ export function If({ branches }: IfProps) {
|
|
|
46
51
|
class="error"
|
|
47
52
|
title={String(err)}
|
|
48
53
|
>
|
|
49
|
-
{`{if error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
54
|
+
{`{if error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
50
55
|
</span>
|
|
51
56
|
);
|
|
52
57
|
}
|
|
@@ -4,6 +4,8 @@ import { tokenize } from '../../markup/tokenizer';
|
|
|
4
4
|
import { buildAST } from '../../markup/ast';
|
|
5
5
|
import { renderNodes } from '../../markup/render';
|
|
6
6
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
7
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
8
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
7
9
|
|
|
8
10
|
interface IncludeProps {
|
|
9
11
|
rawArgs: string;
|
|
@@ -12,6 +14,9 @@ interface IncludeProps {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function Include({ rawArgs, className, id }: IncludeProps) {
|
|
17
|
+
const resolve = useInterpolate();
|
|
18
|
+
className = resolve(className);
|
|
19
|
+
id = resolve(id);
|
|
15
20
|
const storyData = useStoryStore((s) => s.storyData);
|
|
16
21
|
const [variables, temporary, locals] = useMergedLocals();
|
|
17
22
|
|
|
@@ -31,7 +36,7 @@ export function Include({ rawArgs, className, id }: IncludeProps) {
|
|
|
31
36
|
}
|
|
32
37
|
if (!passage) {
|
|
33
38
|
return (
|
|
34
|
-
<span class="error">{`{include: passage "${passageName}" not found}`}</span>
|
|
39
|
+
<span class="error">{`{include${currentSourceLocation()}: passage "${passageName}" not found}`}</span>
|
|
35
40
|
);
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -5,6 +5,8 @@ import type { ASTNode } from '../../markup/ast';
|
|
|
5
5
|
import { deepClone } from '../../class-registry';
|
|
6
6
|
import { LocalsContext } from '../../markup/render';
|
|
7
7
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
8
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
9
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
8
10
|
|
|
9
11
|
interface MacroLinkProps {
|
|
10
12
|
rawArgs: string;
|
|
@@ -56,14 +58,20 @@ function executeChildren(
|
|
|
56
58
|
try {
|
|
57
59
|
execute(node.rawArgs, vars, temps, localsClone);
|
|
58
60
|
} catch (err) {
|
|
59
|
-
console.error(
|
|
61
|
+
console.error(
|
|
62
|
+
`spindle: Error in {link} child {set}${currentSourceLocation()}:`,
|
|
63
|
+
err,
|
|
64
|
+
);
|
|
60
65
|
}
|
|
61
66
|
} else if (node.name === 'do') {
|
|
62
67
|
const code = collectText(node.children);
|
|
63
68
|
try {
|
|
64
69
|
execute(code, vars, temps, localsClone);
|
|
65
70
|
} catch (err) {
|
|
66
|
-
console.error(
|
|
71
|
+
console.error(
|
|
72
|
+
`spindle: Error in {link} child {do}${currentSourceLocation()}:`,
|
|
73
|
+
err,
|
|
74
|
+
);
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -92,6 +100,9 @@ export function MacroLink({
|
|
|
92
100
|
className,
|
|
93
101
|
id,
|
|
94
102
|
}: MacroLinkProps) {
|
|
103
|
+
const resolve = useInterpolate();
|
|
104
|
+
className = resolve(className);
|
|
105
|
+
id = resolve(id);
|
|
95
106
|
const { display, passage } = parseArgs(rawArgs);
|
|
96
107
|
const scope = useContext(LocalsContext);
|
|
97
108
|
const [, , mergedLocals] = useMergedLocals();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluate } from '../../expression';
|
|
2
2
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
3
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
3
4
|
|
|
4
5
|
interface MeterProps {
|
|
5
6
|
rawArgs: string;
|
|
@@ -83,6 +84,9 @@ function formatLabel(
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function Meter({ rawArgs, className, id }: MeterProps) {
|
|
87
|
+
const resolve = useInterpolate();
|
|
88
|
+
className = resolve(className);
|
|
89
|
+
id = resolve(id);
|
|
86
90
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
87
91
|
|
|
88
92
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { Passage } from '../Passage';
|
|
3
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
3
4
|
|
|
4
5
|
interface PassageDisplayProps {
|
|
5
6
|
className?: string;
|
|
@@ -7,6 +8,9 @@ interface PassageDisplayProps {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function PassageDisplay({ className, id }: PassageDisplayProps) {
|
|
11
|
+
const resolve = useInterpolate();
|
|
12
|
+
className = resolve(className);
|
|
13
|
+
id = resolve(id);
|
|
10
14
|
const currentPassage = useStoryStore((s) => s.currentPassage);
|
|
11
15
|
const storyData = useStoryStore((s) => s.storyData);
|
|
12
16
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { evaluate } from '../../expression';
|
|
2
2
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
3
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
4
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
3
5
|
|
|
4
6
|
interface PrintProps {
|
|
5
7
|
rawArgs: string;
|
|
@@ -8,6 +10,9 @@ interface PrintProps {
|
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export function Print({ rawArgs, className, id }: PrintProps) {
|
|
13
|
+
const resolve = useInterpolate();
|
|
14
|
+
className = resolve(className);
|
|
15
|
+
id = resolve(id);
|
|
11
16
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
12
17
|
|
|
13
18
|
try {
|
|
@@ -29,7 +34,7 @@ export function Print({ rawArgs, className, id }: PrintProps) {
|
|
|
29
34
|
class="error"
|
|
30
35
|
title={String(err)}
|
|
31
36
|
>
|
|
32
|
-
{`{print error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
37
|
+
{`{print error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
33
38
|
</span>
|
|
34
39
|
);
|
|
35
40
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createContext } from 'preact';
|
|
2
|
-
import { useState, useEffect, useCallback } from 'preact/hooks';
|
|
2
|
+
import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';
|
|
3
3
|
import { renderNodes } from '../../markup/render';
|
|
4
4
|
import { parseDelay } from '../../utils/parse-delay';
|
|
5
|
+
import { hasInterpolation, interpolate } from '../../interpolation';
|
|
6
|
+
import { useStoryStore } from '../../store';
|
|
5
7
|
import type { ASTNode } from '../../markup/ast';
|
|
6
8
|
|
|
7
9
|
export const RepeatContext = createContext<{ stop: () => void }>({
|
|
@@ -15,7 +17,21 @@ interface RepeatProps {
|
|
|
15
17
|
id?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export function Repeat({
|
|
20
|
+
export function Repeat({
|
|
21
|
+
rawArgs,
|
|
22
|
+
children,
|
|
23
|
+
className: rawClassName,
|
|
24
|
+
id: rawId,
|
|
25
|
+
}: RepeatProps) {
|
|
26
|
+
const [className, id] = useMemo(() => {
|
|
27
|
+
const resolveOnce = (s: string | undefined) => {
|
|
28
|
+
if (!s || !hasInterpolation(s)) return s;
|
|
29
|
+
const st = useStoryStore.getState();
|
|
30
|
+
return interpolate(s, st.variables, st.temporary, {});
|
|
31
|
+
};
|
|
32
|
+
return [resolveOnce(rawClassName), resolveOnce(rawId)];
|
|
33
|
+
}, [rawClassName, rawId]);
|
|
34
|
+
|
|
19
35
|
const delay = parseDelay(rawArgs);
|
|
20
36
|
const [count, setCount] = useState(0);
|
|
21
37
|
const [stopped, setStopped] = useState(false);
|
|
@@ -36,7 +52,7 @@ export function Repeat({ rawArgs, children, className, id }: RepeatProps) {
|
|
|
36
52
|
|
|
37
53
|
const content = (
|
|
38
54
|
<RepeatContext.Provider value={{ stop }}>
|
|
39
|
-
{renderNodes(children)}
|
|
55
|
+
<span key={count}>{renderNodes(children)}</span>
|
|
40
56
|
</RepeatContext.Provider>
|
|
41
57
|
);
|
|
42
58
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef, useContext } from 'preact/hooks';
|
|
2
2
|
import { useStoryStore } from '../../store';
|
|
3
3
|
import { execute } from '../../expression';
|
|
4
4
|
import { deepClone } from '../../class-registry';
|
|
5
5
|
import { LocalsContext } from '../../markup/render';
|
|
6
6
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
7
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
7
8
|
|
|
8
9
|
interface SetProps {
|
|
9
10
|
rawArgs: string;
|
|
@@ -12,8 +13,10 @@ interface SetProps {
|
|
|
12
13
|
export function Set({ rawArgs }: SetProps) {
|
|
13
14
|
const scope = useContext(LocalsContext);
|
|
14
15
|
const [, , mergedLocals] = useMergedLocals();
|
|
16
|
+
const ran = useRef(false);
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
if (!ran.current) {
|
|
19
|
+
ran.current = true;
|
|
17
20
|
const state = useStoryStore.getState();
|
|
18
21
|
const vars = deepClone(state.variables);
|
|
19
22
|
const temps = deepClone(state.temporary);
|
|
@@ -22,8 +25,11 @@ export function Set({ rawArgs }: SetProps) {
|
|
|
22
25
|
try {
|
|
23
26
|
execute(rawArgs, vars, temps, localsClone);
|
|
24
27
|
} catch (err) {
|
|
25
|
-
console.error(
|
|
26
|
-
|
|
28
|
+
console.error(
|
|
29
|
+
`spindle: Error in {set ${rawArgs}}${currentSourceLocation()}:`,
|
|
30
|
+
err,
|
|
31
|
+
);
|
|
32
|
+
return null;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
// Diff and apply store changes
|
|
@@ -44,7 +50,7 @@ export function Set({ rawArgs }: SetProps) {
|
|
|
44
50
|
scope.update(`@${key}`, localsClone[key]);
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
|
-
}
|
|
53
|
+
}
|
|
48
54
|
|
|
49
55
|
return null;
|
|
50
56
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { evaluate } from '../../expression';
|
|
2
2
|
import { renderNodes } from '../../markup/render';
|
|
3
3
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
4
|
+
import { currentSourceLocation } from '../../utils/source-location';
|
|
4
5
|
import type { Branch } from '../../markup/ast';
|
|
5
6
|
|
|
6
7
|
interface SwitchProps {
|
|
@@ -20,7 +21,7 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
|
|
|
20
21
|
class="error"
|
|
21
22
|
title={String(err)}
|
|
22
23
|
>
|
|
23
|
-
{`{switch error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
24
|
+
{`{switch error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
24
25
|
</span>
|
|
25
26
|
);
|
|
26
27
|
}
|
|
@@ -52,7 +53,7 @@ export function Switch({ rawArgs, branches }: SwitchProps) {
|
|
|
52
53
|
class="error"
|
|
53
54
|
title={String(err)}
|
|
54
55
|
>
|
|
55
|
-
{`{case error: ${err instanceof Error ? err.message : String(err)}}`}
|
|
56
|
+
{`{case error${currentSourceLocation()}: ${err instanceof Error ? err.message : String(err)}}`}
|
|
56
57
|
</span>
|
|
57
58
|
);
|
|
58
59
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo } from 'preact/hooks';
|
|
2
2
|
import { renderNodes } from '../../markup/render';
|
|
3
3
|
import { parseDelay } from '../../utils/parse-delay';
|
|
4
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
4
5
|
import type { ASTNode, Branch } from '../../markup/ast';
|
|
5
6
|
|
|
6
7
|
interface TimedProps {
|
|
@@ -18,6 +19,9 @@ export function Timed({
|
|
|
18
19
|
className,
|
|
19
20
|
id,
|
|
20
21
|
}: TimedProps) {
|
|
22
|
+
const resolve = useInterpolate();
|
|
23
|
+
className = resolve(className);
|
|
24
|
+
id = resolve(id);
|
|
21
25
|
// Section 0 = initial children, sections 1..N = {next} branches
|
|
22
26
|
// Each section has its own delay
|
|
23
27
|
const sections = useMemo(() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
2
2
|
import { renderInlineNodes } from '../../markup/render';
|
|
3
3
|
import { parseDelay } from '../../utils/parse-delay';
|
|
4
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
4
5
|
import type { ASTNode } from '../../markup/ast';
|
|
5
6
|
|
|
6
7
|
interface TypeProps {
|
|
@@ -11,6 +12,9 @@ interface TypeProps {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function Type({ rawArgs, children, className, id }: TypeProps) {
|
|
15
|
+
const resolve = useInterpolate();
|
|
16
|
+
className = resolve(className);
|
|
17
|
+
id = resolve(id);
|
|
14
18
|
const speed = parseDelay(rawArgs);
|
|
15
19
|
const containerRef = useRef<HTMLSpanElement>(null);
|
|
16
20
|
const [totalChars, setTotalChars] = useState(0);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef } from 'preact/hooks';
|
|
2
2
|
import { useStoryStore } from '../../store';
|
|
3
3
|
|
|
4
4
|
interface UnsetProps {
|
|
@@ -6,7 +6,10 @@ interface UnsetProps {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function Unset({ rawArgs }: UnsetProps) {
|
|
9
|
-
|
|
9
|
+
const ran = useRef(false);
|
|
10
|
+
|
|
11
|
+
if (!ran.current) {
|
|
12
|
+
ran.current = true;
|
|
10
13
|
const state = useStoryStore.getState();
|
|
11
14
|
const name = rawArgs.trim();
|
|
12
15
|
|
|
@@ -19,7 +22,7 @@ export function Unset({ rawArgs }: UnsetProps) {
|
|
|
19
22
|
`spindle: {unset} expects a variable ($name or _name), got "${name}"`,
|
|
20
23
|
);
|
|
21
24
|
}
|
|
22
|
-
}
|
|
25
|
+
}
|
|
23
26
|
|
|
24
27
|
return null;
|
|
25
28
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { useContext } from 'preact/hooks';
|
|
3
3
|
import { LocalsContext } from '../../markup/render';
|
|
4
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
4
5
|
|
|
5
6
|
interface VarDisplayProps {
|
|
6
7
|
name: string;
|
|
@@ -10,6 +11,9 @@ interface VarDisplayProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
14
|
+
const resolve = useInterpolate();
|
|
15
|
+
className = resolve(className);
|
|
16
|
+
id = resolve(id);
|
|
13
17
|
const localsScope = useContext(LocalsContext);
|
|
14
18
|
const parts = name.split('.');
|
|
15
19
|
const root = parts[0]!;
|
|
@@ -7,7 +7,10 @@ export function extractOptions(children: ASTNode[]): string[] {
|
|
|
7
7
|
const options: string[] = [];
|
|
8
8
|
for (const node of children) {
|
|
9
9
|
if (node.type === 'macro' && node.name === 'option') {
|
|
10
|
-
|
|
10
|
+
const raw = node.rawArgs.trim();
|
|
11
|
+
// Strip surrounding quotes so {option "Long Sword"} gives "Long Sword"
|
|
12
|
+
const stripped = raw.replace(/^(["'])(.+)\1$/, '$2');
|
|
13
|
+
options.push(stripped);
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
return options;
|