@rohal12/spindle 0.52.5 → 0.52.6
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/dist/pkg/headless.js +651 -544
- package/dist/pkg/macro-registry.json +255 -0
- package/package.json +1 -1
- package/src/components/macros/Button.tsx +2 -4
- package/src/components/macros/Checkbox.tsx +6 -13
- package/src/components/macros/Computed.tsx +38 -55
- package/src/components/macros/Cycle.tsx +1 -0
- package/src/components/macros/Dialog.tsx +7 -8
- package/src/components/macros/Do.tsx +2 -1
- package/src/components/macros/For.tsx +27 -78
- package/src/components/macros/Goto.tsx +6 -9
- package/src/components/macros/Include.tsx +10 -57
- package/src/components/macros/Listbox.tsx +1 -0
- package/src/components/macros/MacroError.tsx +37 -0
- package/src/components/macros/MacroLink.tsx +9 -24
- package/src/components/macros/Meter.tsx +19 -37
- package/src/components/macros/Numberbox.tsx +5 -32
- package/src/components/macros/PassageDisplay.tsx +37 -47
- package/src/components/macros/Print.tsx +1 -0
- package/src/components/macros/Radiobutton.tsx +8 -38
- package/src/components/macros/Repeat.tsx +3 -3
- package/src/components/macros/Set.tsx +3 -29
- package/src/components/macros/Switch.tsx +36 -37
- package/src/components/macros/Textarea.tsx +2 -28
- package/src/components/macros/Textbox.tsx +2 -29
- package/src/components/macros/Type.tsx +3 -3
- package/src/components/macros/Unset.tsx +17 -40
- package/src/components/macros/Watch.tsx +23 -91
- package/src/components/macros/WidgetInvocation.tsx +9 -71
- package/src/components/macros/detached-body.tsx +2 -2
- package/src/components/macros/input-macro.ts +48 -0
- package/src/components/macros/locals-scope.tsx +60 -0
- package/src/components/macros/macro-args.ts +280 -0
- package/src/components/macros/option-utils.ts +7 -12
- package/src/define-macro.ts +47 -30
- package/src/hooks/use-render-options.ts +26 -0
- package/src/markup/render.tsx +2 -4
- package/src/registry.ts +48 -0
- package/types/index.d.ts +69 -6
- package/types/tooling.d.ts +33 -1
|
@@ -1,36 +1,10 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
|
-
import {
|
|
2
|
+
import { useRunOnce } from './MacroError';
|
|
3
3
|
|
|
4
4
|
defineMacro({
|
|
5
5
|
name: 'set',
|
|
6
|
+
parameters: [{ name: 'code', type: 'expression', required: true }],
|
|
6
7
|
render({ rawArgs }, ctx) {
|
|
7
|
-
|
|
8
|
-
// Boxed: anything can be thrown, including null and other falsy values
|
|
9
|
-
const failure = ctx.hooks.useRef<{ error: unknown } | null>(null);
|
|
10
|
-
|
|
11
|
-
if (!ran.current) {
|
|
12
|
-
ran.current = true;
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
ctx.mutate(rawArgs);
|
|
16
|
-
} catch (err) {
|
|
17
|
-
failure.current = { error: err };
|
|
18
|
-
console.error(
|
|
19
|
-
`spindle: Error in {set ${rawArgs}}${ctx.sourceLocation()}:`,
|
|
20
|
-
err,
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (failure.current) {
|
|
26
|
-
return (
|
|
27
|
-
<MacroError
|
|
28
|
-
macro="set"
|
|
29
|
-
error={failure.current.error}
|
|
30
|
-
/>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return null;
|
|
8
|
+
return useRunOnce('set', rawArgs, () => ctx.mutate(rawArgs));
|
|
35
9
|
},
|
|
36
10
|
});
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { MacroError } from './MacroError';
|
|
3
|
+
import type { Branch } from '../../markup/ast';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The first {case} branch whose value equals `value`, else the {default}
|
|
7
|
+
* branch, else null. Throws what a case expression throws.
|
|
8
|
+
*/
|
|
9
|
+
function selectBranch(
|
|
10
|
+
value: unknown,
|
|
11
|
+
branches: Branch[],
|
|
12
|
+
evaluate: (expr: string) => unknown,
|
|
13
|
+
): Branch | null {
|
|
14
|
+
let defaultBranch: Branch | null = null;
|
|
15
|
+
for (let i = 1; i < branches.length; i++) {
|
|
16
|
+
const branch = branches[i]!;
|
|
17
|
+
if (branch.rawArgs === '') {
|
|
18
|
+
defaultBranch = branch;
|
|
19
|
+
} else if (value === evaluate(branch.rawArgs)) {
|
|
20
|
+
return branch;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return defaultBranch;
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
defineMacro({
|
|
5
27
|
name: 'switch',
|
|
6
28
|
subMacros: ['case', 'default'],
|
|
29
|
+
parameters: [{ name: 'expression', type: 'expression', required: true }],
|
|
7
30
|
merged: true,
|
|
8
31
|
render({ rawArgs, branches = [] }, ctx) {
|
|
9
32
|
let switchValue: unknown;
|
|
@@ -18,46 +41,22 @@ defineMacro({
|
|
|
18
41
|
);
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (switchValue === caseValue) {
|
|
32
|
-
return <>{ctx.renderNodes(branch.children)}</>;
|
|
33
|
-
}
|
|
34
|
-
} catch (err) {
|
|
35
|
-
return (
|
|
36
|
-
<MacroError
|
|
37
|
-
macro="case"
|
|
38
|
-
error={err}
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (defaultBranch) {
|
|
45
|
-
return <>{ctx.renderNodes(defaultBranch.children)}</>;
|
|
44
|
+
let branch: Branch | null;
|
|
45
|
+
try {
|
|
46
|
+
branch = selectBranch(switchValue, branches, ctx.evaluate!);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return (
|
|
49
|
+
<MacroError
|
|
50
|
+
macro="case"
|
|
51
|
+
error={err}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
return
|
|
56
|
+
return branch && <>{ctx.renderNodes(branch.children)}</>;
|
|
49
57
|
},
|
|
50
58
|
text({ rawArgs, branches = [] }, ctx) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
for (let i = 1; i < branches.length; i++) {
|
|
54
|
-
const branch = branches[i]!;
|
|
55
|
-
if (branch.rawArgs === '') {
|
|
56
|
-
defaultBranch = branch;
|
|
57
|
-
} else if (switchValue === ctx.evaluate(branch.rawArgs)) {
|
|
58
|
-
return ctx.renderText(branch.children);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return defaultBranch ? ctx.renderText(defaultBranch.children) : '';
|
|
59
|
+
const branch = selectBranch(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
60
|
+
return branch ? ctx.renderText(branch.children) : '';
|
|
62
61
|
},
|
|
63
62
|
});
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineInputMacro } from './input-macro';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
name: 'textarea',
|
|
5
|
-
storeVar: true,
|
|
6
|
-
render({ rawArgs }, ctx) {
|
|
7
|
-
const { placeholder } = ctx.parseVarArgs(rawArgs);
|
|
8
|
-
|
|
9
|
-
ctx.useAction({
|
|
10
|
-
type: 'textarea',
|
|
11
|
-
key: `$${ctx.varName}`,
|
|
12
|
-
authorId: ctx.id,
|
|
13
|
-
label: placeholder || ctx.varName!,
|
|
14
|
-
variable: ctx.varName,
|
|
15
|
-
value: ctx.value,
|
|
16
|
-
perform: (v) => ctx.setValue!(v !== undefined ? String(v) : ''),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<textarea
|
|
21
|
-
id={ctx.id}
|
|
22
|
-
class={ctx.cls}
|
|
23
|
-
value={ctx.value == null ? '' : String(ctx.value)}
|
|
24
|
-
placeholder={placeholder}
|
|
25
|
-
onInput={(e) => ctx.setValue!((e.target as HTMLTextAreaElement).value)}
|
|
26
|
-
/>
|
|
27
|
-
);
|
|
28
|
-
},
|
|
29
|
-
});
|
|
3
|
+
defineInputMacro('textarea');
|
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineInputMacro } from './input-macro';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
name: 'textbox',
|
|
5
|
-
storeVar: true,
|
|
6
|
-
render({ rawArgs }, ctx) {
|
|
7
|
-
const { placeholder } = ctx.parseVarArgs(rawArgs);
|
|
8
|
-
|
|
9
|
-
ctx.useAction({
|
|
10
|
-
type: 'textbox',
|
|
11
|
-
key: `$${ctx.varName}`,
|
|
12
|
-
authorId: ctx.id,
|
|
13
|
-
label: placeholder || ctx.varName!,
|
|
14
|
-
variable: ctx.varName,
|
|
15
|
-
value: ctx.value,
|
|
16
|
-
perform: (v) => ctx.setValue!(v !== undefined ? String(v) : ''),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<input
|
|
21
|
-
type="text"
|
|
22
|
-
id={ctx.id}
|
|
23
|
-
class={ctx.cls}
|
|
24
|
-
value={ctx.value == null ? '' : String(ctx.value)}
|
|
25
|
-
placeholder={placeholder}
|
|
26
|
-
onInput={(e) => ctx.setValue!((e.target as HTMLInputElement).value)}
|
|
27
|
-
/>
|
|
28
|
-
);
|
|
29
|
-
},
|
|
30
|
-
});
|
|
3
|
+
defineInputMacro('textbox', 'text');
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { parseDelay } from '../../utils/parse-delay';
|
|
2
1
|
import { defineMacro } from '../../define-macro';
|
|
3
2
|
|
|
4
3
|
defineMacro({
|
|
5
4
|
name: 'type',
|
|
6
5
|
block: true,
|
|
7
6
|
interpolate: true,
|
|
8
|
-
|
|
7
|
+
parameters: [{ name: 'delay', type: 'delay', required: true }],
|
|
8
|
+
render({ children = [] }, ctx) {
|
|
9
9
|
const { useState, useEffect, useRef } = ctx.hooks;
|
|
10
|
-
const speed =
|
|
10
|
+
const speed = ctx.args.delay ?? 0;
|
|
11
11
|
const containerRef = useRef<HTMLSpanElement>(null);
|
|
12
12
|
const [totalChars, setTotalChars] = useState(0);
|
|
13
13
|
const [visibleChars, setVisibleChars] = useState(0);
|
|
@@ -1,52 +1,29 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { defineMacro } from '../../define-macro';
|
|
3
|
-
import {
|
|
3
|
+
import { useRunOnce } from './MacroError';
|
|
4
4
|
|
|
5
5
|
defineMacro({
|
|
6
6
|
name: 'unset',
|
|
7
|
+
parameters: [{ name: 'variable', type: 'variable', required: true }],
|
|
7
8
|
render({ rawArgs }, ctx) {
|
|
8
|
-
|
|
9
|
-
// Boxed: anything can be thrown, including null and other falsy values
|
|
10
|
-
const failure = ctx.hooks.useRef<{ error: unknown } | null>(null);
|
|
11
|
-
|
|
12
|
-
if (!ran.current) {
|
|
13
|
-
ran.current = true;
|
|
9
|
+
return useRunOnce('unset', rawArgs, () => {
|
|
14
10
|
const state = useStoryStore.getState();
|
|
15
|
-
const name =
|
|
11
|
+
const name = ctx.args.variable ?? '';
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`{unset} expects a variable ($name, _name, %name, or @name), got "${name}"`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
} catch (err) {
|
|
33
|
-
failure.current = { error: err };
|
|
34
|
-
console.error(
|
|
35
|
-
`spindle: Error in {unset ${rawArgs}}${ctx.sourceLocation()}:`,
|
|
36
|
-
err,
|
|
13
|
+
if (name.startsWith('$')) {
|
|
14
|
+
state.deleteVariable(name.slice(1));
|
|
15
|
+
} else if (name.startsWith('_')) {
|
|
16
|
+
state.deleteTemporary(name.slice(1));
|
|
17
|
+
} else if (name.startsWith('%')) {
|
|
18
|
+
state.deleteTransient(name.slice(1));
|
|
19
|
+
} else if (name.startsWith('@')) {
|
|
20
|
+
// Throws outside a locals scope ({for}, widget, {link}, {button})
|
|
21
|
+
ctx.update(name.slice(1), undefined);
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`{unset} expects a variable ($name, _name, %name, or @name), got "${name}"`,
|
|
37
25
|
);
|
|
38
26
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (failure.current) {
|
|
42
|
-
return (
|
|
43
|
-
<MacroError
|
|
44
|
-
macro="unset"
|
|
45
|
-
error={failure.current.error}
|
|
46
|
-
/>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return null;
|
|
27
|
+
});
|
|
51
28
|
},
|
|
52
29
|
});
|
|
@@ -1,99 +1,30 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { addMacroTrigger, removeTrigger } from '../../triggers';
|
|
3
|
-
import type { WatchOptions } from '../../triggers';
|
|
4
|
-
import { isWhitespace, readQuoted, readWholeQuoted } from './arg-utils';
|
|
5
|
-
|
|
6
|
-
const WORD_RE = /\w+/y;
|
|
7
|
-
const DIGITS_RE = /\d+/y;
|
|
8
|
-
|
|
9
|
-
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
10
|
-
function matchAt(re: RegExp, src: string, pos: number): string | null {
|
|
11
|
-
re.lastIndex = pos;
|
|
12
|
-
return re.exec(src)?.[0] ?? null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse `{watch "condition" keyword value …}`: a quoted condition followed
|
|
17
|
-
* by keyword options (`goto "X"`, `dialog "X"`, `run "X"`, `name "X"`,
|
|
18
|
-
* `priority N`, `once`). Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
19
|
-
* Returns `null` when the arguments do not start with a quoted condition.
|
|
20
|
-
*/
|
|
21
|
-
export function parseWatchArgs(
|
|
22
|
-
rawArgs: string,
|
|
23
|
-
): { condition: string; options: WatchOptions } | null {
|
|
24
|
-
const raw = rawArgs.trim();
|
|
25
|
-
const cond = readQuoted(raw, 0);
|
|
26
|
-
if (!cond) return null;
|
|
27
|
-
|
|
28
|
-
const options: WatchOptions = {};
|
|
29
|
-
let i = cond.end;
|
|
30
|
-
while (i < raw.length) {
|
|
31
|
-
const key = matchAt(WORD_RE, raw, i);
|
|
32
|
-
if (!key) {
|
|
33
|
-
// Skip whitespace and stray characters, or a stray quoted string.
|
|
34
|
-
i = readQuoted(raw, i)?.end ?? i + 1;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
i += key.length;
|
|
38
|
-
|
|
39
|
-
// A value is a quoted string or a digit run, after whitespace.
|
|
40
|
-
let val: string | undefined;
|
|
41
|
-
let j = i;
|
|
42
|
-
while (j < raw.length && isWhitespace(raw[j]!)) j++;
|
|
43
|
-
if (j > i) {
|
|
44
|
-
const quoted = readQuoted(raw, j);
|
|
45
|
-
const digits = quoted ? null : matchAt(DIGITS_RE, raw, j);
|
|
46
|
-
if (quoted) {
|
|
47
|
-
val = quoted.value;
|
|
48
|
-
i = quoted.end;
|
|
49
|
-
} else if (digits) {
|
|
50
|
-
val = digits;
|
|
51
|
-
i = j + digits.length;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
switch (key) {
|
|
56
|
-
case 'goto':
|
|
57
|
-
options.goto = val;
|
|
58
|
-
break;
|
|
59
|
-
case 'dialog':
|
|
60
|
-
options.dialog = val;
|
|
61
|
-
break;
|
|
62
|
-
case 'run':
|
|
63
|
-
options.run = val;
|
|
64
|
-
break;
|
|
65
|
-
case 'name':
|
|
66
|
-
options.name = val;
|
|
67
|
-
break;
|
|
68
|
-
case 'priority':
|
|
69
|
-
options.priority = val ? Number(val) : 0;
|
|
70
|
-
break;
|
|
71
|
-
case 'once':
|
|
72
|
-
options.once = true;
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { condition: cond.value, options };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Parse the watcher name of `{unwatch "name"}`: a quoted string, with the
|
|
82
|
-
* same escapes as `{watch}`, or a bare name.
|
|
83
|
-
*/
|
|
84
|
-
export function parseUnwatchName(rawArgs: string): string {
|
|
85
|
-
const raw = rawArgs.trim();
|
|
86
|
-
return readWholeQuoted(raw) ?? raw.replace(/^['"]|['"]$/g, '');
|
|
87
|
-
}
|
|
88
3
|
|
|
89
4
|
defineMacro({
|
|
90
5
|
name: 'watch',
|
|
91
|
-
|
|
6
|
+
// {watch "condition" goto "X" dialog "X" run "X" name "X" priority N once}
|
|
7
|
+
parameters: [
|
|
8
|
+
{ name: 'condition', type: 'string', required: true },
|
|
9
|
+
{
|
|
10
|
+
name: 'options',
|
|
11
|
+
type: 'options',
|
|
12
|
+
parameters: [
|
|
13
|
+
{ name: 'goto', type: 'string' },
|
|
14
|
+
{ name: 'dialog', type: 'string' },
|
|
15
|
+
{ name: 'run', type: 'string' },
|
|
16
|
+
{ name: 'name', type: 'string' },
|
|
17
|
+
{ name: 'priority', type: 'number' },
|
|
18
|
+
{ name: 'once', type: 'flag' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
render(_props, ctx) {
|
|
92
23
|
const { hooks } = ctx;
|
|
93
24
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
25
|
+
// Without a quoted condition there is nothing to watch.
|
|
26
|
+
const { condition, options } = ctx.args;
|
|
27
|
+
if (condition === undefined) return null;
|
|
97
28
|
|
|
98
29
|
// Register during render (like {set}) — triggers survive navigation
|
|
99
30
|
// and are cleaned up via resetTriggers() on restart or {unwatch}.
|
|
@@ -110,8 +41,9 @@ defineMacro({
|
|
|
110
41
|
|
|
111
42
|
defineMacro({
|
|
112
43
|
name: 'unwatch',
|
|
113
|
-
|
|
114
|
-
|
|
44
|
+
parameters: [{ name: 'name', type: 'text', required: true }],
|
|
45
|
+
render(_props, ctx) {
|
|
46
|
+
const name = ctx.args.name ?? '';
|
|
115
47
|
|
|
116
48
|
const ran = ctx.hooks.useRef(false);
|
|
117
49
|
if (!ran.current) {
|
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
useState,
|
|
4
|
-
useCallback,
|
|
5
|
-
useRef,
|
|
6
|
-
useMemo,
|
|
7
|
-
} from 'preact/hooks';
|
|
8
|
-
import {
|
|
9
|
-
LocalsValuesContext,
|
|
10
|
-
LocalsUpdateContext,
|
|
11
|
-
NobrContext,
|
|
12
|
-
InlineContext,
|
|
13
|
-
WidgetChildrenContext,
|
|
14
|
-
RawTextContext,
|
|
15
|
-
renderNodes,
|
|
16
|
-
} from '../../markup/render';
|
|
1
|
+
import { useMemo } from 'preact/hooks';
|
|
2
|
+
import { WidgetChildrenContext, renderNodes } from '../../markup/render';
|
|
17
3
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
18
4
|
import { evaluate } from '../../expression';
|
|
19
5
|
import type { ASTNode } from '../../markup/ast';
|
|
20
6
|
import { splitArgs } from './arg-utils';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
withEntry,
|
|
25
|
-
} from '../../utils/namespace';
|
|
7
|
+
import { createNamespace } from '../../utils/namespace';
|
|
8
|
+
import { useRenderOptions } from '../../hooks/use-render-options';
|
|
9
|
+
import { LocalsScope } from './locals-scope';
|
|
26
10
|
|
|
27
11
|
export { splitArgs };
|
|
28
12
|
|
|
@@ -33,60 +17,14 @@ interface WidgetInvocationProps {
|
|
|
33
17
|
invocationChildren?: ASTNode[];
|
|
34
18
|
}
|
|
35
19
|
|
|
36
|
-
function WidgetBody({
|
|
37
|
-
body,
|
|
38
|
-
parentValues,
|
|
39
|
-
ownKeys,
|
|
40
|
-
}: {
|
|
41
|
-
body: ASTNode[];
|
|
42
|
-
parentValues: Record<string, unknown>;
|
|
43
|
-
ownKeys: Record<string, unknown>;
|
|
44
|
-
}) {
|
|
45
|
-
const nobr = useContext(NobrContext);
|
|
46
|
-
const inline = useContext(InlineContext);
|
|
47
|
-
const raw = useContext(RawTextContext);
|
|
48
|
-
const [localMutations, setLocalMutations] = useState<Record<string, unknown>>(
|
|
49
|
-
{},
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const localState = useMemo(
|
|
53
|
-
() => createNamespace(parentValues, ownKeys, localMutations),
|
|
54
|
-
[parentValues, ownKeys, localMutations],
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const valuesRef = useRef(localState);
|
|
58
|
-
valuesRef.current = localState;
|
|
59
|
-
|
|
60
|
-
const getValues = useCallback(() => valuesRef.current, []);
|
|
61
|
-
const update = useCallback((key: string, value: unknown) => {
|
|
62
|
-
// Apply synchronously so later macros in the same render pass (e.g. a
|
|
63
|
-
// second {set}) read the new value via getValues(); the state update
|
|
64
|
-
// then re-renders consumers of LocalsValuesContext.
|
|
65
|
-
checkVariableName(key, `@${key}`);
|
|
66
|
-
valuesRef.current = withEntry(valuesRef.current, key, value);
|
|
67
|
-
setLocalMutations((prev) => ({ ...prev, [key]: value }));
|
|
68
|
-
}, []);
|
|
69
|
-
const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<LocalsUpdateContext.Provider value={updater}>
|
|
73
|
-
<LocalsValuesContext.Provider value={localState}>
|
|
74
|
-
{renderNodes(body, { nobr, inline, raw, locals: localState })}
|
|
75
|
-
</LocalsValuesContext.Provider>
|
|
76
|
-
</LocalsUpdateContext.Provider>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
20
|
export function WidgetInvocation({
|
|
81
21
|
body,
|
|
82
22
|
params,
|
|
83
23
|
rawArgs,
|
|
84
24
|
invocationChildren,
|
|
85
25
|
}: WidgetInvocationProps) {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const inline = useContext(InlineContext);
|
|
89
|
-
const raw = useContext(RawTextContext);
|
|
26
|
+
const renderOptions = useRenderOptions();
|
|
27
|
+
const parentValues = renderOptions.locals;
|
|
90
28
|
const [mergedVars, mergedTemps, mergedLocals, mergedTrans] =
|
|
91
29
|
useMergedLocals();
|
|
92
30
|
|
|
@@ -97,7 +35,7 @@ export function WidgetInvocation({
|
|
|
97
35
|
if (params.length === 0) {
|
|
98
36
|
return (
|
|
99
37
|
<WidgetChildrenContext.Provider value={childrenValue}>
|
|
100
|
-
{renderNodes(body,
|
|
38
|
+
{renderNodes(body, renderOptions)}
|
|
101
39
|
</WidgetChildrenContext.Provider>
|
|
102
40
|
);
|
|
103
41
|
}
|
|
@@ -137,7 +75,7 @@ export function WidgetInvocation({
|
|
|
137
75
|
|
|
138
76
|
return (
|
|
139
77
|
<WidgetChildrenContext.Provider value={childrenValue}>
|
|
140
|
-
<
|
|
78
|
+
<LocalsScope
|
|
141
79
|
body={body}
|
|
142
80
|
parentValues={parentValues}
|
|
143
81
|
ownKeys={ownKeys}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
WidgetChildrenContext,
|
|
10
10
|
} from '../../markup/render';
|
|
11
11
|
import type { ASTNode } from '../../markup/ast';
|
|
12
|
+
import { useRenderOptions } from '../../hooks/use-render-options';
|
|
12
13
|
import { liveLocalsView } from '../../utils/live-locals';
|
|
13
14
|
import { runWithCommittedMutations } from '../../execute-mutation';
|
|
14
15
|
import { RepeatContext } from './Repeat';
|
|
@@ -35,8 +36,7 @@ import { DialogCloseContext } from '../PassageDialog';
|
|
|
35
36
|
*/
|
|
36
37
|
export function useDetachedBody(): (children: ASTNode[]) => void {
|
|
37
38
|
const updater = useContext(LocalsUpdateContext);
|
|
38
|
-
const nobr =
|
|
39
|
-
const inline = useContext(InlineContext);
|
|
39
|
+
const { nobr, inline } = useRenderOptions();
|
|
40
40
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
41
41
|
const repeat = useContext(RepeatContext);
|
|
42
42
|
const closeDialog = useContext(DialogCloseContext);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { h } from 'preact';
|
|
2
|
+
import { defineMacro } from '../../define-macro';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
|
|
6
|
+
* `type` (or, without one, a `<textarea>`) bound to the story variable.
|
|
7
|
+
* `toValue` turns what the reader types, or an automation action's value
|
|
8
|
+
* (`undefined` when it has none), into the variable's value: by default,
|
|
9
|
+
* the text.
|
|
10
|
+
*/
|
|
11
|
+
export function defineInputMacro(
|
|
12
|
+
name: 'textbox' | 'numberbox' | 'textarea',
|
|
13
|
+
type?: 'text' | 'number',
|
|
14
|
+
toValue = (input: unknown): unknown =>
|
|
15
|
+
input !== undefined ? String(input) : '',
|
|
16
|
+
): void {
|
|
17
|
+
defineMacro({
|
|
18
|
+
name,
|
|
19
|
+
storeVar: true,
|
|
20
|
+
parameters: [
|
|
21
|
+
{ name: 'variable', type: 'variable', required: true },
|
|
22
|
+
{ name: 'placeholder', type: 'string' },
|
|
23
|
+
],
|
|
24
|
+
render(_props, ctx) {
|
|
25
|
+
const placeholder = ctx.args.placeholder ?? '';
|
|
26
|
+
|
|
27
|
+
ctx.useAction({
|
|
28
|
+
type: name,
|
|
29
|
+
key: `$${ctx.varName}`,
|
|
30
|
+
authorId: ctx.id,
|
|
31
|
+
label: placeholder || ctx.varName!,
|
|
32
|
+
variable: ctx.varName,
|
|
33
|
+
value: ctx.value,
|
|
34
|
+
perform: (v) => ctx.setValue!(toValue(v)),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return h(type ? 'input' : 'textarea', {
|
|
38
|
+
type,
|
|
39
|
+
id: ctx.id,
|
|
40
|
+
class: ctx.cls,
|
|
41
|
+
value: ctx.value == null ? '' : String(ctx.value),
|
|
42
|
+
placeholder,
|
|
43
|
+
onInput: (e: Event) =>
|
|
44
|
+
ctx.setValue!(toValue((e.target as HTMLInputElement).value)),
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useMemo } from 'preact/hooks';
|
|
2
|
+
import {
|
|
3
|
+
LocalsValuesContext,
|
|
4
|
+
LocalsUpdateContext,
|
|
5
|
+
renderNodes,
|
|
6
|
+
} from '../../markup/render';
|
|
7
|
+
import type { ASTNode } from '../../markup/ast';
|
|
8
|
+
import { useRenderOptions } from '../../hooks/use-render-options';
|
|
9
|
+
import {
|
|
10
|
+
checkVariableName,
|
|
11
|
+
createNamespace,
|
|
12
|
+
withEntry,
|
|
13
|
+
} from '../../utils/namespace';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render `body` in a new locals scope: the locals of `parentValues`, with
|
|
17
|
+
* `ownKeys` (an iteration's loop variables, a widget's parameters) on top.
|
|
18
|
+
* Locals the body assigns are kept in this scope.
|
|
19
|
+
*/
|
|
20
|
+
export function LocalsScope({
|
|
21
|
+
body,
|
|
22
|
+
parentValues,
|
|
23
|
+
ownKeys,
|
|
24
|
+
}: {
|
|
25
|
+
body: ASTNode[];
|
|
26
|
+
parentValues: Record<string, unknown>;
|
|
27
|
+
ownKeys: Record<string, unknown>;
|
|
28
|
+
}) {
|
|
29
|
+
const options = useRenderOptions();
|
|
30
|
+
const [localMutations, setLocalMutations] = useState<Record<string, unknown>>(
|
|
31
|
+
{},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const localState = useMemo(
|
|
35
|
+
() => createNamespace(parentValues, ownKeys, localMutations),
|
|
36
|
+
[parentValues, ownKeys, localMutations],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const valuesRef = useRef(localState);
|
|
40
|
+
valuesRef.current = localState;
|
|
41
|
+
|
|
42
|
+
const getValues = useCallback(() => valuesRef.current, []);
|
|
43
|
+
const update = useCallback((key: string, value: unknown) => {
|
|
44
|
+
// Apply synchronously so later macros in the same render pass (e.g. a
|
|
45
|
+
// second {set}) read the new value via getValues(); the state update
|
|
46
|
+
// then re-renders consumers of LocalsValuesContext.
|
|
47
|
+
checkVariableName(key, `@${key}`);
|
|
48
|
+
valuesRef.current = withEntry(valuesRef.current, key, value);
|
|
49
|
+
setLocalMutations((prev) => ({ ...prev, [key]: value }));
|
|
50
|
+
}, []);
|
|
51
|
+
const updater = useMemo(() => ({ update, getValues }), [update, getValues]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<LocalsUpdateContext.Provider value={updater}>
|
|
55
|
+
<LocalsValuesContext.Provider value={localState}>
|
|
56
|
+
{renderNodes(body, { ...options, locals: localState })}
|
|
57
|
+
</LocalsValuesContext.Provider>
|
|
58
|
+
</LocalsUpdateContext.Provider>
|
|
59
|
+
);
|
|
60
|
+
}
|