@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.
Files changed (46) hide show
  1. package/README.md +26 -0
  2. package/dist/pkg/format.js +1 -1
  3. package/dist/pkg/headless.js +710 -703
  4. package/dist/pkg/macro-registry.json +18 -0
  5. package/package.json +1 -1
  6. package/src/action-registry.ts +9 -0
  7. package/src/class-registry.ts +1 -5
  8. package/src/components/Passage.tsx +35 -39
  9. package/src/components/PassageDialog.tsx +2 -2
  10. package/src/components/StoryInterface.tsx +2 -2
  11. package/src/components/macros/Checkbox.tsx +3 -8
  12. package/src/components/macros/Computed.tsx +1 -1
  13. package/src/components/macros/Cycle.tsx +3 -6
  14. package/src/components/macros/ExprDisplay.tsx +12 -29
  15. package/src/components/macros/If.tsx +35 -41
  16. package/src/components/macros/Include.tsx +2 -1
  17. package/src/components/macros/Listbox.tsx +5 -7
  18. package/src/components/macros/Print.tsx +3 -5
  19. package/src/components/macros/Radiobutton.tsx +3 -5
  20. package/src/components/macros/Repeat.tsx +2 -1
  21. package/src/components/macros/Switch.tsx +6 -13
  22. package/src/components/macros/Timed.tsx +14 -18
  23. package/src/components/macros/Type.tsx +2 -1
  24. package/src/components/macros/VarDisplay.tsx +12 -32
  25. package/src/components/macros/Widget.tsx +49 -20
  26. package/src/components/macros/branches.ts +18 -0
  27. package/src/components/macros/display.ts +19 -0
  28. package/src/components/macros/input-macro.ts +33 -11
  29. package/src/components/macros/macro-args.ts +7 -0
  30. package/src/components/macros/option-utils.ts +10 -7
  31. package/src/define-macro.ts +4 -6
  32. package/src/expression.ts +58 -40
  33. package/src/hooks/use-action.ts +7 -15
  34. package/src/index.tsx +5 -15
  35. package/src/interpolation.ts +34 -41
  36. package/src/markup/ast.ts +4 -3
  37. package/src/markup/render.tsx +4 -12
  38. package/src/registry.ts +13 -12
  39. package/src/saves/save-manager.ts +18 -13
  40. package/src/saves/storage.ts +19 -20
  41. package/src/story-api.ts +30 -73
  42. package/src/structural.ts +1 -18
  43. package/src/types-drift-check.ts +37 -50
  44. package/src/utils/macro-names.ts +40 -0
  45. package/src/utils/namespace.ts +17 -0
  46. package/src/widgets/widget-registry.ts +5 -4
@@ -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 Set([
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.toLowerCase());
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
- var cachedFns = null;
3042
- var cachedVisitCounts = null;
3043
- var cachedRenderCounts = null;
3044
- function buildExpressionFns() {
3045
- const { visitCounts, renderCounts } = useStoryStore.getState();
3046
- if (cachedFns && cachedVisitCounts === visitCounts && cachedRenderCounts === renderCounts) return cachedFns;
3047
- const visits = countQueries(() => visitCounts);
3048
- const renders = countQueries(() => renderCounts);
3049
- cachedFns = {
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, variables, temporary, locals = EMPTY_NAMESPACE, transient = EMPTY_NAMESPACE) {
2803
+ function evaluate(expr, ...namespaces) {
3085
2804
  const body = `return (${transform(expr)}\n);`;
3086
- return getOrCompile(body, body)(namespaceArg(variables), namespaceArg(temporary), namespaceArg(locals), buildExpressionFns(), namespaceArg(transient));
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, variables, temporary, locals = EMPTY_NAMESPACE, transient = EMPTY_NAMESPACE) {
2811
+ function execute(code, ...namespaces) {
3093
2812
  const transformed = transform(code, "statements");
3094
- getOrCompile("exec:" + transformed, transformed)(namespaceArg(variables), namespaceArg(temporary), namespaceArg(locals), buildExpressionFns(), namespaceArg(transient));
2813
+ run(getOrCompile("exec:" + transformed, transformed), ...namespaces);
3095
2814
  }
3096
2815
  //#endregion
3097
2816
  //#region src/registry.ts
3098
- var registry = /* @__PURE__ */ new Map();
2817
+ var registry = new NameMap();
3099
2818
  function registerMacro(name, component) {
3100
- registry.set(name.toLowerCase(), component);
2819
+ registry.set(name, component);
3101
2820
  }
3102
2821
  function getMacro(name) {
3103
- return registry.get(name.toLowerCase());
2822
+ return registry.get(name);
3104
2823
  }
3105
- var textRegistry = /* @__PURE__ */ new Map();
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.toLowerCase(), text);
3109
- else textRegistry.delete(name.toLowerCase());
2827
+ if (text) textRegistry.set(name, text);
2828
+ else textRegistry.delete(name);
3110
2829
  }
3111
2830
  function getMacroText(name) {
3112
- return textRegistry.get(name.toLowerCase());
2831
+ return textRegistry.get(name);
3113
2832
  }
3114
- var subMacros = /* @__PURE__ */ new Set();
2833
+ var subMacros = new NameSet();
3115
2834
  function registerSubMacro(name) {
3116
- subMacros.add(name.toLowerCase());
2835
+ subMacros.add(name);
3117
2836
  }
3118
2837
  function isSubMacro(name) {
3119
- return subMacros.has(name.toLowerCase());
2838
+ return subMacros.has(name);
3120
2839
  }
3121
- var metadataRegistry = /* @__PURE__ */ new Map();
2840
+ var metadataRegistry = new NameMap();
3122
2841
  function registerMacroMetadata(name, metadata) {
3123
- metadataRegistry.set(name.toLowerCase(), metadata);
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 = /* @__PURE__ */ new Map();
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.toLowerCase(), {
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.toLowerCase());
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(this.evaluate(node.expression, scope));
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 = this.evaluate(expr, scope);
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) => this.evaluate(expr, scope),
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(evaluate(part.expr, scope.variables, scope.temporary, scope.locals, scope.transient));
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
- function VarDisplay({ name, scope, className, id }) {
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 nameError = variableNameError(root, SIGILS[scope] + root);
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: `{${SIGILS[scope]}${name} error: ${nameError}}`
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
- const display = value == null ? "" : String(value);
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
- function ExprDisplay({ expression, className, id }) {
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 display;
3416
+ let text;
3690
3417
  try {
3691
- const value = evaluate(expression, variables, temporary, localsValues, transient);
3692
- display = value == null ? "" : String(value);
3418
+ text = display(evaluate(expression, variables, temporary, localsValues, transient));
3693
3419
  } catch {
3694
- display = `{error: ${expression}}`;
3420
+ text = `{error: ${expression}}`;
3695
3421
  }
3696
- if (className || id) return /* @__PURE__ */ jsx("span", {
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 nobr = useContext(NobrContext);
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
- name: node.name,
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
- if (!db.objectStoreNames.contains("saves")) {
4796
- const savesStore = db.createObjectStore("saves", { keyPath: "meta.id" });
4797
- savesStore.createIndex("ifid", "meta.ifid", { unique: false });
4798
- savesStore.createIndex("playthroughId", "meta.playthroughId", { unique: false });
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
- var _backendPromise = null;
4882
- function getBackend() {
4883
- if (_backend) return Promise.resolve(_backend);
4884
- if (!_backendPromise) {
4885
- const pending = detectBackend().then((backend) => {
4886
- if (_backendPromise === pending) _backend = backend;
4887
- return backend;
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
- _backendPromise = pending;
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
- return _backendPromise;
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
- function resetBackend() {
4894
- _backend = null;
4895
- _backendPromise = null;
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
- * Synchronous accessor for the current backend type.
4899
- * Returns 'memory' if the backend hasn't been detected yet.
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 getBackendType() {
4902
- return _backend?.type ?? "memory";
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
- queued(async (saveId) => livePayload(await (await getBackend()).getSave(saveId)));
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
- queued(async (ifid, slot) => livePayload(await slotRecord(ifid, slot)));
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 = queued(async (saveId) => toExport(await (await getBackend()).getSave(saveId)));
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 = queued(async (ifid, slot) => toExport(await slotRecord(ifid, slot)));
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 overridePassage = useStoryStore((s) => s.storyData)?.passages.get("StoryInterface");
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 = useStoryStore((s) => s.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.target !== void 0) action.target = opts.target;
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 rawArgs as values.
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 raw = node.rawArgs.trim();
7792
- const stripped = readWholeQuoted(raw) ?? raw.replace(/^(["'])(.+)\1$/s, "$2");
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(passageName) {
8007
- useStoryStore.getState().navigate(passageName);
8008
- },
8009
- back() {
8010
- useStoryStore.getState().goBack();
8011
- },
8012
- forward() {
8013
- useStoryStore.getState().goForward();
8014
- },
8015
- restart() {
8016
- useStoryStore.getState().restart();
8017
- },
8018
- save(slot, custom) {
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
- return useStoryStore.getState().clearGameData();
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
- * Read the arguments of a {widget} definition: its name (quotes dropped)
8515
- * and its parameters, the words after it that start with one of the
8516
- * characters of `sigils`.
8517
- */
8518
- function parseWidgetDef(rawArgs, sigils = "@") {
8519
- const [first, ...rest] = rawArgs.trim().split(/\s+/);
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: first.replace(/["']/g, ""),
8522
- params: rest.filter((word) => sigils.includes(word[0]))
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
- render({ rawArgs, children = [] }, ctx) {
8580
+ parameters: WIDGET_PARAMETERS,
8581
+ render({ children = [] }, ctx) {
8529
8582
  let parsed;
8530
8583
  let error;
8531
8584
  try {
8532
- const def = parseWidgetDef(rawArgs);
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 (!parsed) return;
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
- const result = ctx.evaluate(rawArgs);
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
- const result = ctx.evaluate(rawArgs);
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
- function renderBranch(branch) {
8777
- const children = ctx.renderNodes(branch.children);
8778
- const cls = ctx.resolve(branch.className);
8779
- const branchId = ctx.resolve(branch.id);
8780
- if (cls || branchId) return /* @__PURE__ */ jsx("span", {
8781
- id: branchId,
8782
- class: cls,
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
- for (const branch of branches) if (branch.rawArgs === "" || ctx.evaluate(branch.rawArgs)) return ctx.renderText(branch.children);
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 selectBranch(value, branches, evaluate) {
9170
- let defaultBranch = null;
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 = selectBranch(switchValue, branches, ctx.evaluate);
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 = selectBranch(ctx.evaluate(rawArgs), branches, ctx.evaluate);
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 ? parseDelay(branch.rawArgs) : 0,
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
- const content = ctx.renderNodes(section.nodes);
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 = useStoryStore((s) => s.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.useAction({
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: ctx.value == null ? "" : String(ctx.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.useAction({
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.useAction({
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.useAction({
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: ctx.value == null ? "" : String(ctx.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.useAction({
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 = useStoryStore((s) => s.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 headerPassage = isCodePassage ? void 0 : storyData?.passages.get("PassageHeader");
10223
- const footerPassage = isCodePassage ? void 0 : storyData?.passages.get("PassageFooter");
10224
- const donePassage = isCodePassage ? void 0 : storyData?.passages.get("PassageDone");
10225
- const headerContent = useMemo(() => {
10226
- if (!headerPassage) return null;
10227
- try {
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 { name: widgetName, params } = parseWidgetDef(node.rawArgs, "$_@");
10657
- const children = node.children;
10658
- const isBlock = astContainsChildren(children);
10667
+ const def = parseWidgetDef(node.rawArgs);
10659
10668
  try {
10660
- registerWidget(widgetName, children, params, isBlock);
10669
+ registerWidgetDef(def, node.children);
10661
10670
  } catch (err) {
10662
- console.error(`spindle: widget "${widgetName}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`);
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;