@rohal12/spindle 0.10.0 → 0.12.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 +0 -1
- package/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/PassageLink.tsx +4 -0
- package/src/components/macros/Button.tsx +6 -26
- package/src/components/macros/Computed.tsx +55 -24
- package/src/components/macros/Do.tsx +2 -27
- package/src/components/macros/For.tsx +4 -0
- package/src/components/macros/If.tsx +7 -3
- package/src/components/macros/Include.tsx +4 -0
- package/src/components/macros/MacroLink.tsx +7 -26
- package/src/components/macros/Meter.tsx +4 -0
- package/src/components/macros/PassageDisplay.tsx +4 -0
- package/src/components/macros/Print.tsx +4 -0
- package/src/components/macros/Repeat.tsx +19 -3
- package/src/components/macros/Set.tsx +8 -31
- 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/execute-mutation.ts +32 -0
- 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/package.json
CHANGED
|
@@ -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();
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { useContext } from 'preact/hooks';
|
|
2
|
-
import { useStoryStore } from '../../store';
|
|
3
|
-
import { execute } from '../../expression';
|
|
4
2
|
import { renderInlineNodes, LocalsContext } from '../../markup/render';
|
|
5
|
-
import { deepClone } from '../../class-registry';
|
|
6
3
|
import { collectText } from '../../utils/extract-text';
|
|
7
4
|
import { useAction } from '../../hooks/use-action';
|
|
8
5
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
7
|
+
import { executeMutation } from '../../execute-mutation';
|
|
9
8
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
10
9
|
import type { ASTNode } from '../../markup/ast';
|
|
11
10
|
|
|
@@ -17,39 +16,20 @@ interface ButtonProps {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export function Button({ rawArgs, children, className, id }: ButtonProps) {
|
|
19
|
+
const resolve = useInterpolate();
|
|
20
|
+
className = resolve(className);
|
|
21
|
+
id = resolve(id);
|
|
20
22
|
const scope = useContext(LocalsContext);
|
|
21
23
|
const [, , mergedLocals] = useMergedLocals();
|
|
22
24
|
|
|
23
25
|
const handleClick = () => {
|
|
24
|
-
const state = useStoryStore.getState();
|
|
25
|
-
const vars = deepClone(state.variables);
|
|
26
|
-
const temps = deepClone(state.temporary);
|
|
27
|
-
const localsClone = { ...mergedLocals };
|
|
28
|
-
|
|
29
26
|
try {
|
|
30
|
-
|
|
27
|
+
executeMutation(rawArgs, mergedLocals, scope.update);
|
|
31
28
|
} catch (err) {
|
|
32
29
|
console.error(
|
|
33
30
|
`spindle: Error in {button ${rawArgs}}${currentSourceLocation()}:`,
|
|
34
31
|
err,
|
|
35
32
|
);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (const key of Object.keys(vars)) {
|
|
40
|
-
if (vars[key] !== state.variables[key]) {
|
|
41
|
-
state.setVariable(key, vars[key]);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
for (const key of Object.keys(temps)) {
|
|
45
|
-
if (temps[key] !== state.temporary[key]) {
|
|
46
|
-
state.setTemporary(key, temps[key]);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
for (const key of Object.keys(localsClone)) {
|
|
50
|
-
if (localsClone[key] !== mergedLocals[key]) {
|
|
51
|
-
scope.update(`@${key}`, localsClone[key]);
|
|
52
|
-
}
|
|
53
33
|
}
|
|
54
34
|
};
|
|
55
35
|
|
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -59,6 +59,34 @@ function valuesEqual(a: unknown, b: unknown): boolean {
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
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
|
+
|
|
62
90
|
export function Computed({ rawArgs }: ComputedProps) {
|
|
63
91
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
64
92
|
|
|
@@ -79,31 +107,34 @@ export function Computed({ rawArgs }: ComputedProps) {
|
|
|
79
107
|
const isTemp = target.startsWith('_');
|
|
80
108
|
const name = target.slice(1);
|
|
81
109
|
|
|
82
|
-
//
|
|
83
|
-
|
|
110
|
+
// Synchronous first evaluation — sees preceding synchronous {set} mutations
|
|
111
|
+
const ran = useRef(false);
|
|
112
|
+
if (!ran.current) {
|
|
113
|
+
ran.current = true;
|
|
84
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
|
+
}
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!valuesEqual(current, newValue)) {
|
|
100
|
-
if (isTemp) {
|
|
101
|
-
state.setTemporary(name, newValue);
|
|
102
|
-
} else {
|
|
103
|
-
state.setVariable(name, newValue);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
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]);
|
|
107
138
|
|
|
108
139
|
return null;
|
|
109
140
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useLayoutEffect, useContext } from 'preact/hooks';
|
|
2
|
-
import { useStoryStore } from '../../store';
|
|
3
|
-
import { execute } from '../../expression';
|
|
4
2
|
import type { ASTNode } from '../../markup/ast';
|
|
5
|
-
import { deepClone } from '../../class-registry';
|
|
6
3
|
import { LocalsContext } from '../../markup/render';
|
|
7
4
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
5
|
+
import { executeMutation } from '../../execute-mutation';
|
|
8
6
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
9
7
|
|
|
10
8
|
interface DoProps {
|
|
@@ -24,33 +22,10 @@ export function Do({ children }: DoProps) {
|
|
|
24
22
|
const [, , mergedLocals] = useMergedLocals();
|
|
25
23
|
|
|
26
24
|
useLayoutEffect(() => {
|
|
27
|
-
const state = useStoryStore.getState();
|
|
28
|
-
const vars = deepClone(state.variables);
|
|
29
|
-
const temps = deepClone(state.temporary);
|
|
30
|
-
const localsClone = { ...mergedLocals };
|
|
31
|
-
|
|
32
25
|
try {
|
|
33
|
-
|
|
26
|
+
executeMutation(code, mergedLocals, scope.update);
|
|
34
27
|
} catch (err) {
|
|
35
28
|
console.error(`spindle: Error in {do}${currentSourceLocation()}:`, err);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Diff and apply
|
|
40
|
-
for (const key of Object.keys(vars)) {
|
|
41
|
-
if (vars[key] !== state.variables[key]) {
|
|
42
|
-
state.setVariable(key, vars[key]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
for (const key of Object.keys(temps)) {
|
|
46
|
-
if (temps[key] !== state.temporary[key]) {
|
|
47
|
-
state.setTemporary(key, temps[key]);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
for (const key of Object.keys(localsClone)) {
|
|
51
|
-
if (localsClone[key] !== mergedLocals[key]) {
|
|
52
|
-
scope.update(`@${key}`, localsClone[key]);
|
|
53
|
-
}
|
|
54
29
|
}
|
|
55
30
|
}, []);
|
|
56
31
|
|
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
7
8
|
import type { ASTNode } from '../../markup/ast';
|
|
8
9
|
|
|
@@ -76,6 +77,9 @@ function ForIteration({
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export function For({ rawArgs, children, className, id }: ForProps) {
|
|
80
|
+
const resolve = useInterpolate();
|
|
81
|
+
className = resolve(className);
|
|
82
|
+
id = resolve(id);
|
|
79
83
|
const parentScope = useContext(LocalsContext);
|
|
80
84
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
81
85
|
|
|
@@ -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 { useInterpolate } from '../../hooks/use-interpolate';
|
|
4
5
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
5
6
|
import type { Branch } from '../../markup/ast';
|
|
6
7
|
|
|
@@ -9,15 +10,18 @@ interface IfProps {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function If({ branches }: IfProps) {
|
|
13
|
+
const resolve = useInterpolate();
|
|
12
14
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
13
15
|
|
|
14
16
|
function renderBranch(branch: Branch) {
|
|
15
17
|
const children = renderNodes(branch.children);
|
|
16
|
-
|
|
18
|
+
const cls = resolve(branch.className);
|
|
19
|
+
const branchId = resolve(branch.id);
|
|
20
|
+
if (cls || branchId)
|
|
17
21
|
return (
|
|
18
22
|
<span
|
|
19
|
-
id={
|
|
20
|
-
class={
|
|
23
|
+
id={branchId}
|
|
24
|
+
class={cls}
|
|
21
25
|
>
|
|
22
26
|
{children}
|
|
23
27
|
</span>
|
|
@@ -4,6 +4,7 @@ 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';
|
|
7
8
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
8
9
|
|
|
9
10
|
interface IncludeProps {
|
|
@@ -13,6 +14,9 @@ interface IncludeProps {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function Include({ rawArgs, className, id }: IncludeProps) {
|
|
17
|
+
const resolve = useInterpolate();
|
|
18
|
+
className = resolve(className);
|
|
19
|
+
id = resolve(id);
|
|
16
20
|
const storyData = useStoryStore((s) => s.storyData);
|
|
17
21
|
const [variables, temporary, locals] = useMergedLocals();
|
|
18
22
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useContext } from 'preact/hooks';
|
|
2
2
|
import { useStoryStore } from '../../store';
|
|
3
|
-
import { execute } from '../../expression';
|
|
4
3
|
import type { ASTNode } from '../../markup/ast';
|
|
5
|
-
import { deepClone } from '../../class-registry';
|
|
6
4
|
import { LocalsContext } from '../../markup/render';
|
|
7
5
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
|
+
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
7
|
+
import { executeMutation } from '../../execute-mutation';
|
|
8
8
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
9
9
|
|
|
10
10
|
interface MacroLinkProps {
|
|
@@ -46,16 +46,11 @@ function executeChildren(
|
|
|
46
46
|
mergedLocals: Record<string, unknown>,
|
|
47
47
|
scopeUpdate: (key: string, value: unknown) => void,
|
|
48
48
|
) {
|
|
49
|
-
const state = useStoryStore.getState();
|
|
50
|
-
const vars = deepClone(state.variables);
|
|
51
|
-
const temps = deepClone(state.temporary);
|
|
52
|
-
const localsClone = { ...mergedLocals };
|
|
53
|
-
|
|
54
49
|
for (const node of children) {
|
|
55
50
|
if (node.type !== 'macro') continue;
|
|
56
51
|
if (node.name === 'set') {
|
|
57
52
|
try {
|
|
58
|
-
|
|
53
|
+
executeMutation(node.rawArgs, mergedLocals, scopeUpdate);
|
|
59
54
|
} catch (err) {
|
|
60
55
|
console.error(
|
|
61
56
|
`spindle: Error in {link} child {set}${currentSourceLocation()}:`,
|
|
@@ -65,7 +60,7 @@ function executeChildren(
|
|
|
65
60
|
} else if (node.name === 'do') {
|
|
66
61
|
const code = collectText(node.children);
|
|
67
62
|
try {
|
|
68
|
-
|
|
63
|
+
executeMutation(code, mergedLocals, scopeUpdate);
|
|
69
64
|
} catch (err) {
|
|
70
65
|
console.error(
|
|
71
66
|
`spindle: Error in {link} child {do}${currentSourceLocation()}:`,
|
|
@@ -74,23 +69,6 @@ function executeChildren(
|
|
|
74
69
|
}
|
|
75
70
|
}
|
|
76
71
|
}
|
|
77
|
-
|
|
78
|
-
// Apply changes
|
|
79
|
-
for (const key of Object.keys(vars)) {
|
|
80
|
-
if (vars[key] !== state.variables[key]) {
|
|
81
|
-
state.setVariable(key, vars[key]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const key of Object.keys(temps)) {
|
|
85
|
-
if (temps[key] !== state.temporary[key]) {
|
|
86
|
-
state.setTemporary(key, temps[key]);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
for (const key of Object.keys(localsClone)) {
|
|
90
|
-
if (localsClone[key] !== mergedLocals[key]) {
|
|
91
|
-
scopeUpdate(`@${key}`, localsClone[key]);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
72
|
}
|
|
95
73
|
|
|
96
74
|
export function MacroLink({
|
|
@@ -99,6 +77,9 @@ export function MacroLink({
|
|
|
99
77
|
className,
|
|
100
78
|
id,
|
|
101
79
|
}: MacroLinkProps) {
|
|
80
|
+
const resolve = useInterpolate();
|
|
81
|
+
className = resolve(className);
|
|
82
|
+
id = resolve(id);
|
|
102
83
|
const { display, passage } = parseArgs(rawArgs);
|
|
103
84
|
const scope = useContext(LocalsContext);
|
|
104
85
|
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,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
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
4
5
|
|
|
5
6
|
interface PrintProps {
|
|
@@ -9,6 +10,9 @@ interface PrintProps {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function Print({ rawArgs, className, id }: PrintProps) {
|
|
13
|
+
const resolve = useInterpolate();
|
|
14
|
+
className = resolve(className);
|
|
15
|
+
id = resolve(id);
|
|
12
16
|
const [mergedVars, mergedTemps, mergedLocals] = useMergedLocals();
|
|
13
17
|
|
|
14
18
|
try {
|
|
@@ -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,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useStoryStore } from '../../store';
|
|
3
|
-
import { execute } from '../../expression';
|
|
4
|
-
import { deepClone } from '../../class-registry';
|
|
1
|
+
import { useRef, useContext } from 'preact/hooks';
|
|
5
2
|
import { LocalsContext } from '../../markup/render';
|
|
6
3
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
4
|
+
import { executeMutation } from '../../execute-mutation';
|
|
7
5
|
import { currentSourceLocation } from '../../utils/source-location';
|
|
8
6
|
|
|
9
7
|
interface SetProps {
|
|
@@ -13,42 +11,21 @@ interface SetProps {
|
|
|
13
11
|
export function Set({ rawArgs }: SetProps) {
|
|
14
12
|
const scope = useContext(LocalsContext);
|
|
15
13
|
const [, , mergedLocals] = useMergedLocals();
|
|
14
|
+
const ran = useRef(false);
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const vars = deepClone(state.variables);
|
|
20
|
-
const temps = deepClone(state.temporary);
|
|
21
|
-
const localsClone = { ...mergedLocals };
|
|
16
|
+
if (!ran.current) {
|
|
17
|
+
ran.current = true;
|
|
22
18
|
|
|
23
19
|
try {
|
|
24
|
-
|
|
20
|
+
executeMutation(rawArgs, mergedLocals, scope.update);
|
|
25
21
|
} catch (err) {
|
|
26
22
|
console.error(
|
|
27
23
|
`spindle: Error in {set ${rawArgs}}${currentSourceLocation()}:`,
|
|
28
24
|
err,
|
|
29
25
|
);
|
|
30
|
-
return;
|
|
26
|
+
return null;
|
|
31
27
|
}
|
|
32
|
-
|
|
33
|
-
// Diff and apply store changes
|
|
34
|
-
for (const key of Object.keys(vars)) {
|
|
35
|
-
if (vars[key] !== state.variables[key]) {
|
|
36
|
-
state.setVariable(key, vars[key]);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
for (const key of Object.keys(temps)) {
|
|
40
|
-
if (temps[key] !== state.temporary[key]) {
|
|
41
|
-
state.setTemporary(key, temps[key]);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Diff and apply locals changes
|
|
46
|
-
for (const key of Object.keys(localsClone)) {
|
|
47
|
-
if (localsClone[key] !== mergedLocals[key]) {
|
|
48
|
-
scope.update(`@${key}`, localsClone[key]);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}, []);
|
|
28
|
+
}
|
|
52
29
|
|
|
53
30
|
return null;
|
|
54
31
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useStoryStore } from './store';
|
|
2
|
+
import { execute } from './expression';
|
|
3
|
+
import { deepClone } from './class-registry';
|
|
4
|
+
|
|
5
|
+
export function executeMutation(
|
|
6
|
+
code: string,
|
|
7
|
+
mergedLocals: Record<string, unknown>,
|
|
8
|
+
scopeUpdate: (key: string, value: unknown) => void,
|
|
9
|
+
): void {
|
|
10
|
+
const state = useStoryStore.getState();
|
|
11
|
+
const vars = deepClone(state.variables);
|
|
12
|
+
const temps = deepClone(state.temporary);
|
|
13
|
+
const localsClone = { ...mergedLocals };
|
|
14
|
+
|
|
15
|
+
execute(code, vars, temps, localsClone);
|
|
16
|
+
|
|
17
|
+
for (const key of Object.keys(vars)) {
|
|
18
|
+
if (vars[key] !== state.variables[key]) {
|
|
19
|
+
state.setVariable(key, vars[key]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const key of Object.keys(temps)) {
|
|
23
|
+
if (temps[key] !== state.temporary[key]) {
|
|
24
|
+
state.setTemporary(key, temps[key]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const key of Object.keys(localsClone)) {
|
|
28
|
+
if (localsClone[key] !== mergedLocals[key]) {
|
|
29
|
+
scopeUpdate(`@${key}`, localsClone[key]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|