@rohal12/spindle 0.51.4 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +4512 -1702
- package/dist/pkg/macro-registry.json +7 -7
- package/dist/pkg/story-variables.js +1636 -177
- package/package.json +5 -2
- package/src/action-registry.ts +70 -18
- package/src/automation/runner.ts +2 -1
- package/src/class-registry.ts +214 -103
- package/src/components/Passage.tsx +2 -2
- package/src/components/PassageDialog.tsx +2 -5
- package/src/components/StoryInterface.tsx +2 -4
- package/src/components/macros/Button.tsx +5 -31
- package/src/components/macros/Checkbox.tsx +7 -4
- package/src/components/macros/Computed.tsx +28 -14
- package/src/components/macros/For.tsx +29 -3
- package/src/components/macros/If.tsx +8 -0
- package/src/components/macros/Include.tsx +7 -6
- package/src/components/macros/MacroError.tsx +2 -1
- package/src/components/macros/MacroLink.tsx +12 -45
- package/src/components/macros/Meter.tsx +11 -3
- package/src/components/macros/Nobr.tsx +1 -0
- package/src/components/macros/PassageDisplay.tsx +3 -0
- package/src/components/macros/Print.tsx +4 -0
- package/src/components/macros/Radiobutton.tsx +5 -2
- package/src/components/macros/SaveManager.tsx +25 -8
- package/src/components/macros/Set.tsx +5 -4
- package/src/components/macros/Span.tsx +1 -0
- package/src/components/macros/StoryTitle.tsx +1 -0
- package/src/components/macros/Switch.tsx +13 -0
- package/src/components/macros/Unset.tsx +31 -10
- package/src/components/macros/VarDisplay.tsx +21 -4
- package/src/components/macros/Widget.tsx +20 -1
- package/src/components/macros/WidgetInvocation.tsx +17 -75
- package/src/components/macros/arg-utils.ts +107 -1
- package/src/components/macros/detached-body.tsx +68 -0
- package/src/components/macros/option-utils.ts +3 -2
- package/src/define-macro.ts +32 -5
- package/src/execute-mutation.ts +271 -68
- package/src/expression.ts +91 -59
- package/src/hooks/use-action.ts +24 -3
- package/src/hooks/use-interpolate.ts +36 -5
- package/src/index.tsx +12 -2
- package/src/interpolation.ts +394 -96
- package/src/js-lexer.ts +1231 -97
- package/src/markup/ast.ts +7 -2
- package/src/markup/code-attributes.ts +64 -0
- package/src/markup/markdown.ts +188 -9
- package/src/markup/render.tsx +430 -49
- package/src/markup/tokenizer.ts +578 -110
- package/src/prng.ts +41 -8
- package/src/registry.ts +35 -0
- package/src/saves/save-manager.ts +346 -160
- package/src/saves/storage.ts +16 -7
- package/src/saves/types.ts +2 -1
- package/src/settings.ts +12 -9
- package/src/store.ts +640 -186
- package/src/story-api.ts +38 -73
- package/src/story-init.ts +1 -1
- package/src/story-variables.ts +98 -102
- package/src/triggers.ts +10 -12
- package/src/utils/counts.ts +45 -0
- package/src/utils/error-message.ts +12 -0
- package/src/utils/live-locals.ts +10 -3
- package/src/utils/namespace.ts +71 -0
- package/src/utils/object-path.ts +99 -14
- package/src/utils/stable-key.ts +14 -9
- package/src/widgets/widget-registry.ts +9 -0
- package/types/index.d.ts +43 -7
- package/types/tooling.d.ts +1 -0
package/src/utils/live-locals.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createNamespace, ownValue } from './namespace';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Read-only view of a locals scope that always reflects its current values.
|
|
3
5
|
*
|
|
@@ -9,9 +11,14 @@
|
|
|
9
11
|
export function liveLocalsView(
|
|
10
12
|
getValues: () => Record<string, unknown>,
|
|
11
13
|
): Record<string, unknown> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// No prototype and own entries only, like every namespace (see
|
|
15
|
+
// utils/namespace.ts)
|
|
16
|
+
return new Proxy(createNamespace(), {
|
|
17
|
+
get: (_, key) =>
|
|
18
|
+
typeof key === 'string' ? ownValue(getValues(), key) : undefined,
|
|
19
|
+
has: (_, key) =>
|
|
20
|
+
typeof key === 'string' &&
|
|
21
|
+
Object.prototype.hasOwnProperty.call(getValues(), key),
|
|
15
22
|
ownKeys: () => Reflect.ownKeys(getValues()),
|
|
16
23
|
getOwnPropertyDescriptor: (_, key) => {
|
|
17
24
|
const desc = Object.getOwnPropertyDescriptor(getValues(), key);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable namespaces: the records holding $story variables, _temporary
|
|
3
|
+
* variables, %transient variables and @locals, keyed by variable name.
|
|
4
|
+
*
|
|
5
|
+
* They have no prototype, so every name is plain storage: a variable may be
|
|
6
|
+
* called `constructor`, `toString` or `hasOwnProperty`, and reading one
|
|
7
|
+
* that was never set gives undefined rather than an Object.prototype
|
|
8
|
+
* member. The one name they cannot hold is `__proto__`: story state (and so
|
|
9
|
+
* saves) never holds a property by that name, and on an ordinary object
|
|
10
|
+
* writing it would replace the prototype instead of storing a value. It is
|
|
11
|
+
* refused wherever a variable name enters (checkVariableName).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Namespace = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
/** The variable name no namespace can hold. */
|
|
17
|
+
export const RESERVED_NAME = '__proto__';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Throw a TypeError if `name` (without its sigil) cannot be a variable
|
|
21
|
+
* name. `label` is the name as the author wrote it (e.g. `$__proto__`).
|
|
22
|
+
*/
|
|
23
|
+
export function checkVariableName(name: string, label = name): void {
|
|
24
|
+
if (name === RESERVED_NAME) {
|
|
25
|
+
throw new TypeError(
|
|
26
|
+
`spindle: "${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A namespace with no prototype holding the own entries of `sources`. */
|
|
32
|
+
export function createNamespace(
|
|
33
|
+
...sources: readonly (object | null | undefined)[]
|
|
34
|
+
): Namespace {
|
|
35
|
+
const ns = Object.create(null) as Namespace;
|
|
36
|
+
for (const source of sources) {
|
|
37
|
+
if (!source) continue;
|
|
38
|
+
for (const key of Object.keys(source)) {
|
|
39
|
+
ns[key] = (source as Namespace)[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return ns;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether `value` is an object with no prototype. */
|
|
46
|
+
export const isNamespace = (value: object): boolean =>
|
|
47
|
+
Object.getPrototypeOf(value) === null;
|
|
48
|
+
|
|
49
|
+
/** `ns` itself if it has no prototype, else a namespace copy of it. */
|
|
50
|
+
export const asNamespace = (ns: Namespace): Namespace =>
|
|
51
|
+
isNamespace(ns) ? ns : createNamespace(ns);
|
|
52
|
+
|
|
53
|
+
/** A namespace copy of `ns` with `key` set to `value`. */
|
|
54
|
+
export function withEntry(
|
|
55
|
+
ns: Namespace,
|
|
56
|
+
key: string,
|
|
57
|
+
value: unknown,
|
|
58
|
+
): Namespace {
|
|
59
|
+
const next = createNamespace(ns);
|
|
60
|
+
next[key] = value;
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** An empty namespace that cannot change. */
|
|
65
|
+
export const EMPTY_NAMESPACE: Namespace = Object.freeze(createNamespace());
|
|
66
|
+
|
|
67
|
+
/** `ns[key]` if `ns` holds it as an own property, else undefined. */
|
|
68
|
+
export const ownValue = (ns: object, key: string): unknown =>
|
|
69
|
+
Object.prototype.hasOwnProperty.call(ns, key)
|
|
70
|
+
? (ns as Namespace)[key]
|
|
71
|
+
: undefined;
|
package/src/utils/object-path.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { isDraft } from 'immer';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const hasOwn = (obj: object, key: string): boolean =>
|
|
4
|
+
Object.prototype.hasOwnProperty.call(obj, key);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Traverse dot-path segments on an object and return the nested value.
|
|
8
|
+
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
9
|
+
* are not story state: unless an object holds one as its own property, it
|
|
10
|
+
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
11
|
+
* (class getters, `size` of a Map) are read.
|
|
12
|
+
*/
|
|
4
13
|
export function getByPath(
|
|
5
14
|
obj: Record<string, unknown>,
|
|
6
15
|
segments: readonly string[],
|
|
@@ -8,23 +17,50 @@ export function getByPath(
|
|
|
8
17
|
let current: unknown = obj;
|
|
9
18
|
for (const seg of segments) {
|
|
10
19
|
if (current == null || typeof current !== 'object') return undefined;
|
|
20
|
+
if (seg in Object.prototype && !hasOwn(current, seg)) return undefined;
|
|
11
21
|
current = (current as Record<string, unknown>)[seg];
|
|
12
22
|
}
|
|
13
23
|
return current;
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
27
|
+
const isArrayKey = (key: string): boolean =>
|
|
28
|
+
key === 'length' || /^(0|[1-9]\d*)$/.test(key);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Built-ins whose content is not their properties: a property written into
|
|
32
|
+
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
33
|
+
*/
|
|
34
|
+
const builtinName = (value: object): string | undefined =>
|
|
35
|
+
value instanceof Map
|
|
36
|
+
? 'a Map'
|
|
37
|
+
: value instanceof Set
|
|
38
|
+
? 'a Set'
|
|
39
|
+
: value instanceof Date
|
|
40
|
+
? 'a Date'
|
|
41
|
+
: value instanceof RegExp
|
|
42
|
+
? 'a RegExp'
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
16
45
|
/**
|
|
17
46
|
* Shallow copy that keeps the prototype, so a registered class instance
|
|
18
47
|
* stays an instance of its class (with the same own keys deepClone copies).
|
|
19
48
|
*/
|
|
20
49
|
function shallowCopy(value: object): Record<string, unknown> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
const copy = Array.isArray(value)
|
|
51
|
+
? []
|
|
52
|
+
: (Object.create(Object.getPrototypeOf(value) as object | null) as object);
|
|
53
|
+
// Define rather than assign (Object.assign), so that a "__proto__" key
|
|
54
|
+
// stays a key instead of replacing the copy's prototype
|
|
55
|
+
for (const key of Object.keys(value)) {
|
|
56
|
+
Object.defineProperty(copy, key, {
|
|
57
|
+
value: (value as Record<string, unknown>)[key],
|
|
58
|
+
enumerable: true,
|
|
59
|
+
writable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return copy as Record<string, unknown>;
|
|
28
64
|
}
|
|
29
65
|
|
|
30
66
|
export interface SetByPathOptions {
|
|
@@ -45,6 +81,12 @@ export interface SetByPathOptions {
|
|
|
45
81
|
* instance shared with the previous state and with history, and subscribers
|
|
46
82
|
* would see no change. Outside a draft (a private working copy) the path is
|
|
47
83
|
* written in place.
|
|
84
|
+
*
|
|
85
|
+
* The path goes through own properties of plain objects, class instances
|
|
86
|
+
* and arrays (by index); anything else throws a TypeError rather than
|
|
87
|
+
* writing where no clone or save would see it: an inherited property such
|
|
88
|
+
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
89
|
+
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
48
90
|
*/
|
|
49
91
|
export function setByPath(
|
|
50
92
|
root: Record<string, unknown>,
|
|
@@ -59,22 +101,56 @@ export function setByPath(
|
|
|
59
101
|
/**
|
|
60
102
|
* Delete the property at dot-path `segments` below `root`, copying
|
|
61
103
|
* undrafted objects on the way down like setByPath(). Does nothing when an
|
|
62
|
-
* intermediate is missing or not an object, or the property is absent
|
|
104
|
+
* intermediate is missing or not an object, or the property is absent (or
|
|
105
|
+
* only inherited).
|
|
63
106
|
*/
|
|
64
107
|
export function deleteByPath(
|
|
65
108
|
root: Record<string, unknown>,
|
|
66
109
|
segments: readonly string[],
|
|
67
110
|
): void {
|
|
68
111
|
const last = segments[segments.length - 1]!;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
112
|
+
checkSegments(segments);
|
|
113
|
+
// Check first, so a no-op delete copies nothing
|
|
114
|
+
let holder: unknown = root;
|
|
115
|
+
for (const seg of segments.slice(0, -1)) {
|
|
116
|
+
if (holder == null || typeof holder !== 'object' || !hasOwn(holder, seg)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
holder = (holder as Record<string, unknown>)[seg];
|
|
120
|
+
}
|
|
121
|
+
if (holder == null || typeof holder !== 'object' || !hasOwn(holder, last)) {
|
|
72
122
|
return;
|
|
73
123
|
}
|
|
74
124
|
const parent = walkToParent(root, segments, false);
|
|
75
125
|
delete parent[last];
|
|
76
126
|
}
|
|
77
127
|
|
|
128
|
+
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
129
|
+
function checkSegments(segments: readonly string[]): void {
|
|
130
|
+
if (segments.includes('__proto__')) {
|
|
131
|
+
throw new TypeError(
|
|
132
|
+
`spindle: Cannot use "__proto__" in a variable path ("${segments.join('.')}")`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Throw unless `holder` can take `key` as story state (see setByPath). */
|
|
138
|
+
function checkHolder(
|
|
139
|
+
holder: object,
|
|
140
|
+
key: string,
|
|
141
|
+
segments: readonly string[],
|
|
142
|
+
depth: number,
|
|
143
|
+
): void {
|
|
144
|
+
const builtin = builtinName(holder);
|
|
145
|
+
const kind =
|
|
146
|
+
builtin ?? (Array.isArray(holder) && !isArrayKey(key) ? 'an array' : '');
|
|
147
|
+
if (kind) {
|
|
148
|
+
throw new TypeError(
|
|
149
|
+
`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join('.')}")`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
78
154
|
/**
|
|
79
155
|
* Walk to the object holding the last of `segments`, copying undrafted
|
|
80
156
|
* objects when `root` is an Immer draft (see setByPath). A missing or
|
|
@@ -86,11 +162,14 @@ function walkToParent(
|
|
|
86
162
|
segments: readonly string[],
|
|
87
163
|
createMissing: boolean,
|
|
88
164
|
): Record<string, unknown> {
|
|
165
|
+
checkSegments(segments);
|
|
89
166
|
const copyOnWrite = isDraft(root);
|
|
90
167
|
let current: Record<string, unknown> = root;
|
|
91
168
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
92
169
|
const seg = segments[i]!;
|
|
93
|
-
|
|
170
|
+
checkHolder(current, seg, segments, i);
|
|
171
|
+
// An inherited property (a method, "constructor") counts as missing
|
|
172
|
+
let next = hasOwn(current, seg) ? current[seg] : undefined;
|
|
94
173
|
if (next == null || typeof next !== 'object') {
|
|
95
174
|
if (!createMissing) {
|
|
96
175
|
throw new TypeError(
|
|
@@ -99,11 +178,17 @@ function walkToParent(
|
|
|
99
178
|
}
|
|
100
179
|
next = {};
|
|
101
180
|
current[seg] = next;
|
|
102
|
-
} else if (copyOnWrite && !isDraft(next)) {
|
|
181
|
+
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
103
182
|
next = shallowCopy(next);
|
|
104
183
|
current[seg] = next;
|
|
105
184
|
}
|
|
106
185
|
current = next as Record<string, unknown>;
|
|
107
186
|
}
|
|
187
|
+
checkHolder(
|
|
188
|
+
current,
|
|
189
|
+
segments[segments.length - 1]!,
|
|
190
|
+
segments,
|
|
191
|
+
segments.length - 1,
|
|
192
|
+
);
|
|
108
193
|
return current;
|
|
109
194
|
}
|
package/src/utils/stable-key.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type Constructor = new (...args: any[]) => any;
|
|
1
|
+
import { registeredClassName } from '../class-registry';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Content-derived string key for a value, used to remount components when
|
|
@@ -10,9 +8,10 @@ type Constructor = new (...args: any[]) => any;
|
|
|
10
8
|
* plain objects of those) produce exactly their `JSON.stringify` output. Other
|
|
11
9
|
* values get distinct, unambiguous markers so their contents are reflected
|
|
12
10
|
* too: Map and Set entries (nested at any depth), Date, RegExp, BigInt,
|
|
13
|
-
* undefined, NaN/±Infinity, symbols, functions
|
|
14
|
-
* instances.
|
|
15
|
-
*
|
|
11
|
+
* undefined, NaN/±Infinity, symbols (by description), functions (by name)
|
|
12
|
+
* and registered class instances. A hole in an array reads as undefined.
|
|
13
|
+
* Cyclic references become a back-reference marker instead of throwing,
|
|
14
|
+
* and the function never throws.
|
|
16
15
|
*/
|
|
17
16
|
export function stableKey(value: unknown): string {
|
|
18
17
|
try {
|
|
@@ -35,7 +34,11 @@ function keyOf(val: unknown, ancestors: object[]): string {
|
|
|
35
34
|
case 'undefined':
|
|
36
35
|
return 'undefined';
|
|
37
36
|
case 'symbol':
|
|
38
|
-
|
|
37
|
+
// Quote the description: `Symbol('a),Symbol(b')` must not read like
|
|
38
|
+
// two symbols.
|
|
39
|
+
return val.description === undefined
|
|
40
|
+
? 'Symbol()'
|
|
41
|
+
: `Symbol(${JSON.stringify(val.description)})`;
|
|
39
42
|
case 'function':
|
|
40
43
|
return `<function ${JSON.stringify(val.name)}>`;
|
|
41
44
|
}
|
|
@@ -51,7 +54,9 @@ function keyOf(val: unknown, ancestors: object[]): string {
|
|
|
51
54
|
ancestors.push(obj);
|
|
52
55
|
try {
|
|
53
56
|
if (Array.isArray(val)) {
|
|
54
|
-
|
|
57
|
+
// Array.from reads a hole as undefined (map would skip it, giving
|
|
58
|
+
// `[,]` the key of `[]`), matching deepClone, which fills holes.
|
|
59
|
+
return `[${Array.from(val, (v) => keyOf(v, ancestors)).join(',')}]`;
|
|
55
60
|
}
|
|
56
61
|
if (val instanceof Map) {
|
|
57
62
|
const entries = [...val].map(
|
|
@@ -67,7 +72,7 @@ function keyOf(val: unknown, ancestors: object[]): string {
|
|
|
67
72
|
const body = Object.keys(record)
|
|
68
73
|
.map((k) => `${JSON.stringify(k)}:${keyOf(record[k], ancestors)}`)
|
|
69
74
|
.join(',');
|
|
70
|
-
const className =
|
|
75
|
+
const className = registeredClassName(obj);
|
|
71
76
|
return className === undefined
|
|
72
77
|
? `{${body}}`
|
|
73
78
|
: `Class(${JSON.stringify(className)}){${body}}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ASTNode } from '../markup/ast';
|
|
2
|
+
import { checkVariableName } from '../utils/namespace';
|
|
2
3
|
|
|
3
4
|
interface WidgetEntry {
|
|
4
5
|
body: ASTNode[];
|
|
@@ -8,6 +9,11 @@ interface WidgetEntry {
|
|
|
8
9
|
|
|
9
10
|
const widgets = new Map<string, WidgetEntry>();
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Register a widget. Its `@` parameters become locals of its body, so one
|
|
14
|
+
* that no namespace can hold (`@__proto__`) throws a TypeError and the
|
|
15
|
+
* widget is not registered.
|
|
16
|
+
*/
|
|
11
17
|
export function registerWidget(
|
|
12
18
|
name: string,
|
|
13
19
|
bodyAST: ASTNode[],
|
|
@@ -15,6 +21,9 @@ export function registerWidget(
|
|
|
15
21
|
isBlock = false,
|
|
16
22
|
): void {
|
|
17
23
|
const filteredParams = params.filter((p) => p !== '@children');
|
|
24
|
+
for (const param of filteredParams) {
|
|
25
|
+
if (param.startsWith('@')) checkVariableName(param.slice(1), param);
|
|
26
|
+
}
|
|
18
27
|
widgets.set(name.toLowerCase(), {
|
|
19
28
|
body: bodyAST,
|
|
20
29
|
params: filteredParams,
|
package/types/index.d.ts
CHANGED
|
@@ -401,6 +401,8 @@ export interface MacroContext {
|
|
|
401
401
|
nobr?: boolean;
|
|
402
402
|
locals?: Record<string, unknown>;
|
|
403
403
|
inline?: boolean;
|
|
404
|
+
/** Literal content, as inside `<pre>`: no markdown processing. */
|
|
405
|
+
raw?: boolean;
|
|
404
406
|
},
|
|
405
407
|
) => ComponentChildren;
|
|
406
408
|
/** Render AST nodes as inline content (no markdown block processing). */
|
|
@@ -417,6 +419,20 @@ export interface MacroContext {
|
|
|
417
419
|
};
|
|
418
420
|
}
|
|
419
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Context object passed to a macro's text form (`MacroDefinition.text`).
|
|
424
|
+
* @see {@link ../../src/registry.ts} for the implementation.
|
|
425
|
+
*/
|
|
426
|
+
export interface MacroTextContext {
|
|
427
|
+
/** Evaluate an expression in the current scope. */
|
|
428
|
+
evaluate: (expr: string) => unknown;
|
|
429
|
+
/**
|
|
430
|
+
* The text of AST nodes (a body or branch) in the current scope, with
|
|
431
|
+
* `locals` (keys without `@`) added on top of the current locals.
|
|
432
|
+
*/
|
|
433
|
+
renderText: (nodes: ASTNode[], locals?: Record<string, unknown>) => string;
|
|
434
|
+
}
|
|
435
|
+
|
|
420
436
|
/**
|
|
421
437
|
* Configuration object for `Story.defineMacro()`.
|
|
422
438
|
* @see {@link ../../src/define-macro.ts} for the implementation.
|
|
@@ -427,7 +443,7 @@ export interface MacroDefinition {
|
|
|
427
443
|
subMacros?: string[];
|
|
428
444
|
/** Accept a `{name}...{/name}` body. Inferred from `subMacros` when omitted. */
|
|
429
445
|
block?: boolean;
|
|
430
|
-
/** Resolve `{$var}
|
|
446
|
+
/** Resolve markup (`{$var}`, expressions, macros) in the macro's class/id, and provide `ctx.resolve`. */
|
|
431
447
|
interpolate?: boolean;
|
|
432
448
|
/** Provide `ctx.merged` and `ctx.evaluate` (variables, temporaries, locals, transients). */
|
|
433
449
|
merged?: boolean;
|
|
@@ -438,6 +454,12 @@ export interface MacroDefinition {
|
|
|
438
454
|
/** Tooling hint: positional parameters. */
|
|
439
455
|
parameters?: ParameterDef[];
|
|
440
456
|
render: (props: MacroProps, ctx: MacroContext) => ComponentChildren;
|
|
457
|
+
/**
|
|
458
|
+
* The macro's text form, used where markup becomes a string: HTML
|
|
459
|
+
* attribute values, image alt text and link titles, macro labels. Without
|
|
460
|
+
* one, the macro can't be used there and is reported as an error.
|
|
461
|
+
*/
|
|
462
|
+
text?: (props: MacroProps, ctx: MacroTextContext) => string;
|
|
441
463
|
}
|
|
442
464
|
|
|
443
465
|
/**
|
|
@@ -549,8 +571,11 @@ export interface StoryAPI {
|
|
|
549
571
|
save(slot?: string, custom?: Record<string, unknown>): Promise<void>;
|
|
550
572
|
|
|
551
573
|
/**
|
|
552
|
-
* Load a saved state (quick load).
|
|
553
|
-
*
|
|
574
|
+
* Load a saved state (quick load). The game moves to the loaded save's
|
|
575
|
+
* playthrough, in call order: a save issued after the load belongs to it.
|
|
576
|
+
* Resolves once the loaded state is applied (immediately if the slot is
|
|
577
|
+
* empty, without loading if a restart was issued after the load); rejects
|
|
578
|
+
* if loading fails.
|
|
554
579
|
*/
|
|
555
580
|
load(slot?: string): Promise<void>;
|
|
556
581
|
|
|
@@ -671,11 +696,19 @@ export interface StoryAPI {
|
|
|
671
696
|
getInfo(): Promise<StorageInfo>;
|
|
672
697
|
/** Get browser storage quota estimate. */
|
|
673
698
|
getQuota(): Promise<StorageQuota>;
|
|
674
|
-
/**
|
|
699
|
+
/**
|
|
700
|
+
* Delete all saves and playthroughs of the current game and restart it.
|
|
701
|
+
* The restart happens at once; the promise settles once the data is
|
|
702
|
+
* deleted.
|
|
703
|
+
*/
|
|
675
704
|
clearGameData(): Promise<void>;
|
|
676
|
-
/** Delete all Spindle data across all games. */
|
|
705
|
+
/** Delete all Spindle data across all games and restart, as clearGameData. */
|
|
677
706
|
clearAllData(): Promise<void>;
|
|
678
|
-
/**
|
|
707
|
+
/**
|
|
708
|
+
* Delete a specific playthrough and its saves. Deleting the current
|
|
709
|
+
* playthrough (the one the game started, restarted or last loaded a save
|
|
710
|
+
* in) moves the running game to a new one.
|
|
711
|
+
*/
|
|
679
712
|
deletePlaythrough(playthroughId: string): Promise<void>;
|
|
680
713
|
/** The active storage backend. */
|
|
681
714
|
readonly backend: 'indexeddb' | 'localstorage' | 'memory';
|
|
@@ -731,7 +764,10 @@ export interface StoryAPI {
|
|
|
731
764
|
|
|
732
765
|
/** Story configuration. */
|
|
733
766
|
readonly config: {
|
|
734
|
-
/**
|
|
767
|
+
/**
|
|
768
|
+
* Maximum number of history moments to retain. Lowering it trims history
|
|
769
|
+
* at once, keeping the newest moments that include the current one.
|
|
770
|
+
*/
|
|
735
771
|
maxHistory: number;
|
|
736
772
|
/**
|
|
737
773
|
* Key that triggers a quick save (`KeyboardEvent.key`, default `'F6'`).
|