@rohal12/spindle 0.52.7 → 0.52.8
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 +26 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +710 -703
- package/dist/pkg/macro-registry.json +18 -0
- package/package.json +1 -1
- package/src/action-registry.ts +9 -0
- package/src/class-registry.ts +1 -5
- package/src/components/Passage.tsx +35 -39
- package/src/components/PassageDialog.tsx +2 -2
- package/src/components/StoryInterface.tsx +2 -2
- package/src/components/macros/Checkbox.tsx +3 -8
- package/src/components/macros/Computed.tsx +1 -1
- package/src/components/macros/Cycle.tsx +3 -6
- package/src/components/macros/ExprDisplay.tsx +12 -29
- package/src/components/macros/If.tsx +35 -41
- package/src/components/macros/Include.tsx +2 -1
- package/src/components/macros/Listbox.tsx +5 -7
- package/src/components/macros/Print.tsx +3 -5
- package/src/components/macros/Radiobutton.tsx +3 -5
- package/src/components/macros/Repeat.tsx +2 -1
- package/src/components/macros/Switch.tsx +6 -13
- package/src/components/macros/Timed.tsx +14 -18
- package/src/components/macros/Type.tsx +2 -1
- package/src/components/macros/VarDisplay.tsx +12 -32
- package/src/components/macros/Widget.tsx +49 -20
- package/src/components/macros/branches.ts +18 -0
- package/src/components/macros/display.ts +19 -0
- package/src/components/macros/input-macro.ts +33 -11
- package/src/components/macros/macro-args.ts +7 -0
- package/src/components/macros/option-utils.ts +10 -7
- package/src/define-macro.ts +4 -6
- package/src/expression.ts +58 -40
- package/src/hooks/use-action.ts +7 -15
- package/src/index.tsx +5 -15
- package/src/interpolation.ts +34 -41
- package/src/markup/ast.ts +4 -3
- package/src/markup/render.tsx +4 -12
- package/src/registry.ts +13 -12
- package/src/saves/save-manager.ts +18 -13
- package/src/saves/storage.ts +19 -20
- package/src/story-api.ts +30 -73
- package/src/structural.ts +1 -18
- package/src/types-drift-check.ts +37 -50
- package/src/utils/macro-names.ts +40 -0
- package/src/utils/namespace.ts +17 -0
- package/src/widgets/widget-registry.ts +5 -4
package/dist/pkg/headless.js
CHANGED
|
@@ -52,6 +52,19 @@ var hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
|
52
52
|
/** `ns[key]` if `ns` holds it as an own property, else undefined. */
|
|
53
53
|
var ownValue = (ns, key) => ns && hasOwn(ns, key) ? ns[key] : void 0;
|
|
54
54
|
/**
|
|
55
|
+
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
56
|
+
* property (as JSON.parse does) instead of replacing the prototype.
|
|
57
|
+
*/
|
|
58
|
+
function setOwn(target, key, value) {
|
|
59
|
+
if (key === "__proto__") Object.defineProperty(target, key, {
|
|
60
|
+
value,
|
|
61
|
+
enumerable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
configurable: true
|
|
64
|
+
});
|
|
65
|
+
else target[key] = value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
55
68
|
* A record with no prototype holding the own entries of `sources`, or only
|
|
56
69
|
* those whose value passes `keep`.
|
|
57
70
|
*/
|
|
@@ -102,351 +115,6 @@ function createCounts(source, name) {
|
|
|
102
115
|
return counts;
|
|
103
116
|
}
|
|
104
117
|
//#endregion
|
|
105
|
-
//#region src/utils/object-path.ts
|
|
106
|
-
/**
|
|
107
|
-
* Traverse dot-path segments on an object and return the nested value.
|
|
108
|
-
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
109
|
-
* are not story state: unless an object holds one as its own property, it
|
|
110
|
-
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
111
|
-
* (class getters, `size` of a Map) are read.
|
|
112
|
-
*/
|
|
113
|
-
function getByPath(obj, segments) {
|
|
114
|
-
let current = obj;
|
|
115
|
-
for (const seg of segments) {
|
|
116
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
117
|
-
if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
|
|
118
|
-
current = current[seg];
|
|
119
|
-
}
|
|
120
|
-
return current;
|
|
121
|
-
}
|
|
122
|
-
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
123
|
-
var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
124
|
-
/**
|
|
125
|
-
* Built-ins whose content is not their properties: a property written into
|
|
126
|
-
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
127
|
-
*/
|
|
128
|
-
var builtinName = (value) => value instanceof Map ? "a Map" : value instanceof Set ? "a Set" : value instanceof Date ? "a Date" : value instanceof RegExp ? "a RegExp" : void 0;
|
|
129
|
-
/**
|
|
130
|
-
* Shallow copy that keeps the prototype, so a registered class instance
|
|
131
|
-
* stays an instance of its class (with the same own keys deepClone copies).
|
|
132
|
-
*/
|
|
133
|
-
function shallowCopy(value) {
|
|
134
|
-
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
135
|
-
for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
|
|
136
|
-
value: value[key],
|
|
137
|
-
enumerable: true,
|
|
138
|
-
writable: true,
|
|
139
|
-
configurable: true
|
|
140
|
-
});
|
|
141
|
-
return copy;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Set a value at dot-path `segments` below `root`.
|
|
145
|
-
*
|
|
146
|
-
* When `root` is an Immer draft, objects Immer does not draft (registered
|
|
147
|
-
* class instances and everything below them) are copied on the way down and
|
|
148
|
-
* the copy is assigned to its parent, so the write produces new references
|
|
149
|
-
* up to the root. Writing into such an object directly would mutate the
|
|
150
|
-
* instance shared with the previous state and with history, and subscribers
|
|
151
|
-
* would see no change. Outside a draft (a private working copy) the path is
|
|
152
|
-
* written in place.
|
|
153
|
-
*
|
|
154
|
-
* The path goes through own properties of plain objects, class instances
|
|
155
|
-
* and arrays (by index); anything else throws a TypeError rather than
|
|
156
|
-
* writing where no clone or save would see it: an inherited property such
|
|
157
|
-
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
158
|
-
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
159
|
-
*/
|
|
160
|
-
function setByPath(root, segments, value, options = {}) {
|
|
161
|
-
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
162
|
-
parent[segments[segments.length - 1]] = value;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Delete the property at dot-path `segments` below `root`, copying
|
|
166
|
-
* undrafted objects on the way down like setByPath(). Does nothing when an
|
|
167
|
-
* intermediate is missing or not an object, or the property is absent (or
|
|
168
|
-
* only inherited).
|
|
169
|
-
*/
|
|
170
|
-
function deleteByPath(root, segments) {
|
|
171
|
-
const last = segments[segments.length - 1];
|
|
172
|
-
checkSegments(segments);
|
|
173
|
-
let holder = root;
|
|
174
|
-
for (const seg of segments.slice(0, -1)) {
|
|
175
|
-
if (holder == null || typeof holder !== "object" || !hasOwn(holder, seg)) return;
|
|
176
|
-
holder = holder[seg];
|
|
177
|
-
}
|
|
178
|
-
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
179
|
-
const parent = walkToParent(root, segments, false);
|
|
180
|
-
delete parent[last];
|
|
181
|
-
}
|
|
182
|
-
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
183
|
-
function checkSegments(segments) {
|
|
184
|
-
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
185
|
-
}
|
|
186
|
-
/** Throw unless `holder` can take `key` as story state (see setByPath). */
|
|
187
|
-
function checkHolder(holder, key, segments, depth) {
|
|
188
|
-
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
189
|
-
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Walk to the object holding the last of `segments`, copying undrafted
|
|
193
|
-
* objects when `root` is an Immer draft (see setByPath). A missing or
|
|
194
|
-
* non-object intermediate is replaced by a plain object when
|
|
195
|
-
* `createMissing` is set, and throws a TypeError otherwise.
|
|
196
|
-
*/
|
|
197
|
-
function walkToParent(root, segments, createMissing) {
|
|
198
|
-
checkSegments(segments);
|
|
199
|
-
const copyOnWrite = isDraft(root);
|
|
200
|
-
let current = root;
|
|
201
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
202
|
-
const seg = segments[i];
|
|
203
|
-
checkHolder(current, seg, segments, i);
|
|
204
|
-
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
205
|
-
if (next == null || typeof next !== "object") {
|
|
206
|
-
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
207
|
-
next = {};
|
|
208
|
-
current[seg] = next;
|
|
209
|
-
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
210
|
-
next = shallowCopy(next);
|
|
211
|
-
current[seg] = next;
|
|
212
|
-
}
|
|
213
|
-
current = next;
|
|
214
|
-
}
|
|
215
|
-
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
216
|
-
return current;
|
|
217
|
-
}
|
|
218
|
-
//#endregion
|
|
219
|
-
//#region src/structural.ts
|
|
220
|
-
/**
|
|
221
|
-
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
222
|
-
* property (as JSON.parse does) instead of replacing the prototype.
|
|
223
|
-
*/
|
|
224
|
-
function setOwn(target, key, value) {
|
|
225
|
-
if (key === "__proto__") Object.defineProperty(target, key, {
|
|
226
|
-
value,
|
|
227
|
-
enumerable: true,
|
|
228
|
-
writable: true,
|
|
229
|
-
configurable: true
|
|
230
|
-
});
|
|
231
|
-
else target[key] = value;
|
|
232
|
-
}
|
|
233
|
-
function isPlainObject(value) {
|
|
234
|
-
if (typeof value !== "object" || value === null) return false;
|
|
235
|
-
const proto = Object.getPrototypeOf(value);
|
|
236
|
-
return proto === Object.prototype || proto === null;
|
|
237
|
-
}
|
|
238
|
-
function deepClone(value, options = {}) {
|
|
239
|
-
const seen = /* @__PURE__ */ new Map();
|
|
240
|
-
function clone(val) {
|
|
241
|
-
if (val === null || typeof val !== "object") return val;
|
|
242
|
-
const obj = val;
|
|
243
|
-
if (seen.has(obj)) return seen.get(obj);
|
|
244
|
-
if (val instanceof Date) return new Date(val.getTime());
|
|
245
|
-
if (val instanceof RegExp) return new RegExp(val.source, val.flags);
|
|
246
|
-
if (Array.isArray(val)) {
|
|
247
|
-
const arr = [];
|
|
248
|
-
seen.set(obj, arr);
|
|
249
|
-
for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
|
|
250
|
-
return arr;
|
|
251
|
-
}
|
|
252
|
-
if (val instanceof Map) {
|
|
253
|
-
const copy = /* @__PURE__ */ new Map();
|
|
254
|
-
seen.set(obj, copy);
|
|
255
|
-
for (const [k, v] of val) copy.set(clone(k), clone(v));
|
|
256
|
-
return copy;
|
|
257
|
-
}
|
|
258
|
-
if (val instanceof Set) {
|
|
259
|
-
const copy = /* @__PURE__ */ new Set();
|
|
260
|
-
seen.set(obj, copy);
|
|
261
|
-
for (const v of val) copy.add(clone(v));
|
|
262
|
-
return copy;
|
|
263
|
-
}
|
|
264
|
-
const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
|
|
265
|
-
if (!keepsProto && options.keepUnregistered) return val;
|
|
266
|
-
const copy = keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {};
|
|
267
|
-
seen.set(obj, copy);
|
|
268
|
-
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
269
|
-
return copy;
|
|
270
|
-
}
|
|
271
|
-
return clone(value);
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Structural equality over the value types deepClone() supports: primitives,
|
|
275
|
-
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
276
|
-
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
277
|
-
* are compared by length and index, so a hole equals an undefined element
|
|
278
|
-
* (deepClone() and save/load turn holes into undefined elements).
|
|
279
|
-
*/
|
|
280
|
-
function deepEqual(a, b) {
|
|
281
|
-
return equal(a, b, /* @__PURE__ */ new Map());
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
285
|
-
* cycle) assumes it equal. Every object may pair with several others, since
|
|
286
|
-
* cycles of different lengths can still unfold to the same value.
|
|
287
|
-
*/
|
|
288
|
-
function equal(a, b, assumed) {
|
|
289
|
-
if (Object.is(a, b)) return true;
|
|
290
|
-
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
291
|
-
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
292
|
-
const pairs = assumed.get(a);
|
|
293
|
-
if (pairs?.has(b)) return true;
|
|
294
|
-
if (pairs) pairs.add(b);
|
|
295
|
-
else assumed.set(a, new Set([b]));
|
|
296
|
-
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
297
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
298
|
-
if (a instanceof Map || a instanceof Set) {
|
|
299
|
-
const bc = b;
|
|
300
|
-
if (a.size !== bc.size) return false;
|
|
301
|
-
const ai = a.entries();
|
|
302
|
-
const bi = bc.entries();
|
|
303
|
-
for (let x = ai.next(), y = bi.next(); !x.done; x = ai.next(), y = bi.next()) if (!equal(x.value, y.value, assumed)) return false;
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
if (Array.isArray(a)) {
|
|
307
|
-
const bc = b;
|
|
308
|
-
if (a.length !== bc.length) return false;
|
|
309
|
-
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
const ao = a;
|
|
313
|
-
const bo = b;
|
|
314
|
-
const keys = Object.keys(ao);
|
|
315
|
-
if (keys.length !== Object.keys(bo).length) return false;
|
|
316
|
-
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Objects merged property by property: plain objects and (registered)
|
|
321
|
-
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
322
|
-
* replaced as a whole, since their elements have no stable identity to
|
|
323
|
-
* merge by (a shift moves every index).
|
|
324
|
-
*/
|
|
325
|
-
function isMergeable(value) {
|
|
326
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
330
|
-
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
331
|
-
*/
|
|
332
|
-
function mergesWith(a, b, arrays) {
|
|
333
|
-
if (arrays && (Array.isArray(a) || Array.isArray(b))) return Array.isArray(a) && Array.isArray(b);
|
|
334
|
-
return isMergeable(a) && isMergeable(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* The keys at which `after` differs from `before`, two values that merge
|
|
338
|
-
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
339
|
-
* updates are immutable, so a value that kept its identity is no change,
|
|
340
|
-
* and one rebuilt with equal content (mutation code commits whole values)
|
|
341
|
-
* is none either. Values that merge are reported as 'nested', to be
|
|
342
|
-
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
343
|
-
* merge too: elements at the indices both hold are compared, and the
|
|
344
|
-
* elements one has beyond the other's length are added or deleted.
|
|
345
|
-
*/
|
|
346
|
-
function keyChanges(before, after, arrays) {
|
|
347
|
-
const changes = [];
|
|
348
|
-
const compare = (key, b, a) => {
|
|
349
|
-
if (Object.is(b, a)) return;
|
|
350
|
-
if (mergesWith(b, a, arrays)) changes.push({
|
|
351
|
-
key,
|
|
352
|
-
kind: "nested",
|
|
353
|
-
before: b,
|
|
354
|
-
after: a
|
|
355
|
-
});
|
|
356
|
-
else if (!deepEqual(b, a)) changes.push({
|
|
357
|
-
key,
|
|
358
|
-
kind: "changed",
|
|
359
|
-
after: a
|
|
360
|
-
});
|
|
361
|
-
};
|
|
362
|
-
if (Array.isArray(after)) {
|
|
363
|
-
const b = before;
|
|
364
|
-
const a = after;
|
|
365
|
-
for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
|
|
366
|
-
for (let i = b.length; i < a.length; i++) changes.push({
|
|
367
|
-
key: String(i),
|
|
368
|
-
kind: "added",
|
|
369
|
-
after: a[i]
|
|
370
|
-
});
|
|
371
|
-
for (let i = a.length; i < b.length; i++) changes.push({
|
|
372
|
-
key: String(i),
|
|
373
|
-
kind: "deleted"
|
|
374
|
-
});
|
|
375
|
-
return changes;
|
|
376
|
-
}
|
|
377
|
-
for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
378
|
-
else changes.push({
|
|
379
|
-
key,
|
|
380
|
-
kind: "added",
|
|
381
|
-
after: after[key]
|
|
382
|
-
});
|
|
383
|
-
for (const key of Object.keys(before)) if (!hasOwn(after, key)) changes.push({
|
|
384
|
-
key,
|
|
385
|
-
kind: "deleted"
|
|
386
|
-
});
|
|
387
|
-
return changes;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* The property paths where `after` differs from `before`, two objects of
|
|
391
|
-
* one class: objects that merge (see isMergeable) are compared property by
|
|
392
|
-
* property, any other value as a whole.
|
|
393
|
-
*/
|
|
394
|
-
function diffPaths(before, after) {
|
|
395
|
-
const changes = [];
|
|
396
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
397
|
-
(function walk(b, a, path) {
|
|
398
|
-
if (ancestors.has(a)) return;
|
|
399
|
-
ancestors.add(a);
|
|
400
|
-
for (const change of keyChanges(b, a, false)) {
|
|
401
|
-
const at = [...path, change.key];
|
|
402
|
-
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
403
|
-
else changes.push(change.kind === "deleted" ? {
|
|
404
|
-
path: at,
|
|
405
|
-
deleted: true
|
|
406
|
-
} : {
|
|
407
|
-
path: at,
|
|
408
|
-
deleted: false,
|
|
409
|
-
value: change.after
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
ancestors.delete(a);
|
|
413
|
-
})(before, after, []);
|
|
414
|
-
return changes;
|
|
415
|
-
}
|
|
416
|
-
/** Whether `target` already holds what `change` would write. */
|
|
417
|
-
function isApplied(target, change) {
|
|
418
|
-
const parent = getByPath(target, change.path.slice(0, -1));
|
|
419
|
-
if (parent === null || typeof parent !== "object") return change.deleted;
|
|
420
|
-
const key = change.path[change.path.length - 1];
|
|
421
|
-
if (!hasOwn(parent, key)) return change.deleted;
|
|
422
|
-
return !change.deleted && deepEqual(parent[key], change.value);
|
|
423
|
-
}
|
|
424
|
-
function applyChange(target, change) {
|
|
425
|
-
if (change.deleted) deleteByPath(target, change.path);
|
|
426
|
-
else setByPath(target, change.path, change.value);
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
430
|
-
* index) into `target`, another version of the same object or array.
|
|
431
|
-
* Values that changed are written whole, as deep copies; for values that
|
|
432
|
-
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
433
|
-
* whether it did. Into an array, elements removed from the end are removed
|
|
434
|
-
* at the same indices and elements added are appended, and changes at
|
|
435
|
-
* indices `target` lacks are dropped: where `target` was resized, its
|
|
436
|
-
* indices do not line up with `before`'s.
|
|
437
|
-
*/
|
|
438
|
-
function mergeKeys(target, before, after, mergeNested) {
|
|
439
|
-
const list = Array.isArray(target) ? target : void 0;
|
|
440
|
-
for (const change of keyChanges(before, after, true)) {
|
|
441
|
-
const { key } = change;
|
|
442
|
-
if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
|
|
443
|
-
else delete target[key];
|
|
444
|
-
else if (list && change.kind === "added") list.push(deepClone(change.after));
|
|
445
|
-
else if (list && Number(key) >= list.length) continue;
|
|
446
|
-
else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
//#endregion
|
|
450
118
|
//#region src/class-registry.ts
|
|
451
119
|
var registry$1 = /* @__PURE__ */ new Map();
|
|
452
120
|
var ctorToName = /* @__PURE__ */ new Map();
|
|
@@ -2624,9 +2292,46 @@ function tokenize(input, options = {}) {
|
|
|
2624
2292
|
return tokens;
|
|
2625
2293
|
}
|
|
2626
2294
|
//#endregion
|
|
2295
|
+
//#region src/utils/macro-names.ts
|
|
2296
|
+
/**
|
|
2297
|
+
* Macro and widget names are case-insensitive: `{StatusBar}` invokes the
|
|
2298
|
+
* widget defined as `statusbar`. A NameMap keys its entries by the lowercased
|
|
2299
|
+
* name.
|
|
2300
|
+
*/
|
|
2301
|
+
var NameMap = class extends Map {
|
|
2302
|
+
get(name) {
|
|
2303
|
+
return super.get(name.toLowerCase());
|
|
2304
|
+
}
|
|
2305
|
+
set(name, value) {
|
|
2306
|
+
return super.set(name.toLowerCase(), value);
|
|
2307
|
+
}
|
|
2308
|
+
has(name) {
|
|
2309
|
+
return super.has(name.toLowerCase());
|
|
2310
|
+
}
|
|
2311
|
+
delete(name) {
|
|
2312
|
+
return super.delete(name.toLowerCase());
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
/** A set of macro or widget names (see NameMap). */
|
|
2316
|
+
var NameSet = class {
|
|
2317
|
+
constructor(names = []) {
|
|
2318
|
+
this.names = new NameMap();
|
|
2319
|
+
for (const name of names) this.add(name);
|
|
2320
|
+
}
|
|
2321
|
+
add(name) {
|
|
2322
|
+
this.names.set(name, true);
|
|
2323
|
+
}
|
|
2324
|
+
has(name) {
|
|
2325
|
+
return this.names.has(name);
|
|
2326
|
+
}
|
|
2327
|
+
delete(name) {
|
|
2328
|
+
this.names.delete(name);
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
//#endregion
|
|
2627
2332
|
//#region src/markup/ast.ts
|
|
2628
2333
|
/** Macros that require a closing tag and can contain children */
|
|
2629
|
-
var BLOCK_MACROS = new
|
|
2334
|
+
var BLOCK_MACROS = new NameSet([
|
|
2630
2335
|
"if",
|
|
2631
2336
|
"for",
|
|
2632
2337
|
"do",
|
|
@@ -2644,7 +2349,7 @@ var BLOCK_MACROS = new Set([
|
|
|
2644
2349
|
]);
|
|
2645
2350
|
/** Register a custom macro as a block macro so the AST builder nests children. */
|
|
2646
2351
|
function registerBlockMacro(name) {
|
|
2647
|
-
BLOCK_MACROS.add(name
|
|
2352
|
+
BLOCK_MACROS.add(name);
|
|
2648
2353
|
}
|
|
2649
2354
|
/** Map from branch macro name → required parent macro name */
|
|
2650
2355
|
var BRANCH_PARENT = {
|
|
@@ -3038,15 +2743,15 @@ function countQueries(counts) {
|
|
|
3038
2743
|
all: (...names) => names.every((n) => count(n) > 0)
|
|
3039
2744
|
};
|
|
3040
2745
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
const visits = countQueries(
|
|
3048
|
-
const renders = countQueries(
|
|
3049
|
-
|
|
2746
|
+
/**
|
|
2747
|
+
* The history functions of passage code and the story API: the current and
|
|
2748
|
+
* previous passage, and visited()/rendered() with their has-forms, counting
|
|
2749
|
+
* in the counters `visitCounts()` and `renderCounts()` return.
|
|
2750
|
+
*/
|
|
2751
|
+
function historyQueries(visitCounts, renderCounts) {
|
|
2752
|
+
const visits = countQueries(visitCounts);
|
|
2753
|
+
const renders = countQueries(renderCounts);
|
|
2754
|
+
return {
|
|
3050
2755
|
currentPassage,
|
|
3051
2756
|
previousPassage,
|
|
3052
2757
|
visited: visits.count,
|
|
@@ -3056,7 +2761,17 @@ function buildExpressionFns() {
|
|
|
3056
2761
|
rendered: renders.count,
|
|
3057
2762
|
hasRendered: renders.has,
|
|
3058
2763
|
hasRenderedAny: renders.any,
|
|
3059
|
-
hasRenderedAll: renders.all
|
|
2764
|
+
hasRenderedAll: renders.all
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
var cachedFns = null;
|
|
2768
|
+
var cachedVisitCounts = null;
|
|
2769
|
+
var cachedRenderCounts = null;
|
|
2770
|
+
function buildExpressionFns() {
|
|
2771
|
+
const { visitCounts, renderCounts } = useStoryStore.getState();
|
|
2772
|
+
if (cachedFns && cachedVisitCounts === visitCounts && cachedRenderCounts === renderCounts) return cachedFns;
|
|
2773
|
+
cachedFns = {
|
|
2774
|
+
...historyQueries(() => visitCounts, () => renderCounts),
|
|
3060
2775
|
random,
|
|
3061
2776
|
randomInt
|
|
3062
2777
|
};
|
|
@@ -3077,57 +2792,61 @@ function namespaceArg(ns) {
|
|
|
3077
2792
|
Object.setPrototypeOf(ns, null);
|
|
3078
2793
|
return ns;
|
|
3079
2794
|
}
|
|
2795
|
+
/** Run compiled code in `namespaces`. */
|
|
2796
|
+
function run(fn, ...[variables, temporary, locals = EMPTY_NAMESPACE, transient = EMPTY_NAMESPACE]) {
|
|
2797
|
+
return fn(namespaceArg(variables), namespaceArg(temporary), namespaceArg(locals), buildExpressionFns(), namespaceArg(transient));
|
|
2798
|
+
}
|
|
3080
2799
|
/**
|
|
3081
2800
|
* Evaluate an expression and return its value.
|
|
3082
2801
|
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
3083
2802
|
*/
|
|
3084
|
-
function evaluate(expr,
|
|
2803
|
+
function evaluate(expr, ...namespaces) {
|
|
3085
2804
|
const body = `return (${transform(expr)}\n);`;
|
|
3086
|
-
return getOrCompile(body, body)
|
|
2805
|
+
return run(getOrCompile(body, body), ...namespaces);
|
|
3087
2806
|
}
|
|
3088
2807
|
/**
|
|
3089
2808
|
* Execute statements (no return value).
|
|
3090
2809
|
* e.g. execute("$health = 100; $name = 'Hero'", variables, temporary)
|
|
3091
2810
|
*/
|
|
3092
|
-
function execute(code,
|
|
2811
|
+
function execute(code, ...namespaces) {
|
|
3093
2812
|
const transformed = transform(code, "statements");
|
|
3094
|
-
getOrCompile("exec:" + transformed, transformed)
|
|
2813
|
+
run(getOrCompile("exec:" + transformed, transformed), ...namespaces);
|
|
3095
2814
|
}
|
|
3096
2815
|
//#endregion
|
|
3097
2816
|
//#region src/registry.ts
|
|
3098
|
-
var registry =
|
|
2817
|
+
var registry = new NameMap();
|
|
3099
2818
|
function registerMacro(name, component) {
|
|
3100
|
-
registry.set(name
|
|
2819
|
+
registry.set(name, component);
|
|
3101
2820
|
}
|
|
3102
2821
|
function getMacro(name) {
|
|
3103
|
-
return registry.get(name
|
|
2822
|
+
return registry.get(name);
|
|
3104
2823
|
}
|
|
3105
|
-
var textRegistry =
|
|
2824
|
+
var textRegistry = new NameMap();
|
|
3106
2825
|
/** Register (or, with undefined, remove) the text form of a macro. */
|
|
3107
2826
|
function registerMacroText(name, text) {
|
|
3108
|
-
if (text) textRegistry.set(name
|
|
3109
|
-
else textRegistry.delete(name
|
|
2827
|
+
if (text) textRegistry.set(name, text);
|
|
2828
|
+
else textRegistry.delete(name);
|
|
3110
2829
|
}
|
|
3111
2830
|
function getMacroText(name) {
|
|
3112
|
-
return textRegistry.get(name
|
|
2831
|
+
return textRegistry.get(name);
|
|
3113
2832
|
}
|
|
3114
|
-
var subMacros =
|
|
2833
|
+
var subMacros = new NameSet();
|
|
3115
2834
|
function registerSubMacro(name) {
|
|
3116
|
-
subMacros.add(name
|
|
2835
|
+
subMacros.add(name);
|
|
3117
2836
|
}
|
|
3118
2837
|
function isSubMacro(name) {
|
|
3119
|
-
return subMacros.has(name
|
|
2838
|
+
return subMacros.has(name);
|
|
3120
2839
|
}
|
|
3121
|
-
var metadataRegistry =
|
|
2840
|
+
var metadataRegistry = new NameMap();
|
|
3122
2841
|
function registerMacroMetadata(name, metadata) {
|
|
3123
|
-
metadataRegistry.set(name
|
|
2842
|
+
metadataRegistry.set(name, metadata);
|
|
3124
2843
|
}
|
|
3125
2844
|
function getMacroRegistry() {
|
|
3126
2845
|
return Array.from(metadataRegistry.values());
|
|
3127
2846
|
}
|
|
3128
2847
|
//#endregion
|
|
3129
2848
|
//#region src/widgets/widget-registry.ts
|
|
3130
|
-
var widgets =
|
|
2849
|
+
var widgets = new NameMap();
|
|
3131
2850
|
/**
|
|
3132
2851
|
* Register a widget. Its `@` parameters become locals of its body, so one
|
|
3133
2852
|
* that no namespace can hold (`@__proto__`) throws a TypeError and the
|
|
@@ -3136,14 +2855,14 @@ var widgets = /* @__PURE__ */ new Map();
|
|
|
3136
2855
|
function registerWidget(name, bodyAST, params, isBlock = false) {
|
|
3137
2856
|
const filteredParams = params.filter((p) => p !== "@children");
|
|
3138
2857
|
for (const param of filteredParams) if (param.startsWith("@")) checkVariableName(param.slice(1), param);
|
|
3139
|
-
widgets.set(name
|
|
2858
|
+
widgets.set(name, {
|
|
3140
2859
|
body: bodyAST,
|
|
3141
2860
|
params: filteredParams,
|
|
3142
2861
|
isBlock
|
|
3143
2862
|
});
|
|
3144
2863
|
}
|
|
3145
2864
|
function getWidget(name) {
|
|
3146
|
-
return widgets.get(name
|
|
2865
|
+
return widgets.get(name);
|
|
3147
2866
|
}
|
|
3148
2867
|
//#endregion
|
|
3149
2868
|
//#region src/components/macros/arg-utils.ts
|
|
@@ -3310,6 +3029,23 @@ function splitArgs(raw) {
|
|
|
3310
3029
|
return args;
|
|
3311
3030
|
}
|
|
3312
3031
|
//#endregion
|
|
3032
|
+
//#region src/components/macros/display.ts
|
|
3033
|
+
/** The text a value is shown as: nothing for null and undefined. */
|
|
3034
|
+
function display(value) {
|
|
3035
|
+
return value == null ? "" : String(value);
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* `content` in a `<span>` carrying `className` and `id`, or in a fragment if
|
|
3039
|
+
* it has neither.
|
|
3040
|
+
*/
|
|
3041
|
+
function wrapContent(className, id, content) {
|
|
3042
|
+
if (className || id) return h("span", {
|
|
3043
|
+
id,
|
|
3044
|
+
class: className
|
|
3045
|
+
}, content);
|
|
3046
|
+
return h(Fragment, null, content);
|
|
3047
|
+
}
|
|
3048
|
+
//#endregion
|
|
3313
3049
|
//#region src/interpolation.ts
|
|
3314
3050
|
/**
|
|
3315
3051
|
* Text-only markup: HTML attribute values, image alt text and link titles,
|
|
@@ -3378,9 +3114,6 @@ function mapTextNodes(nodes, fn) {
|
|
|
3378
3114
|
}
|
|
3379
3115
|
/** Deep enough for any real nesting; stops a widget that includes itself. */
|
|
3380
3116
|
var MAX_DEPTH = 100;
|
|
3381
|
-
function display(value) {
|
|
3382
|
-
return value == null ? "" : String(value);
|
|
3383
|
-
}
|
|
3384
3117
|
/**
|
|
3385
3118
|
* The value of a variable reference (`name` without its sigil, with an
|
|
3386
3119
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -3398,6 +3131,10 @@ function resolveVariable(scope, kind, name) {
|
|
|
3398
3131
|
}
|
|
3399
3132
|
return value;
|
|
3400
3133
|
}
|
|
3134
|
+
/** Evaluate an expression in `scope`. */
|
|
3135
|
+
function evaluateIn(expr, scope) {
|
|
3136
|
+
return evaluate(expr, scope.variables, scope.temporary, scope.locals, scope.transient);
|
|
3137
|
+
}
|
|
3401
3138
|
var TextEvaluator = class {
|
|
3402
3139
|
constructor() {
|
|
3403
3140
|
this.errors = [];
|
|
@@ -3407,9 +3144,6 @@ var TextEvaluator = class {
|
|
|
3407
3144
|
for (const node of nodes) text += this.node(node, scope, depth);
|
|
3408
3145
|
return text;
|
|
3409
3146
|
}
|
|
3410
|
-
evaluate(expr, scope) {
|
|
3411
|
-
return evaluate(expr, scope.variables, scope.temporary, scope.locals, scope.transient);
|
|
3412
|
-
}
|
|
3413
3147
|
node(node, scope, depth) {
|
|
3414
3148
|
switch (node.type) {
|
|
3415
3149
|
case "text": return node.value;
|
|
@@ -3421,7 +3155,7 @@ var TextEvaluator = class {
|
|
|
3421
3155
|
}
|
|
3422
3156
|
return this.variable(node.scope, node.name, scope);
|
|
3423
3157
|
case "expression": try {
|
|
3424
|
-
return display(
|
|
3158
|
+
return display(evaluateIn(node.expression, scope));
|
|
3425
3159
|
} catch (error) {
|
|
3426
3160
|
this.errors.push({
|
|
3427
3161
|
macro: "expression",
|
|
@@ -3471,7 +3205,7 @@ var TextEvaluator = class {
|
|
|
3471
3205
|
let value;
|
|
3472
3206
|
const expr = argExprs[i];
|
|
3473
3207
|
if (expr !== void 0) try {
|
|
3474
|
-
value =
|
|
3208
|
+
value = evaluateIn(expr, scope);
|
|
3475
3209
|
} catch {
|
|
3476
3210
|
value = void 0;
|
|
3477
3211
|
}
|
|
@@ -3493,7 +3227,7 @@ var TextEvaluator = class {
|
|
|
3493
3227
|
return "";
|
|
3494
3228
|
}
|
|
3495
3229
|
const ctx = {
|
|
3496
|
-
evaluate: (expr) =>
|
|
3230
|
+
evaluate: (expr) => evaluateIn(expr, scope),
|
|
3497
3231
|
renderText: (nodes, locals) => this.nested(node.name, nodes, scope, depth, locals ? { locals: withLocals(scope.locals, locals) } : {})
|
|
3498
3232
|
};
|
|
3499
3233
|
try {
|
|
@@ -3570,7 +3304,7 @@ function interpolateCode(template, scope, decode) {
|
|
|
3570
3304
|
});
|
|
3571
3305
|
}
|
|
3572
3306
|
} else try {
|
|
3573
|
-
text += display(
|
|
3307
|
+
text += display(evaluateIn(part.expr, scope));
|
|
3574
3308
|
} catch (error) {
|
|
3575
3309
|
errors.push({
|
|
3576
3310
|
macro: "expression",
|
|
@@ -3636,29 +3370,26 @@ function useInterpolate() {
|
|
|
3636
3370
|
}
|
|
3637
3371
|
//#endregion
|
|
3638
3372
|
//#region src/components/macros/VarDisplay.tsx
|
|
3639
|
-
var SIGILS = {
|
|
3640
|
-
variable: "$",
|
|
3641
|
-
temporary: "_",
|
|
3642
|
-
local: "@",
|
|
3643
|
-
transient: "%"
|
|
3644
|
-
};
|
|
3645
3373
|
var STORE_NAMESPACES = {
|
|
3646
3374
|
variable: "variables",
|
|
3647
3375
|
temporary: "temporary",
|
|
3648
3376
|
transient: "transient"
|
|
3649
3377
|
};
|
|
3650
|
-
|
|
3378
|
+
/** A `{$name}` (`{_name}`, `{@name}`, `{%name}`) variable display. */
|
|
3379
|
+
function VarDisplay({ node }) {
|
|
3380
|
+
const { name, scope } = node;
|
|
3651
3381
|
const resolve = useInterpolate();
|
|
3652
|
-
className = resolve(className);
|
|
3653
|
-
id = resolve(id);
|
|
3382
|
+
const className = resolve(node.className);
|
|
3383
|
+
const id = resolve(node.id);
|
|
3654
3384
|
const localsValues = useContext(LocalsValuesContext);
|
|
3655
3385
|
const parts = name.split(".");
|
|
3656
3386
|
const root = parts[0];
|
|
3657
3387
|
const storeValue = useStoryStore((s) => scope === "local" ? void 0 : ownValue(s[STORE_NAMESPACES[scope]], root));
|
|
3658
|
-
const
|
|
3388
|
+
const sigil = SCOPE_SIGILS[scope];
|
|
3389
|
+
const nameError = variableNameError(root, sigil + root);
|
|
3659
3390
|
if (nameError) return /* @__PURE__ */ jsx("span", {
|
|
3660
3391
|
class: "error",
|
|
3661
|
-
children: `{${
|
|
3392
|
+
children: `{${sigil}${name} error: ${nameError}}`
|
|
3662
3393
|
});
|
|
3663
3394
|
let value;
|
|
3664
3395
|
if (scope === "local") value = ownValue(localsValues, root);
|
|
@@ -3671,34 +3402,24 @@ function VarDisplay({ name, scope, className, id }) {
|
|
|
3671
3402
|
const part = parts[i];
|
|
3672
3403
|
value = value[part];
|
|
3673
3404
|
}
|
|
3674
|
-
|
|
3675
|
-
if (className || id) return /* @__PURE__ */ jsx("span", {
|
|
3676
|
-
id,
|
|
3677
|
-
class: className,
|
|
3678
|
-
children: display
|
|
3679
|
-
});
|
|
3680
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3405
|
+
return wrapContent(className, id, display(value));
|
|
3681
3406
|
}
|
|
3682
3407
|
//#endregion
|
|
3683
3408
|
//#region src/components/macros/ExprDisplay.tsx
|
|
3684
|
-
|
|
3409
|
+
/** An expression display: `{$hp + 1}`. */
|
|
3410
|
+
function ExprDisplay({ node }) {
|
|
3411
|
+
const { expression } = node;
|
|
3685
3412
|
const resolve = useInterpolate();
|
|
3686
|
-
className = resolve(className);
|
|
3687
|
-
id = resolve(id);
|
|
3413
|
+
const className = resolve(node.className);
|
|
3414
|
+
const id = resolve(node.id);
|
|
3688
3415
|
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
3689
|
-
let
|
|
3416
|
+
let text;
|
|
3690
3417
|
try {
|
|
3691
|
-
|
|
3692
|
-
display = value == null ? "" : String(value);
|
|
3418
|
+
text = display(evaluate(expression, variables, temporary, localsValues, transient));
|
|
3693
3419
|
} catch {
|
|
3694
|
-
|
|
3420
|
+
text = `{error: ${expression}}`;
|
|
3695
3421
|
}
|
|
3696
|
-
|
|
3697
|
-
id,
|
|
3698
|
-
class: className,
|
|
3699
|
-
children: display
|
|
3700
|
-
});
|
|
3701
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3422
|
+
return wrapContent(className, id, text);
|
|
3702
3423
|
}
|
|
3703
3424
|
//#endregion
|
|
3704
3425
|
//#region src/hooks/use-render-options.ts
|
|
@@ -4433,17 +4154,9 @@ function HtmlNodeRenderer({ node }) {
|
|
|
4433
4154
|
}
|
|
4434
4155
|
function ChildrenSlot() {
|
|
4435
4156
|
const childrenAST = useContext(WidgetChildrenContext);
|
|
4436
|
-
const
|
|
4437
|
-
const inline = useContext(InlineContext);
|
|
4438
|
-
const raw = useContext(RawTextContext);
|
|
4439
|
-
const locals = useContext(LocalsValuesContext);
|
|
4157
|
+
const renderOptions = useRenderOptions();
|
|
4440
4158
|
if (!childrenAST || childrenAST.length === 0) return null;
|
|
4441
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: renderNodes(childrenAST,
|
|
4442
|
-
nobr,
|
|
4443
|
-
locals,
|
|
4444
|
-
inline,
|
|
4445
|
-
raw
|
|
4446
|
-
}) });
|
|
4159
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: renderNodes(childrenAST, renderOptions) });
|
|
4447
4160
|
}
|
|
4448
4161
|
/**
|
|
4449
4162
|
* Stable per-node keys. Keying rendered children by AST node identity (not
|
|
@@ -4493,17 +4206,8 @@ function renderSingleNode(node) {
|
|
|
4493
4206
|
switch (node.type) {
|
|
4494
4207
|
case "variable":
|
|
4495
4208
|
if (node.scope === "local" && node.name === "children") return /* @__PURE__ */ jsx(ChildrenSlot, {}, key);
|
|
4496
|
-
return /* @__PURE__ */ jsx(VarDisplay, {
|
|
4497
|
-
|
|
4498
|
-
scope: node.scope,
|
|
4499
|
-
className: node.className,
|
|
4500
|
-
id: node.id
|
|
4501
|
-
}, key);
|
|
4502
|
-
case "expression": return /* @__PURE__ */ jsx(ExprDisplay, {
|
|
4503
|
-
expression: node.expression,
|
|
4504
|
-
className: node.className,
|
|
4505
|
-
id: node.id
|
|
4506
|
-
}, key);
|
|
4209
|
+
return /* @__PURE__ */ jsx(VarDisplay, { node }, key);
|
|
4210
|
+
case "expression": return /* @__PURE__ */ jsx(ExprDisplay, { node }, key);
|
|
4507
4211
|
case "macro": return renderMacro(node, key);
|
|
4508
4212
|
case "html": return /* @__PURE__ */ jsx(HtmlNodeRenderer, { node }, key);
|
|
4509
4213
|
default: return node;
|
|
@@ -4784,6 +4488,24 @@ function createLocalStorageBackend() {
|
|
|
4784
4488
|
}
|
|
4785
4489
|
var IDB_DB_NAME = "spindle";
|
|
4786
4490
|
var IDB_DB_VERSION = 1;
|
|
4491
|
+
/** Each object store's key path, and the key path of each of its indexes. */
|
|
4492
|
+
var IDB_STORES = {
|
|
4493
|
+
saves: {
|
|
4494
|
+
keyPath: "meta.id",
|
|
4495
|
+
indexes: {
|
|
4496
|
+
ifid: "meta.ifid",
|
|
4497
|
+
playthroughId: "meta.playthroughId"
|
|
4498
|
+
}
|
|
4499
|
+
},
|
|
4500
|
+
playthroughs: {
|
|
4501
|
+
keyPath: "id",
|
|
4502
|
+
indexes: { ifid: "ifid" }
|
|
4503
|
+
},
|
|
4504
|
+
meta: {
|
|
4505
|
+
keyPath: "key",
|
|
4506
|
+
indexes: {}
|
|
4507
|
+
}
|
|
4508
|
+
};
|
|
4787
4509
|
function createIDBBackend() {
|
|
4788
4510
|
let dbPromise = null;
|
|
4789
4511
|
function openDB() {
|
|
@@ -4792,13 +4514,11 @@ function createIDBBackend() {
|
|
|
4792
4514
|
const req = indexedDB.open(IDB_DB_NAME, IDB_DB_VERSION);
|
|
4793
4515
|
req.onupgradeneeded = () => {
|
|
4794
4516
|
const db = req.result;
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4517
|
+
for (const [name, { keyPath, indexes }] of Object.entries(IDB_STORES)) {
|
|
4518
|
+
if (db.objectStoreNames.contains(name)) continue;
|
|
4519
|
+
const store = db.createObjectStore(name, { keyPath });
|
|
4520
|
+
for (const [index, path] of Object.entries(indexes)) store.createIndex(index, path, { unique: false });
|
|
4799
4521
|
}
|
|
4800
|
-
if (!db.objectStoreNames.contains("playthroughs")) db.createObjectStore("playthroughs", { keyPath: "id" }).createIndex("ifid", "ifid", { unique: false });
|
|
4801
|
-
if (!db.objectStoreNames.contains("meta")) db.createObjectStore("meta", { keyPath: "key" });
|
|
4802
4522
|
};
|
|
4803
4523
|
req.onsuccess = () => resolve(req.result);
|
|
4804
4524
|
req.onerror = () => reject(req.error);
|
|
@@ -4875,31 +4595,363 @@ async function detectBackend() {
|
|
|
4875
4595
|
return createMemoryBackend();
|
|
4876
4596
|
}
|
|
4877
4597
|
/**
|
|
4878
|
-
* Detection in flight. Concurrent first callers share it so they all get the
|
|
4879
|
-
* same backend (the memory fallback makes a new, empty store per detection).
|
|
4598
|
+
* Detection in flight. Concurrent first callers share it so they all get the
|
|
4599
|
+
* same backend (the memory fallback makes a new, empty store per detection).
|
|
4600
|
+
*/
|
|
4601
|
+
var _backendPromise = null;
|
|
4602
|
+
function getBackend() {
|
|
4603
|
+
if (_backend) return Promise.resolve(_backend);
|
|
4604
|
+
if (!_backendPromise) {
|
|
4605
|
+
const pending = detectBackend().then((backend) => {
|
|
4606
|
+
if (_backendPromise === pending) _backend = backend;
|
|
4607
|
+
return backend;
|
|
4608
|
+
});
|
|
4609
|
+
_backendPromise = pending;
|
|
4610
|
+
}
|
|
4611
|
+
return _backendPromise;
|
|
4612
|
+
}
|
|
4613
|
+
function resetBackend() {
|
|
4614
|
+
_backend = null;
|
|
4615
|
+
_backendPromise = null;
|
|
4616
|
+
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Synchronous accessor for the current backend type.
|
|
4619
|
+
* Returns 'memory' if the backend hasn't been detected yet.
|
|
4620
|
+
*/
|
|
4621
|
+
function getBackendType() {
|
|
4622
|
+
return _backend?.type ?? "memory";
|
|
4623
|
+
}
|
|
4624
|
+
//#endregion
|
|
4625
|
+
//#region src/utils/object-path.ts
|
|
4626
|
+
/**
|
|
4627
|
+
* Traverse dot-path segments on an object and return the nested value.
|
|
4628
|
+
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
4629
|
+
* are not story state: unless an object holds one as its own property, it
|
|
4630
|
+
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
4631
|
+
* (class getters, `size` of a Map) are read.
|
|
4632
|
+
*/
|
|
4633
|
+
function getByPath(obj, segments) {
|
|
4634
|
+
let current = obj;
|
|
4635
|
+
for (const seg of segments) {
|
|
4636
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
4637
|
+
if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
|
|
4638
|
+
current = current[seg];
|
|
4639
|
+
}
|
|
4640
|
+
return current;
|
|
4641
|
+
}
|
|
4642
|
+
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
4643
|
+
var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
4644
|
+
/**
|
|
4645
|
+
* Built-ins whose content is not their properties: a property written into
|
|
4646
|
+
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
4647
|
+
*/
|
|
4648
|
+
var builtinName = (value) => value instanceof Map ? "a Map" : value instanceof Set ? "a Set" : value instanceof Date ? "a Date" : value instanceof RegExp ? "a RegExp" : void 0;
|
|
4649
|
+
/**
|
|
4650
|
+
* Shallow copy that keeps the prototype, so a registered class instance
|
|
4651
|
+
* stays an instance of its class (with the same own keys deepClone copies).
|
|
4652
|
+
*/
|
|
4653
|
+
function shallowCopy(value) {
|
|
4654
|
+
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
4655
|
+
for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
|
|
4656
|
+
value: value[key],
|
|
4657
|
+
enumerable: true,
|
|
4658
|
+
writable: true,
|
|
4659
|
+
configurable: true
|
|
4660
|
+
});
|
|
4661
|
+
return copy;
|
|
4662
|
+
}
|
|
4663
|
+
/**
|
|
4664
|
+
* Set a value at dot-path `segments` below `root`.
|
|
4665
|
+
*
|
|
4666
|
+
* When `root` is an Immer draft, objects Immer does not draft (registered
|
|
4667
|
+
* class instances and everything below them) are copied on the way down and
|
|
4668
|
+
* the copy is assigned to its parent, so the write produces new references
|
|
4669
|
+
* up to the root. Writing into such an object directly would mutate the
|
|
4670
|
+
* instance shared with the previous state and with history, and subscribers
|
|
4671
|
+
* would see no change. Outside a draft (a private working copy) the path is
|
|
4672
|
+
* written in place.
|
|
4673
|
+
*
|
|
4674
|
+
* The path goes through own properties of plain objects, class instances
|
|
4675
|
+
* and arrays (by index); anything else throws a TypeError rather than
|
|
4676
|
+
* writing where no clone or save would see it: an inherited property such
|
|
4677
|
+
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
4678
|
+
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
4679
|
+
*/
|
|
4680
|
+
function setByPath(root, segments, value, options = {}) {
|
|
4681
|
+
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
4682
|
+
parent[segments[segments.length - 1]] = value;
|
|
4683
|
+
}
|
|
4684
|
+
/**
|
|
4685
|
+
* Delete the property at dot-path `segments` below `root`, copying
|
|
4686
|
+
* undrafted objects on the way down like setByPath(). Does nothing when an
|
|
4687
|
+
* intermediate is missing or not an object, or the property is absent (or
|
|
4688
|
+
* only inherited).
|
|
4689
|
+
*/
|
|
4690
|
+
function deleteByPath(root, segments) {
|
|
4691
|
+
const last = segments[segments.length - 1];
|
|
4692
|
+
checkSegments(segments);
|
|
4693
|
+
let holder = root;
|
|
4694
|
+
for (const seg of segments.slice(0, -1)) {
|
|
4695
|
+
if (holder == null || typeof holder !== "object" || !hasOwn(holder, seg)) return;
|
|
4696
|
+
holder = holder[seg];
|
|
4697
|
+
}
|
|
4698
|
+
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
4699
|
+
const parent = walkToParent(root, segments, false);
|
|
4700
|
+
delete parent[last];
|
|
4701
|
+
}
|
|
4702
|
+
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
4703
|
+
function checkSegments(segments) {
|
|
4704
|
+
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
4705
|
+
}
|
|
4706
|
+
/** Throw unless `holder` can take `key` as story state (see setByPath). */
|
|
4707
|
+
function checkHolder(holder, key, segments, depth) {
|
|
4708
|
+
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
4709
|
+
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
4710
|
+
}
|
|
4711
|
+
/**
|
|
4712
|
+
* Walk to the object holding the last of `segments`, copying undrafted
|
|
4713
|
+
* objects when `root` is an Immer draft (see setByPath). A missing or
|
|
4714
|
+
* non-object intermediate is replaced by a plain object when
|
|
4715
|
+
* `createMissing` is set, and throws a TypeError otherwise.
|
|
4716
|
+
*/
|
|
4717
|
+
function walkToParent(root, segments, createMissing) {
|
|
4718
|
+
checkSegments(segments);
|
|
4719
|
+
const copyOnWrite = isDraft(root);
|
|
4720
|
+
let current = root;
|
|
4721
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
4722
|
+
const seg = segments[i];
|
|
4723
|
+
checkHolder(current, seg, segments, i);
|
|
4724
|
+
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
4725
|
+
if (next == null || typeof next !== "object") {
|
|
4726
|
+
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
4727
|
+
next = {};
|
|
4728
|
+
current[seg] = next;
|
|
4729
|
+
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
4730
|
+
next = shallowCopy(next);
|
|
4731
|
+
current[seg] = next;
|
|
4732
|
+
}
|
|
4733
|
+
current = next;
|
|
4734
|
+
}
|
|
4735
|
+
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
4736
|
+
return current;
|
|
4737
|
+
}
|
|
4738
|
+
//#endregion
|
|
4739
|
+
//#region src/structural.ts
|
|
4740
|
+
function isPlainObject(value) {
|
|
4741
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4742
|
+
const proto = Object.getPrototypeOf(value);
|
|
4743
|
+
return proto === Object.prototype || proto === null;
|
|
4744
|
+
}
|
|
4745
|
+
function deepClone(value, options = {}) {
|
|
4746
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4747
|
+
function clone(val) {
|
|
4748
|
+
if (val === null || typeof val !== "object") return val;
|
|
4749
|
+
const obj = val;
|
|
4750
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
4751
|
+
if (val instanceof Date) return new Date(val.getTime());
|
|
4752
|
+
if (val instanceof RegExp) return new RegExp(val.source, val.flags);
|
|
4753
|
+
if (Array.isArray(val)) {
|
|
4754
|
+
const arr = [];
|
|
4755
|
+
seen.set(obj, arr);
|
|
4756
|
+
for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
|
|
4757
|
+
return arr;
|
|
4758
|
+
}
|
|
4759
|
+
if (val instanceof Map) {
|
|
4760
|
+
const copy = /* @__PURE__ */ new Map();
|
|
4761
|
+
seen.set(obj, copy);
|
|
4762
|
+
for (const [k, v] of val) copy.set(clone(k), clone(v));
|
|
4763
|
+
return copy;
|
|
4764
|
+
}
|
|
4765
|
+
if (val instanceof Set) {
|
|
4766
|
+
const copy = /* @__PURE__ */ new Set();
|
|
4767
|
+
seen.set(obj, copy);
|
|
4768
|
+
for (const v of val) copy.add(clone(v));
|
|
4769
|
+
return copy;
|
|
4770
|
+
}
|
|
4771
|
+
const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
|
|
4772
|
+
if (!keepsProto && options.keepUnregistered) return val;
|
|
4773
|
+
const copy = keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {};
|
|
4774
|
+
seen.set(obj, copy);
|
|
4775
|
+
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
4776
|
+
return copy;
|
|
4777
|
+
}
|
|
4778
|
+
return clone(value);
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4781
|
+
* Structural equality over the value types deepClone() supports: primitives,
|
|
4782
|
+
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
4783
|
+
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
4784
|
+
* are compared by length and index, so a hole equals an undefined element
|
|
4785
|
+
* (deepClone() and save/load turn holes into undefined elements).
|
|
4786
|
+
*/
|
|
4787
|
+
function deepEqual(a, b) {
|
|
4788
|
+
return equal(a, b, /* @__PURE__ */ new Map());
|
|
4789
|
+
}
|
|
4790
|
+
/**
|
|
4791
|
+
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
4792
|
+
* cycle) assumes it equal. Every object may pair with several others, since
|
|
4793
|
+
* cycles of different lengths can still unfold to the same value.
|
|
4794
|
+
*/
|
|
4795
|
+
function equal(a, b, assumed) {
|
|
4796
|
+
if (Object.is(a, b)) return true;
|
|
4797
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
4798
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
4799
|
+
const pairs = assumed.get(a);
|
|
4800
|
+
if (pairs?.has(b)) return true;
|
|
4801
|
+
if (pairs) pairs.add(b);
|
|
4802
|
+
else assumed.set(a, new Set([b]));
|
|
4803
|
+
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
4804
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
4805
|
+
if (a instanceof Map || a instanceof Set) {
|
|
4806
|
+
const bc = b;
|
|
4807
|
+
if (a.size !== bc.size) return false;
|
|
4808
|
+
const ai = a.entries();
|
|
4809
|
+
const bi = bc.entries();
|
|
4810
|
+
for (let x = ai.next(), y = bi.next(); !x.done; x = ai.next(), y = bi.next()) if (!equal(x.value, y.value, assumed)) return false;
|
|
4811
|
+
return true;
|
|
4812
|
+
}
|
|
4813
|
+
if (Array.isArray(a)) {
|
|
4814
|
+
const bc = b;
|
|
4815
|
+
if (a.length !== bc.length) return false;
|
|
4816
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
4817
|
+
return true;
|
|
4818
|
+
}
|
|
4819
|
+
const ao = a;
|
|
4820
|
+
const bo = b;
|
|
4821
|
+
const keys = Object.keys(ao);
|
|
4822
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
4823
|
+
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
4824
|
+
return true;
|
|
4825
|
+
}
|
|
4826
|
+
/**
|
|
4827
|
+
* Objects merged property by property: plain objects and (registered)
|
|
4828
|
+
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
4829
|
+
* replaced as a whole, since their elements have no stable identity to
|
|
4830
|
+
* merge by (a shift moves every index).
|
|
4831
|
+
*/
|
|
4832
|
+
function isMergeable(value) {
|
|
4833
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
4837
|
+
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
4880
4838
|
*/
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4839
|
+
function mergesWith(a, b, arrays) {
|
|
4840
|
+
if (arrays && (Array.isArray(a) || Array.isArray(b))) return Array.isArray(a) && Array.isArray(b);
|
|
4841
|
+
return isMergeable(a) && isMergeable(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
4842
|
+
}
|
|
4843
|
+
/**
|
|
4844
|
+
* The keys at which `after` differs from `before`, two values that merge
|
|
4845
|
+
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
4846
|
+
* updates are immutable, so a value that kept its identity is no change,
|
|
4847
|
+
* and one rebuilt with equal content (mutation code commits whole values)
|
|
4848
|
+
* is none either. Values that merge are reported as 'nested', to be
|
|
4849
|
+
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
4850
|
+
* merge too: elements at the indices both hold are compared, and the
|
|
4851
|
+
* elements one has beyond the other's length are added or deleted.
|
|
4852
|
+
*/
|
|
4853
|
+
function keyChanges(before, after, arrays) {
|
|
4854
|
+
const changes = [];
|
|
4855
|
+
const compare = (key, b, a) => {
|
|
4856
|
+
if (Object.is(b, a)) return;
|
|
4857
|
+
if (mergesWith(b, a, arrays)) changes.push({
|
|
4858
|
+
key,
|
|
4859
|
+
kind: "nested",
|
|
4860
|
+
before: b,
|
|
4861
|
+
after: a
|
|
4888
4862
|
});
|
|
4889
|
-
|
|
4863
|
+
else if (!deepEqual(b, a)) changes.push({
|
|
4864
|
+
key,
|
|
4865
|
+
kind: "changed",
|
|
4866
|
+
after: a
|
|
4867
|
+
});
|
|
4868
|
+
};
|
|
4869
|
+
if (Array.isArray(after)) {
|
|
4870
|
+
const b = before;
|
|
4871
|
+
const a = after;
|
|
4872
|
+
for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
|
|
4873
|
+
for (let i = b.length; i < a.length; i++) changes.push({
|
|
4874
|
+
key: String(i),
|
|
4875
|
+
kind: "added",
|
|
4876
|
+
after: a[i]
|
|
4877
|
+
});
|
|
4878
|
+
for (let i = a.length; i < b.length; i++) changes.push({
|
|
4879
|
+
key: String(i),
|
|
4880
|
+
kind: "deleted"
|
|
4881
|
+
});
|
|
4882
|
+
return changes;
|
|
4890
4883
|
}
|
|
4891
|
-
|
|
4884
|
+
for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
4885
|
+
else changes.push({
|
|
4886
|
+
key,
|
|
4887
|
+
kind: "added",
|
|
4888
|
+
after: after[key]
|
|
4889
|
+
});
|
|
4890
|
+
for (const key of Object.keys(before)) if (!hasOwn(after, key)) changes.push({
|
|
4891
|
+
key,
|
|
4892
|
+
kind: "deleted"
|
|
4893
|
+
});
|
|
4894
|
+
return changes;
|
|
4892
4895
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
+
/**
|
|
4897
|
+
* The property paths where `after` differs from `before`, two objects of
|
|
4898
|
+
* one class: objects that merge (see isMergeable) are compared property by
|
|
4899
|
+
* property, any other value as a whole.
|
|
4900
|
+
*/
|
|
4901
|
+
function diffPaths(before, after) {
|
|
4902
|
+
const changes = [];
|
|
4903
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
4904
|
+
(function walk(b, a, path) {
|
|
4905
|
+
if (ancestors.has(a)) return;
|
|
4906
|
+
ancestors.add(a);
|
|
4907
|
+
for (const change of keyChanges(b, a, false)) {
|
|
4908
|
+
const at = [...path, change.key];
|
|
4909
|
+
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
4910
|
+
else changes.push(change.kind === "deleted" ? {
|
|
4911
|
+
path: at,
|
|
4912
|
+
deleted: true
|
|
4913
|
+
} : {
|
|
4914
|
+
path: at,
|
|
4915
|
+
deleted: false,
|
|
4916
|
+
value: change.after
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
ancestors.delete(a);
|
|
4920
|
+
})(before, after, []);
|
|
4921
|
+
return changes;
|
|
4922
|
+
}
|
|
4923
|
+
/** Whether `target` already holds what `change` would write. */
|
|
4924
|
+
function isApplied(target, change) {
|
|
4925
|
+
const parent = getByPath(target, change.path.slice(0, -1));
|
|
4926
|
+
if (parent === null || typeof parent !== "object") return change.deleted;
|
|
4927
|
+
const key = change.path[change.path.length - 1];
|
|
4928
|
+
if (!hasOwn(parent, key)) return change.deleted;
|
|
4929
|
+
return !change.deleted && deepEqual(parent[key], change.value);
|
|
4930
|
+
}
|
|
4931
|
+
function applyChange(target, change) {
|
|
4932
|
+
if (change.deleted) deleteByPath(target, change.path);
|
|
4933
|
+
else setByPath(target, change.path, change.value);
|
|
4896
4934
|
}
|
|
4897
4935
|
/**
|
|
4898
|
-
*
|
|
4899
|
-
*
|
|
4936
|
+
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
4937
|
+
* index) into `target`, another version of the same object or array.
|
|
4938
|
+
* Values that changed are written whole, as deep copies; for values that
|
|
4939
|
+
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
4940
|
+
* whether it did. Into an array, elements removed from the end are removed
|
|
4941
|
+
* at the same indices and elements added are appended, and changes at
|
|
4942
|
+
* indices `target` lacks are dropped: where `target` was resized, its
|
|
4943
|
+
* indices do not line up with `before`'s.
|
|
4900
4944
|
*/
|
|
4901
|
-
function
|
|
4902
|
-
|
|
4945
|
+
function mergeKeys(target, before, after, mergeNested) {
|
|
4946
|
+
const list = Array.isArray(target) ? target : void 0;
|
|
4947
|
+
for (const change of keyChanges(before, after, true)) {
|
|
4948
|
+
const { key } = change;
|
|
4949
|
+
if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
|
|
4950
|
+
else delete target[key];
|
|
4951
|
+
else if (list && change.kind === "added") list.push(deepClone(change.after));
|
|
4952
|
+
else if (list && Number(key) >= list.length) continue;
|
|
4953
|
+
else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
|
|
4954
|
+
}
|
|
4903
4955
|
}
|
|
4904
4956
|
//#endregion
|
|
4905
4957
|
//#region src/event-emitter.ts
|
|
@@ -5002,6 +5054,10 @@ function inOrder(op) {
|
|
|
5002
5054
|
function queued(op) {
|
|
5003
5055
|
return (...args) => inOrder(() => op(...args));
|
|
5004
5056
|
}
|
|
5057
|
+
/** An operation passing the save `saveId`, if it is stored, to `read`. */
|
|
5058
|
+
var readingSave = (read) => queued(async (saveId) => read(await (await getBackend()).getSave(saveId)));
|
|
5059
|
+
/** An operation passing the save a slot holds, if any, to `read`. */
|
|
5060
|
+
var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(ifid, slot)));
|
|
5005
5061
|
/**
|
|
5006
5062
|
* Store a new playthrough and make it the story's current one. Pass `id` when
|
|
5007
5063
|
* the caller has already switched to the playthrough (restart() does, so
|
|
@@ -5202,7 +5258,7 @@ function deserializePayload(payload) {
|
|
|
5202
5258
|
}
|
|
5203
5259
|
/** The live payload of a stored save, if there is one. */
|
|
5204
5260
|
var livePayload = (record) => record && deserializePayload(record.payload);
|
|
5205
|
-
|
|
5261
|
+
readingSave(livePayload);
|
|
5206
5262
|
/**
|
|
5207
5263
|
* Delete a save record. If the default slot or a named slot holds it, that
|
|
5208
5264
|
* slot is cleared as well (pointer and slot index), as `deleteSlotSave` would.
|
|
@@ -5358,7 +5414,7 @@ queued(hasQuickSaveNow);
|
|
|
5358
5414
|
async function hasQuickSaveNow(ifid, slot) {
|
|
5359
5415
|
return await slotRecord(ifid, slot) !== void 0;
|
|
5360
5416
|
}
|
|
5361
|
-
|
|
5417
|
+
readingSlot(livePayload);
|
|
5362
5418
|
/**
|
|
5363
5419
|
* Read the save held in a slot (the default slot when `slot` is omitted) for
|
|
5364
5420
|
* the running game to load, and make its playthrough the story's current one
|
|
@@ -5454,7 +5510,7 @@ function toExport(record) {
|
|
|
5454
5510
|
save: record
|
|
5455
5511
|
};
|
|
5456
5512
|
}
|
|
5457
|
-
var exportSave =
|
|
5513
|
+
var exportSave = readingSave(toExport);
|
|
5458
5514
|
/**
|
|
5459
5515
|
* Validate an export against the running story and build the record to store:
|
|
5460
5516
|
* a fresh save ID, and the playthrough record created if it doesn't exist yet.
|
|
@@ -5482,7 +5538,7 @@ function importSave(data, ifid) {
|
|
|
5482
5538
|
* Export the save held in a slot (default autosave slot when `slot` is omitted).
|
|
5483
5539
|
* Returns undefined if the slot is empty.
|
|
5484
5540
|
*/
|
|
5485
|
-
var exportSlotSave =
|
|
5541
|
+
var exportSlotSave = readingSlot(toExport);
|
|
5486
5542
|
/**
|
|
5487
5543
|
* Import an exported save into a slot (default autosave slot when `slot` is
|
|
5488
5544
|
* omitted), replacing any save already in that slot. Applies the same
|
|
@@ -7046,7 +7102,8 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
7046
7102
|
//#region src/components/StoryInterface.tsx
|
|
7047
7103
|
var DEFAULT_MARKUP = "<header class=\"story-menubar\">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}";
|
|
7048
7104
|
function StoryInterface() {
|
|
7049
|
-
const
|
|
7105
|
+
const { storyData } = useStoryFields("storyData");
|
|
7106
|
+
const overridePassage = storyData?.passages.get("StoryInterface");
|
|
7050
7107
|
const markup = overridePassage !== void 0 ? overridePassage.content : DEFAULT_MARKUP;
|
|
7051
7108
|
const nobr = overridePassage?.tags.includes("nobr") ?? false;
|
|
7052
7109
|
const rendered = useMemo(() => {
|
|
@@ -7146,7 +7203,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
7146
7203
|
const onCloseRef = useRef(onClose);
|
|
7147
7204
|
onCloseRef.current = onClose;
|
|
7148
7205
|
const stableOnClose = useCallback(() => onCloseRef.current(), []);
|
|
7149
|
-
const storyData =
|
|
7206
|
+
const { storyData } = useStoryFields("storyData");
|
|
7150
7207
|
const passage = passageName ? storyData?.passages.get(passageName) : void 0;
|
|
7151
7208
|
const markup = passage?.content ?? fallbackMarkup;
|
|
7152
7209
|
const nobr = passage?.tags.includes("nobr") ?? false;
|
|
@@ -7428,6 +7485,14 @@ var settings = {
|
|
|
7428
7485
|
};
|
|
7429
7486
|
//#endregion
|
|
7430
7487
|
//#region src/action-registry.ts
|
|
7488
|
+
/** The fields of a StoryAction that are left out when unset. */
|
|
7489
|
+
var OPTIONAL_ACTION_FIELDS = [
|
|
7490
|
+
"target",
|
|
7491
|
+
"variable",
|
|
7492
|
+
"options",
|
|
7493
|
+
"value",
|
|
7494
|
+
"disabled"
|
|
7495
|
+
];
|
|
7431
7496
|
/**
|
|
7432
7497
|
* The registrations holding each ID, oldest first; the newest is the one
|
|
7433
7498
|
* listed. Generated IDs don't collide, but author IDs can, and one control's
|
|
@@ -7510,11 +7575,7 @@ function useAction(opts) {
|
|
|
7510
7575
|
label: opts.label,
|
|
7511
7576
|
perform: (...args) => performRef.current(...args)
|
|
7512
7577
|
};
|
|
7513
|
-
if (opts
|
|
7514
|
-
if (opts.variable !== void 0) action.variable = opts.variable;
|
|
7515
|
-
if (opts.options !== void 0) action.options = opts.options;
|
|
7516
|
-
if (opts.value !== void 0) action.value = opts.value;
|
|
7517
|
-
if (opts.disabled !== void 0) action.disabled = opts.disabled;
|
|
7578
|
+
for (const field of OPTIONAL_ACTION_FIELDS) if (opts[field] !== void 0) Object.assign(action, { [field]: opts[field] });
|
|
7518
7579
|
return action;
|
|
7519
7580
|
};
|
|
7520
7581
|
const registration = useRef(null);
|
|
@@ -7729,6 +7790,12 @@ function readGroup(params, src, args) {
|
|
|
7729
7790
|
const rest = positional[restIndex];
|
|
7730
7791
|
args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
|
|
7731
7792
|
}
|
|
7793
|
+
/** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
|
|
7794
|
+
var DELAY_PARAMETER = {
|
|
7795
|
+
name: "delay",
|
|
7796
|
+
type: "delay",
|
|
7797
|
+
required: true
|
|
7798
|
+
};
|
|
7732
7799
|
/** Read `rawArgs` into the declared `parameters` (see above). */
|
|
7733
7800
|
function parseMacroArgs(rawArgs, parameters) {
|
|
7734
7801
|
const args = {};
|
|
@@ -7782,15 +7849,19 @@ function parseVarArgs(rawArgs) {
|
|
|
7782
7849
|
placeholder
|
|
7783
7850
|
};
|
|
7784
7851
|
}
|
|
7852
|
+
/** The parameters of an `{option}` sub-macro: its value. */
|
|
7853
|
+
var OPTION_PARAMETERS = [{
|
|
7854
|
+
name: "value",
|
|
7855
|
+
type: "string"
|
|
7856
|
+
}];
|
|
7785
7857
|
/**
|
|
7786
|
-
* Walk AST children to find {option} macro nodes, returning their
|
|
7858
|
+
* Walk AST children to find {option} macro nodes, returning their values.
|
|
7787
7859
|
*/
|
|
7788
7860
|
function extractOptions(children) {
|
|
7789
7861
|
const options = [];
|
|
7790
7862
|
for (const node of children) if (node.type === "macro" && node.name === "option") {
|
|
7791
|
-
const
|
|
7792
|
-
|
|
7793
|
-
options.push(stripped);
|
|
7863
|
+
const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
|
|
7864
|
+
options.push(value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, "$2"));
|
|
7794
7865
|
}
|
|
7795
7866
|
return options;
|
|
7796
7867
|
}
|
|
@@ -7844,13 +7915,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
7844
7915
|
mutate: (code) => executeMutation(code, getValues(), update),
|
|
7845
7916
|
update,
|
|
7846
7917
|
getValues,
|
|
7847
|
-
wrap: (content) =>
|
|
7848
|
-
if (className || id) return h("span", {
|
|
7849
|
-
id,
|
|
7850
|
-
class: className
|
|
7851
|
-
}, content);
|
|
7852
|
-
return h(Fragment, null, content);
|
|
7853
|
-
}
|
|
7918
|
+
wrap: (content) => wrapContent(className, id, content)
|
|
7854
7919
|
};
|
|
7855
7920
|
if (config.merged) {
|
|
7856
7921
|
ctx.merged = useMergedLocals();
|
|
@@ -7981,9 +8046,11 @@ function setOne(draft, name, value) {
|
|
|
7981
8046
|
if (key.includes(".")) setByPath(namespace, key.split("."), value);
|
|
7982
8047
|
else namespace[key] = value;
|
|
7983
8048
|
}
|
|
8049
|
+
/** A method calling the store action `name`, as the store holds it then. */
|
|
8050
|
+
function storeAction(name) {
|
|
8051
|
+
return ((...args) => useStoryStore.getState()[name](...args));
|
|
8052
|
+
}
|
|
7984
8053
|
function createStoryAPI() {
|
|
7985
|
-
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
7986
|
-
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
7987
8054
|
return {
|
|
7988
8055
|
get(name) {
|
|
7989
8056
|
const { isTransient, key } = parseName(name);
|
|
@@ -8003,52 +8070,19 @@ function createStoryAPI() {
|
|
|
8003
8070
|
for (const [k, v] of entries) setOne(draft, k, v);
|
|
8004
8071
|
});
|
|
8005
8072
|
},
|
|
8006
|
-
goto(
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
return useStoryStore.getState().save(slot, custom);
|
|
8020
|
-
},
|
|
8021
|
-
load(slot) {
|
|
8022
|
-
return useStoryStore.getState().load(slot);
|
|
8023
|
-
},
|
|
8024
|
-
hasSave(slot) {
|
|
8025
|
-
return useStoryStore.getState().hasSave(slot);
|
|
8026
|
-
},
|
|
8027
|
-
getSaveInfo(slot) {
|
|
8028
|
-
return useStoryStore.getState().getSaveInfo(slot);
|
|
8029
|
-
},
|
|
8030
|
-
listSaves() {
|
|
8031
|
-
return useStoryStore.getState().listSaves();
|
|
8032
|
-
},
|
|
8033
|
-
deleteSave(slot) {
|
|
8034
|
-
return useStoryStore.getState().deleteSave(slot);
|
|
8035
|
-
},
|
|
8036
|
-
exportSave(slot) {
|
|
8037
|
-
return useStoryStore.getState().exportSave(slot);
|
|
8038
|
-
},
|
|
8039
|
-
importSave(data, slot) {
|
|
8040
|
-
return useStoryStore.getState().importSave(data, slot);
|
|
8041
|
-
},
|
|
8042
|
-
visited: visits.count,
|
|
8043
|
-
hasVisited: visits.has,
|
|
8044
|
-
hasVisitedAny: visits.any,
|
|
8045
|
-
hasVisitedAll: visits.all,
|
|
8046
|
-
rendered: renders.count,
|
|
8047
|
-
hasRendered: renders.has,
|
|
8048
|
-
hasRenderedAny: renders.any,
|
|
8049
|
-
hasRenderedAll: renders.all,
|
|
8050
|
-
currentPassage,
|
|
8051
|
-
previousPassage,
|
|
8073
|
+
goto: storeAction("navigate"),
|
|
8074
|
+
back: storeAction("goBack"),
|
|
8075
|
+
forward: storeAction("goForward"),
|
|
8076
|
+
restart: storeAction("restart"),
|
|
8077
|
+
save: storeAction("save"),
|
|
8078
|
+
load: storeAction("load"),
|
|
8079
|
+
hasSave: storeAction("hasSave"),
|
|
8080
|
+
getSaveInfo: storeAction("getSaveInfo"),
|
|
8081
|
+
listSaves: storeAction("listSaves"),
|
|
8082
|
+
deleteSave: storeAction("deleteSave"),
|
|
8083
|
+
exportSave: storeAction("exportSave"),
|
|
8084
|
+
importSave: storeAction("importSave"),
|
|
8085
|
+
...historyQueries(() => useStoryStore.getState().visitCounts, () => useStoryStore.getState().renderCounts),
|
|
8052
8086
|
get title() {
|
|
8053
8087
|
return useStoryStore.getState().storyData?.name || "";
|
|
8054
8088
|
},
|
|
@@ -8094,15 +8128,9 @@ function createStoryAPI() {
|
|
|
8094
8128
|
estimateSupported: false
|
|
8095
8129
|
};
|
|
8096
8130
|
},
|
|
8097
|
-
clearGameData()
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
clearAllData() {
|
|
8101
|
-
return useStoryStore.getState().clearAllData();
|
|
8102
|
-
},
|
|
8103
|
-
deletePlaythrough(playthroughId) {
|
|
8104
|
-
return useStoryStore.getState().deletePlaythrough(playthroughId);
|
|
8105
|
-
},
|
|
8131
|
+
clearGameData: storeAction("clearGameData"),
|
|
8132
|
+
clearAllData: storeAction("clearAllData"),
|
|
8133
|
+
deletePlaythrough: storeAction("deletePlaythrough"),
|
|
8106
8134
|
get backend() {
|
|
8107
8135
|
return getBackendType();
|
|
8108
8136
|
}
|
|
@@ -8510,26 +8538,51 @@ function astContainsChildren(nodes) {
|
|
|
8510
8538
|
}
|
|
8511
8539
|
//#endregion
|
|
8512
8540
|
//#region src/components/macros/Widget.tsx
|
|
8513
|
-
/**
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8541
|
+
/** A {widget} definition's name, then its `@` parameters. */
|
|
8542
|
+
var WIDGET_PARAMETERS = [{
|
|
8543
|
+
name: "name",
|
|
8544
|
+
type: "text",
|
|
8545
|
+
required: true
|
|
8546
|
+
}, {
|
|
8547
|
+
name: "parameters",
|
|
8548
|
+
type: "expression"
|
|
8549
|
+
}];
|
|
8550
|
+
/**
|
|
8551
|
+
* The widget a {widget} definition's arguments declare: its name and its
|
|
8552
|
+
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
8553
|
+
* Other words, such as `$name`, are not parameters.
|
|
8554
|
+
*/
|
|
8555
|
+
function widgetDef({ name = "", parameters = "" }) {
|
|
8520
8556
|
return {
|
|
8521
|
-
name
|
|
8522
|
-
params:
|
|
8557
|
+
name,
|
|
8558
|
+
params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
|
|
8523
8559
|
};
|
|
8524
8560
|
}
|
|
8561
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
8562
|
+
function parseWidgetDef(rawArgs) {
|
|
8563
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
8564
|
+
}
|
|
8565
|
+
/**
|
|
8566
|
+
* Register the widget a definition declares, with its body. Widgets whose
|
|
8567
|
+
* body renders {@children} take a closing tag, so they are registered as
|
|
8568
|
+
* block macros too: passages parsed later nest their content. A parameter
|
|
8569
|
+
* that no namespace can hold throws (see registerWidget), registering
|
|
8570
|
+
* nothing.
|
|
8571
|
+
*/
|
|
8572
|
+
function registerWidgetDef({ name, params }, body) {
|
|
8573
|
+
const isBlock = astContainsChildren(body);
|
|
8574
|
+
registerWidget(name, body, params, isBlock);
|
|
8575
|
+
if (isBlock) registerBlockMacro(name);
|
|
8576
|
+
}
|
|
8525
8577
|
defineMacro({
|
|
8526
8578
|
name: "widget",
|
|
8527
8579
|
block: true,
|
|
8528
|
-
|
|
8580
|
+
parameters: WIDGET_PARAMETERS,
|
|
8581
|
+
render({ children = [] }, ctx) {
|
|
8529
8582
|
let parsed;
|
|
8530
8583
|
let error;
|
|
8531
8584
|
try {
|
|
8532
|
-
const def =
|
|
8585
|
+
const def = widgetDef(ctx.args);
|
|
8533
8586
|
for (const param of def.params) checkVariableName(param.slice(1), param);
|
|
8534
8587
|
parsed = def;
|
|
8535
8588
|
} catch (err) {
|
|
@@ -8542,10 +8595,7 @@ defineMacro({
|
|
|
8542
8595
|
const childrenKey = JSON.stringify(children);
|
|
8543
8596
|
const paramsKey = params.join(",");
|
|
8544
8597
|
ctx.hooks.useLayoutEffect(() => {
|
|
8545
|
-
if (
|
|
8546
|
-
const isBlock = astContainsChildren(children);
|
|
8547
|
-
registerWidget(name, children, params, isBlock);
|
|
8548
|
-
if (isBlock) registerBlockMacro(name);
|
|
8598
|
+
if (parsed) registerWidgetDef(parsed, children);
|
|
8549
8599
|
}, [
|
|
8550
8600
|
name,
|
|
8551
8601
|
childrenKey,
|
|
@@ -8584,9 +8634,7 @@ defineMacro({
|
|
|
8584
8634
|
merged: true,
|
|
8585
8635
|
render({ rawArgs }, ctx) {
|
|
8586
8636
|
try {
|
|
8587
|
-
|
|
8588
|
-
const display = result == null ? "" : String(result);
|
|
8589
|
-
return ctx.wrap(display);
|
|
8637
|
+
return ctx.wrap(display(ctx.evaluate(rawArgs)));
|
|
8590
8638
|
} catch (err) {
|
|
8591
8639
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
8592
8640
|
macro: "print",
|
|
@@ -8595,8 +8643,7 @@ defineMacro({
|
|
|
8595
8643
|
}
|
|
8596
8644
|
},
|
|
8597
8645
|
text({ rawArgs }, ctx) {
|
|
8598
|
-
|
|
8599
|
-
return result == null ? "" : String(result);
|
|
8646
|
+
return display(ctx.evaluate(rawArgs));
|
|
8600
8647
|
}
|
|
8601
8648
|
});
|
|
8602
8649
|
//#endregion
|
|
@@ -8766,40 +8813,50 @@ defineMacro({
|
|
|
8766
8813
|
}
|
|
8767
8814
|
});
|
|
8768
8815
|
//#endregion
|
|
8816
|
+
//#region src/components/macros/branches.ts
|
|
8817
|
+
/**
|
|
8818
|
+
* The branch a conditional block ({if}, {switch}) shows: the first of
|
|
8819
|
+
* `branches` whose argument `matches`, else the last bare one ({else},
|
|
8820
|
+
* {default}), else null. Throws what `matches` throws.
|
|
8821
|
+
*/
|
|
8822
|
+
function selectBranch(branches, matches) {
|
|
8823
|
+
let fallback = null;
|
|
8824
|
+
for (const branch of branches) if (branch.rawArgs === "") fallback = branch;
|
|
8825
|
+
else if (matches(branch.rawArgs)) return branch;
|
|
8826
|
+
return fallback;
|
|
8827
|
+
}
|
|
8828
|
+
//#endregion
|
|
8769
8829
|
//#region src/components/macros/If.tsx
|
|
8830
|
+
/**
|
|
8831
|
+
* The {if}/{elseif}/{else} branch whose condition holds, else the {else}
|
|
8832
|
+
* branch, else null: the chain ends at its first bare branch. Throws what a
|
|
8833
|
+
* condition throws.
|
|
8834
|
+
*/
|
|
8835
|
+
function selectIfBranch(branches, evaluate) {
|
|
8836
|
+
const end = branches.findIndex((branch) => branch.rawArgs === "");
|
|
8837
|
+
return selectBranch(end < 0 ? branches : branches.slice(0, end + 1), (expr) => Boolean(evaluate(expr)));
|
|
8838
|
+
}
|
|
8770
8839
|
defineMacro({
|
|
8771
8840
|
name: "if",
|
|
8772
8841
|
block: true,
|
|
8773
8842
|
interpolate: true,
|
|
8774
8843
|
merged: true,
|
|
8775
8844
|
render({ branches = [] }, ctx) {
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
children
|
|
8845
|
+
let branch;
|
|
8846
|
+
try {
|
|
8847
|
+
branch = selectIfBranch(branches, ctx.evaluate);
|
|
8848
|
+
} catch (err) {
|
|
8849
|
+
return /* @__PURE__ */ jsx(MacroError, {
|
|
8850
|
+
macro: "if",
|
|
8851
|
+
error: err
|
|
8784
8852
|
});
|
|
8785
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
8786
|
-
}
|
|
8787
|
-
for (const branch of branches) {
|
|
8788
|
-
if (branch.rawArgs === "") return renderBranch(branch);
|
|
8789
|
-
try {
|
|
8790
|
-
if (ctx.evaluate(branch.rawArgs)) return renderBranch(branch);
|
|
8791
|
-
} catch (err) {
|
|
8792
|
-
return /* @__PURE__ */ jsx(MacroError, {
|
|
8793
|
-
macro: "if",
|
|
8794
|
-
error: err
|
|
8795
|
-
});
|
|
8796
|
-
}
|
|
8797
8853
|
}
|
|
8798
|
-
return null;
|
|
8854
|
+
if (!branch) return null;
|
|
8855
|
+
return wrapContent(ctx.resolve(branch.className), ctx.resolve(branch.id), ctx.renderNodes(branch.children));
|
|
8799
8856
|
},
|
|
8800
8857
|
text({ branches = [] }, ctx) {
|
|
8801
|
-
|
|
8802
|
-
return "";
|
|
8858
|
+
const branch = selectIfBranch(branches, ctx.evaluate);
|
|
8859
|
+
return branch ? ctx.renderText(branch.children) : "";
|
|
8803
8860
|
}
|
|
8804
8861
|
});
|
|
8805
8862
|
//#endregion
|
|
@@ -8995,11 +9052,7 @@ defineMacro({
|
|
|
8995
9052
|
name: "repeat",
|
|
8996
9053
|
block: true,
|
|
8997
9054
|
interpolate: true,
|
|
8998
|
-
parameters: [
|
|
8999
|
-
name: "delay",
|
|
9000
|
-
type: "delay",
|
|
9001
|
-
required: true
|
|
9002
|
-
}],
|
|
9055
|
+
parameters: [DELAY_PARAMETER],
|
|
9003
9056
|
render({ children = [] }, ctx) {
|
|
9004
9057
|
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
9005
9058
|
const delay = ctx.args.delay ?? 0;
|
|
@@ -9166,14 +9219,8 @@ defineMacro({
|
|
|
9166
9219
|
* The first {case} branch whose value equals `value`, else the {default}
|
|
9167
9220
|
* branch, else null. Throws what a case expression throws.
|
|
9168
9221
|
*/
|
|
9169
|
-
function
|
|
9170
|
-
|
|
9171
|
-
for (let i = 1; i < branches.length; i++) {
|
|
9172
|
-
const branch = branches[i];
|
|
9173
|
-
if (branch.rawArgs === "") defaultBranch = branch;
|
|
9174
|
-
else if (value === evaluate(branch.rawArgs)) return branch;
|
|
9175
|
-
}
|
|
9176
|
-
return defaultBranch;
|
|
9222
|
+
function selectCase(value, branches, evaluate) {
|
|
9223
|
+
return selectBranch(branches.slice(1), (expr) => value === evaluate(expr));
|
|
9177
9224
|
}
|
|
9178
9225
|
defineMacro({
|
|
9179
9226
|
name: "switch",
|
|
@@ -9196,7 +9243,7 @@ defineMacro({
|
|
|
9196
9243
|
}
|
|
9197
9244
|
let branch;
|
|
9198
9245
|
try {
|
|
9199
|
-
branch =
|
|
9246
|
+
branch = selectCase(switchValue, branches, ctx.evaluate);
|
|
9200
9247
|
} catch (err) {
|
|
9201
9248
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
9202
9249
|
macro: "case",
|
|
@@ -9206,21 +9253,24 @@ defineMacro({
|
|
|
9206
9253
|
return branch && /* @__PURE__ */ jsx(Fragment$1, { children: ctx.renderNodes(branch.children) });
|
|
9207
9254
|
},
|
|
9208
9255
|
text({ rawArgs, branches = [] }, ctx) {
|
|
9209
|
-
const branch =
|
|
9256
|
+
const branch = selectCase(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
9210
9257
|
return branch ? ctx.renderText(branch.children) : "";
|
|
9211
9258
|
}
|
|
9212
9259
|
});
|
|
9213
9260
|
//#endregion
|
|
9214
9261
|
//#region src/components/macros/Timed.tsx
|
|
9262
|
+
/** The parameters of {timed} and of each {next}: the delay before it shows. */
|
|
9263
|
+
var TIMED_PARAMETERS = [DELAY_PARAMETER];
|
|
9215
9264
|
defineMacro({
|
|
9216
9265
|
name: "timed",
|
|
9217
9266
|
subMacros: ["next"],
|
|
9218
9267
|
interpolate: true,
|
|
9268
|
+
parameters: TIMED_PARAMETERS,
|
|
9219
9269
|
render({ branches = [] }, ctx) {
|
|
9220
9270
|
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
9221
9271
|
const sections = useMemo(() => {
|
|
9222
9272
|
return branches.map((branch) => ({
|
|
9223
|
-
delay: branch.rawArgs
|
|
9273
|
+
delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
|
|
9224
9274
|
nodes: branch.children,
|
|
9225
9275
|
className: branch.className,
|
|
9226
9276
|
id: branch.id
|
|
@@ -9238,15 +9288,7 @@ defineMacro({
|
|
|
9238
9288
|
}, [visibleIndex, sections]);
|
|
9239
9289
|
if (visibleIndex < 0) return ctx.wrap(null);
|
|
9240
9290
|
const section = sections[visibleIndex];
|
|
9241
|
-
|
|
9242
|
-
const sectionClass = ctx.resolve(section.className);
|
|
9243
|
-
const sectionId = ctx.resolve(section.id);
|
|
9244
|
-
const inner = sectionClass || sectionId ? /* @__PURE__ */ jsx("span", {
|
|
9245
|
-
id: sectionId,
|
|
9246
|
-
class: sectionClass,
|
|
9247
|
-
children: content
|
|
9248
|
-
}) : content;
|
|
9249
|
-
return ctx.wrap(inner);
|
|
9291
|
+
return ctx.wrap(wrapContent(ctx.resolve(section.className), ctx.resolve(section.id), ctx.renderNodes(section.nodes)));
|
|
9250
9292
|
}
|
|
9251
9293
|
});
|
|
9252
9294
|
//#endregion
|
|
@@ -9267,11 +9309,7 @@ defineMacro({
|
|
|
9267
9309
|
name: "type",
|
|
9268
9310
|
block: true,
|
|
9269
9311
|
interpolate: true,
|
|
9270
|
-
parameters: [
|
|
9271
|
-
name: "delay",
|
|
9272
|
-
type: "delay",
|
|
9273
|
-
required: true
|
|
9274
|
-
}],
|
|
9312
|
+
parameters: [DELAY_PARAMETER],
|
|
9275
9313
|
render({ children = [] }, ctx) {
|
|
9276
9314
|
const { useState, useEffect, useRef } = ctx.hooks;
|
|
9277
9315
|
const speed = ctx.args.delay ?? 0;
|
|
@@ -9886,7 +9924,7 @@ defineMacro({
|
|
|
9886
9924
|
required: true
|
|
9887
9925
|
}],
|
|
9888
9926
|
render(_props, ctx) {
|
|
9889
|
-
const storyData =
|
|
9927
|
+
const { storyData } = useStoryFields("storyData");
|
|
9890
9928
|
const { inline } = ctx.args;
|
|
9891
9929
|
const passageName = evaluatePassageName(ctx.args.passage, ctx.evaluate);
|
|
9892
9930
|
const passage = storyData?.passages.get(passageName);
|
|
@@ -9953,6 +9991,26 @@ defineMacro({
|
|
|
9953
9991
|
});
|
|
9954
9992
|
//#endregion
|
|
9955
9993
|
//#region src/components/macros/input-macro.ts
|
|
9994
|
+
/** The story variable a `storeVar` macro binds: its first parameter. */
|
|
9995
|
+
var VARIABLE_PARAMETER = {
|
|
9996
|
+
name: "variable",
|
|
9997
|
+
type: "variable",
|
|
9998
|
+
required: true
|
|
9999
|
+
};
|
|
10000
|
+
/**
|
|
10001
|
+
* Register the automation action of a macro bound to a story variable: keyed
|
|
10002
|
+
* by the variable (or `key`), with the variable's value unless `action`
|
|
10003
|
+
* gives another.
|
|
10004
|
+
*/
|
|
10005
|
+
function useVariableAction(ctx, action) {
|
|
10006
|
+
return ctx.useAction({
|
|
10007
|
+
key: `$${ctx.varName}`,
|
|
10008
|
+
authorId: ctx.id,
|
|
10009
|
+
variable: ctx.varName,
|
|
10010
|
+
value: ctx.value,
|
|
10011
|
+
...action
|
|
10012
|
+
});
|
|
10013
|
+
}
|
|
9956
10014
|
/**
|
|
9957
10015
|
* Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
|
|
9958
10016
|
* `type` (or, without one, a `<textarea>`) bound to the story variable.
|
|
@@ -9964,30 +10022,22 @@ function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? St
|
|
|
9964
10022
|
defineMacro({
|
|
9965
10023
|
name,
|
|
9966
10024
|
storeVar: true,
|
|
9967
|
-
parameters: [{
|
|
9968
|
-
name: "variable",
|
|
9969
|
-
type: "variable",
|
|
9970
|
-
required: true
|
|
9971
|
-
}, {
|
|
10025
|
+
parameters: [VARIABLE_PARAMETER, {
|
|
9972
10026
|
name: "placeholder",
|
|
9973
10027
|
type: "string"
|
|
9974
10028
|
}],
|
|
9975
10029
|
render(_props, ctx) {
|
|
9976
10030
|
const placeholder = ctx.args.placeholder ?? "";
|
|
9977
|
-
ctx
|
|
10031
|
+
useVariableAction(ctx, {
|
|
9978
10032
|
type: name,
|
|
9979
|
-
key: `$${ctx.varName}`,
|
|
9980
|
-
authorId: ctx.id,
|
|
9981
10033
|
label: placeholder || ctx.varName,
|
|
9982
|
-
variable: ctx.varName,
|
|
9983
|
-
value: ctx.value,
|
|
9984
10034
|
perform: (v) => ctx.setValue(toValue(v))
|
|
9985
10035
|
});
|
|
9986
10036
|
return h(type ? "input" : "textarea", {
|
|
9987
10037
|
type,
|
|
9988
10038
|
id: ctx.id,
|
|
9989
10039
|
class: ctx.cls,
|
|
9990
|
-
value:
|
|
10040
|
+
value: display(ctx.value),
|
|
9991
10041
|
placeholder,
|
|
9992
10042
|
onInput: (e) => ctx.setValue(toValue(e.target.value))
|
|
9993
10043
|
});
|
|
@@ -10008,22 +10058,15 @@ defineInputMacro("textarea");
|
|
|
10008
10058
|
defineMacro({
|
|
10009
10059
|
name: "checkbox",
|
|
10010
10060
|
storeVar: true,
|
|
10011
|
-
parameters: [{
|
|
10012
|
-
name: "variable",
|
|
10013
|
-
type: "variable",
|
|
10014
|
-
required: true
|
|
10015
|
-
}, {
|
|
10061
|
+
parameters: [VARIABLE_PARAMETER, {
|
|
10016
10062
|
name: "label",
|
|
10017
10063
|
type: "text"
|
|
10018
10064
|
}],
|
|
10019
10065
|
render(_props, ctx) {
|
|
10020
10066
|
const label = ctx.args.label ?? "";
|
|
10021
|
-
ctx
|
|
10067
|
+
useVariableAction(ctx, {
|
|
10022
10068
|
type: "checkbox",
|
|
10023
|
-
key: `$${ctx.varName}`,
|
|
10024
|
-
authorId: ctx.id,
|
|
10025
10069
|
label: label || ctx.varName || "",
|
|
10026
|
-
variable: ctx.varName,
|
|
10027
10070
|
value: !!ctx.value,
|
|
10028
10071
|
perform: (v) => ctx.setValue(v !== void 0 ? !!v : !ctx.value)
|
|
10029
10072
|
});
|
|
@@ -10044,11 +10087,7 @@ defineMacro({
|
|
|
10044
10087
|
name: "radiobutton",
|
|
10045
10088
|
storeVar: true,
|
|
10046
10089
|
parameters: [
|
|
10047
|
-
|
|
10048
|
-
name: "variable",
|
|
10049
|
-
type: "variable",
|
|
10050
|
-
required: true
|
|
10051
|
-
},
|
|
10090
|
+
VARIABLE_PARAMETER,
|
|
10052
10091
|
{
|
|
10053
10092
|
name: "value",
|
|
10054
10093
|
type: "text",
|
|
@@ -10062,13 +10101,10 @@ defineMacro({
|
|
|
10062
10101
|
render(_props, ctx) {
|
|
10063
10102
|
const radioValue = ctx.args.value ?? "";
|
|
10064
10103
|
const label = ctx.args.label ?? "";
|
|
10065
|
-
ctx
|
|
10104
|
+
useVariableAction(ctx, {
|
|
10066
10105
|
type: "radiobutton",
|
|
10067
10106
|
key: `$${ctx.varName}:${radioValue}`,
|
|
10068
|
-
authorId: ctx.id,
|
|
10069
10107
|
label: label || radioValue,
|
|
10070
|
-
variable: ctx.varName,
|
|
10071
|
-
value: ctx.value,
|
|
10072
10108
|
perform: () => ctx.setValue(radioValue)
|
|
10073
10109
|
});
|
|
10074
10110
|
return /* @__PURE__ */ jsxs("label", {
|
|
@@ -10089,21 +10125,13 @@ defineMacro({
|
|
|
10089
10125
|
name: "listbox",
|
|
10090
10126
|
subMacros: ["option"],
|
|
10091
10127
|
storeVar: true,
|
|
10092
|
-
parameters: [
|
|
10093
|
-
name: "variable",
|
|
10094
|
-
type: "variable",
|
|
10095
|
-
required: true
|
|
10096
|
-
}],
|
|
10128
|
+
parameters: [VARIABLE_PARAMETER],
|
|
10097
10129
|
render({ children = [] }, ctx) {
|
|
10098
10130
|
const options = ctx.extractOptions(children);
|
|
10099
|
-
ctx
|
|
10131
|
+
useVariableAction(ctx, {
|
|
10100
10132
|
type: "listbox",
|
|
10101
|
-
key: `$${ctx.varName}`,
|
|
10102
|
-
authorId: ctx.id,
|
|
10103
10133
|
label: ctx.varName,
|
|
10104
|
-
variable: ctx.varName,
|
|
10105
10134
|
options,
|
|
10106
|
-
value: ctx.value,
|
|
10107
10135
|
perform: (v) => {
|
|
10108
10136
|
if (v !== void 0) ctx.setValue(String(v));
|
|
10109
10137
|
}
|
|
@@ -10111,7 +10139,7 @@ defineMacro({
|
|
|
10111
10139
|
return /* @__PURE__ */ jsx("select", {
|
|
10112
10140
|
id: ctx.id,
|
|
10113
10141
|
class: ctx.cls,
|
|
10114
|
-
value:
|
|
10142
|
+
value: display(ctx.value),
|
|
10115
10143
|
onChange: (e) => ctx.setValue(e.target.value),
|
|
10116
10144
|
children: options.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
10117
10145
|
value: opt,
|
|
@@ -10126,11 +10154,7 @@ defineMacro({
|
|
|
10126
10154
|
name: "cycle",
|
|
10127
10155
|
block: true,
|
|
10128
10156
|
storeVar: true,
|
|
10129
|
-
parameters: [
|
|
10130
|
-
name: "variable",
|
|
10131
|
-
type: "variable",
|
|
10132
|
-
required: true
|
|
10133
|
-
}],
|
|
10157
|
+
parameters: [VARIABLE_PARAMETER],
|
|
10134
10158
|
render({ children = [] }, ctx) {
|
|
10135
10159
|
const options = ctx.extractOptions(children);
|
|
10136
10160
|
const handleClick = () => {
|
|
@@ -10139,14 +10163,10 @@ defineMacro({
|
|
|
10139
10163
|
const nextIndex = (options.indexOf(String(current)) + 1) % options.length;
|
|
10140
10164
|
ctx.setValue(options[nextIndex]);
|
|
10141
10165
|
};
|
|
10142
|
-
ctx
|
|
10166
|
+
useVariableAction(ctx, {
|
|
10143
10167
|
type: "cycle",
|
|
10144
|
-
key: `$${ctx.varName}`,
|
|
10145
|
-
authorId: ctx.id,
|
|
10146
10168
|
label: ctx.value == null ? options[0] || "" : String(ctx.value),
|
|
10147
|
-
variable: ctx.varName,
|
|
10148
10169
|
options,
|
|
10149
|
-
value: ctx.value,
|
|
10150
10170
|
perform: (v) => {
|
|
10151
10171
|
if (v !== void 0) ctx.setValue(v);
|
|
10152
10172
|
else handleClick();
|
|
@@ -10192,6 +10212,20 @@ function parsePassage(passage) {
|
|
|
10192
10212
|
function renderPassageContent(passage) {
|
|
10193
10213
|
return renderNodes(parsePassage(passage), passage.tags.includes("nobr") ? { nobr: true } : void 0);
|
|
10194
10214
|
}
|
|
10215
|
+
/**
|
|
10216
|
+
* The content of a special passage shown with every passage (PassageHeader,
|
|
10217
|
+
* PassageFooter, PassageDone), or null. One that fails to render is logged
|
|
10218
|
+
* and left out, so it can't take the passage down with it.
|
|
10219
|
+
*/
|
|
10220
|
+
function renderSpecialPassage(passage) {
|
|
10221
|
+
if (!passage) return null;
|
|
10222
|
+
try {
|
|
10223
|
+
return renderPassageContent(passage);
|
|
10224
|
+
} catch (err) {
|
|
10225
|
+
console.error(`spindle: Error in ${passage.name}:`, err);
|
|
10226
|
+
return null;
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10195
10229
|
var CODE_PASSAGES = new Set([
|
|
10196
10230
|
"PassageReady",
|
|
10197
10231
|
"PassageHeader",
|
|
@@ -10199,7 +10233,7 @@ var CODE_PASSAGES = new Set([
|
|
|
10199
10233
|
"PassageDone"
|
|
10200
10234
|
]);
|
|
10201
10235
|
function Passage({ passage, dataTransition, navigationId }) {
|
|
10202
|
-
const storyData =
|
|
10236
|
+
const { storyData } = useStoryFields("storyData");
|
|
10203
10237
|
const isCodePassage = CODE_PASSAGES.has(passage.name);
|
|
10204
10238
|
const [doneReady, setDoneReady] = useState(false);
|
|
10205
10239
|
const content = useMemo(() => {
|
|
@@ -10219,40 +10253,17 @@ function Passage({ passage, dataTransition, navigationId }) {
|
|
|
10219
10253
|
});
|
|
10220
10254
|
}
|
|
10221
10255
|
}, [passage.content, passage.name]);
|
|
10222
|
-
const
|
|
10223
|
-
const
|
|
10224
|
-
const
|
|
10225
|
-
const
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
return renderPassageContent(headerPassage);
|
|
10229
|
-
} catch (err) {
|
|
10230
|
-
console.error("spindle: Error in PassageHeader:", err);
|
|
10231
|
-
return null;
|
|
10232
|
-
}
|
|
10233
|
-
}, [headerPassage?.content]);
|
|
10234
|
-
const footerContent = useMemo(() => {
|
|
10235
|
-
if (!footerPassage) return null;
|
|
10236
|
-
try {
|
|
10237
|
-
return renderPassageContent(footerPassage);
|
|
10238
|
-
} catch (err) {
|
|
10239
|
-
console.error("spindle: Error in PassageFooter:", err);
|
|
10240
|
-
return null;
|
|
10241
|
-
}
|
|
10242
|
-
}, [footerPassage?.content]);
|
|
10256
|
+
const special = (name) => isCodePassage ? void 0 : storyData?.passages.get(name);
|
|
10257
|
+
const headerPassage = special("PassageHeader");
|
|
10258
|
+
const footerPassage = special("PassageFooter");
|
|
10259
|
+
const donePassage = special("PassageDone");
|
|
10260
|
+
const headerContent = useMemo(() => renderSpecialPassage(headerPassage), [headerPassage?.content]);
|
|
10261
|
+
const footerContent = useMemo(() => renderSpecialPassage(footerPassage), [footerPassage?.content]);
|
|
10243
10262
|
useEffect(() => {
|
|
10244
10263
|
if (donePassage) setDoneReady(true);
|
|
10245
10264
|
return () => setDoneReady(false);
|
|
10246
10265
|
}, [passage.name]);
|
|
10247
|
-
const doneContent = useMemo(() =>
|
|
10248
|
-
if (!doneReady || !donePassage) return null;
|
|
10249
|
-
try {
|
|
10250
|
-
return renderPassageContent(donePassage);
|
|
10251
|
-
} catch (err) {
|
|
10252
|
-
console.error("spindle: Error in PassageDone:", err);
|
|
10253
|
-
return null;
|
|
10254
|
-
}
|
|
10255
|
-
}, [doneReady, donePassage?.content]);
|
|
10266
|
+
const doneContent = useMemo(() => doneReady ? renderSpecialPassage(donePassage) : null, [doneReady, donePassage?.content]);
|
|
10256
10267
|
const elRef = useRef(null);
|
|
10257
10268
|
useLayoutEffect(() => {
|
|
10258
10269
|
if (navigationId !== void 0) markPassageRendered(navigationId);
|
|
@@ -10653,16 +10664,12 @@ function boot() {
|
|
|
10653
10664
|
for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
|
|
10654
10665
|
const widgetAST = buildAST(tokenize(passage.content));
|
|
10655
10666
|
for (const node of widgetAST) if (node.type === "macro" && node.name === "widget" && node.rawArgs) {
|
|
10656
|
-
const
|
|
10657
|
-
const children = node.children;
|
|
10658
|
-
const isBlock = astContainsChildren(children);
|
|
10667
|
+
const def = parseWidgetDef(node.rawArgs);
|
|
10659
10668
|
try {
|
|
10660
|
-
|
|
10669
|
+
registerWidgetDef(def, node.children);
|
|
10661
10670
|
} catch (err) {
|
|
10662
|
-
console.error(`spindle: widget "${
|
|
10663
|
-
continue;
|
|
10671
|
+
console.error(`spindle: widget "${def.name}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`);
|
|
10664
10672
|
}
|
|
10665
|
-
if (isBlock) registerBlockMacro(widgetName);
|
|
10666
10673
|
}
|
|
10667
10674
|
}
|
|
10668
10675
|
let prevNavigationId = useStoryStore.getState().navigationId;
|