@rohal12/spindle 0.52.9 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, u
3
3
  import { useSyncExternalStore } from "preact/compat";
4
4
  import { createStore } from "zustand/vanilla";
5
5
  import { immer } from "zustand/middleware/immer";
6
- import { Immer, applyPatches, current, enableMapSet, enablePatches, freeze, isDraft, produceWithPatches } from "immer";
6
+ import { Immer, current, enableMapSet, enablePatches, freeze, isDraft } from "immer";
7
7
  import { shallow } from "zustand/vanilla/shallow";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "preact/jsx-runtime";
9
9
  import { micromark } from "micromark";
@@ -32,338 +32,6 @@ function create() {
32
32
  return (initializer) => bindPreact(createStore()(initializer));
33
33
  }
34
34
  //#endregion
35
- //#region src/utils/namespace.ts
36
- /** The variable name no namespace can hold. */
37
- var RESERVED_NAME = "__proto__";
38
- /**
39
- * Why `name` (without its sigil) cannot be a variable name, or undefined if
40
- * it can. `label` is the name as the author wrote it (e.g. `$__proto__`).
41
- */
42
- function variableNameError(name, label = name) {
43
- return name === "__proto__" ? `"${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
44
- }
45
- /** Throw a TypeError if `name` cannot be a variable name. */
46
- function checkVariableName(name, label = name) {
47
- const error = variableNameError(name, label);
48
- if (error) throw new TypeError(`spindle: ${error}`);
49
- }
50
- /** Whether `obj` holds `key` as an own property. */
51
- var hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
52
- /** `ns[key]` if `ns` holds it as an own property, else undefined. */
53
- var ownValue = (ns, key) => ns && hasOwn(ns, key) ? ns[key] : void 0;
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
- /**
68
- * A record with no prototype holding the own entries of `sources`, or only
69
- * those whose value passes `keep`.
70
- */
71
- function ownEntries(sources, keep = () => true) {
72
- const record = Object.create(null);
73
- for (const source of sources) {
74
- if (!source) continue;
75
- for (const key of Object.keys(source)) {
76
- const value = source[key];
77
- if (keep(value)) record[key] = value;
78
- }
79
- }
80
- return record;
81
- }
82
- /** A namespace with no prototype holding the own entries of `sources`. */
83
- var createNamespace = (...sources) => ownEntries(sources);
84
- /** Whether `value` is an object with no prototype. */
85
- var isNamespace = (value) => Object.getPrototypeOf(value) === null;
86
- /** `ns` itself if it has no prototype, else a namespace copy of it. */
87
- var asNamespace = (ns) => isNamespace(ns) ? ns : createNamespace(ns);
88
- /** A namespace copy of `ns` with `key` set to `value`. */
89
- function withEntry(ns, key, value) {
90
- const next = createNamespace(ns);
91
- next[key] = value;
92
- return next;
93
- }
94
- /** An empty namespace that cannot change. */
95
- var EMPTY_NAMESPACE = Object.freeze(createNamespace());
96
- /** The names whose own entries differ between `a` and `b`, `a`'s first. */
97
- function changedNames(a, b) {
98
- return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((name) => ownValue(a, name) !== ownValue(b, name));
99
- }
100
- var isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
101
- /** The count of `name` in `counts`: its own entry, or 0. */
102
- function countOf(counts, name) {
103
- const value = ownValue(counts, name);
104
- return isCount(value) ? value : 0;
105
- }
106
- /**
107
- * Counters with no prototype holding the own entries of `source` that are
108
- * counts, with `name` (if given) counted once more. Immer copies the
109
- * counters on every write anyway, so counting into a fresh copy costs no
110
- * more than counting in place.
111
- */
112
- function createCounts(source, name) {
113
- const counts = ownEntries([source], isCount);
114
- if (name !== void 0) counts[name] = countOf(source, name) + 1;
115
- return counts;
116
- }
117
- //#endregion
118
- //#region src/class-registry.ts
119
- var registry$1 = /* @__PURE__ */ new Map();
120
- var ctorToName = /* @__PURE__ */ new Map();
121
- function registerClass(name, ctor) {
122
- registry$1.set(name, ctor);
123
- ctorToName.set(ctor, name);
124
- }
125
- /**
126
- * The registered name of the class `value` is an instance of, or undefined.
127
- * Looks the constructor up on the prototype, so an own property named
128
- * "constructor" (plain data) cannot hide or fake the class.
129
- */
130
- function registeredClassName(value) {
131
- const proto = Object.getPrototypeOf(value);
132
- return proto === null ? void 0 : ctorToName.get(proto.constructor);
133
- }
134
- var CLASS_TAG = "__spindle_class__";
135
- var DATA_TAG = "__spindle_data__";
136
- /** A tagged value, as serialize() writes and deserialize() reads it. */
137
- var tagged = (name, data) => ({
138
- [CLASS_TAG]: name,
139
- [DATA_TAG]: data
140
- });
141
- /** Numbers JSON text cannot hold (it writes null for them, and 0 for -0). */
142
- var SPECIAL_NUMBERS = {
143
- NaN: NaN,
144
- Infinity: Infinity,
145
- "-Infinity": -Infinity,
146
- "-0": -0
147
- };
148
- function specialNumberName(value) {
149
- if (Number.isNaN(value)) return "NaN";
150
- if (value === Infinity) return "Infinity";
151
- if (value === -Infinity) return "-Infinity";
152
- if (Object.is(value, -0)) return "-0";
153
- }
154
- /**
155
- * Turn a story value into data that survives JSON text (saves, exports,
156
- * the session) and that deserialize() restores. Date, RegExp, Map, Set and
157
- * registered class instances become tagged objects, and so do the values
158
- * JSON text would drop or change: undefined (also array holes), NaN,
159
- * ±Infinity, -0, bigint, invalid dates, and plain objects with a key named
160
- * like the class tag. Throws on circular references and on a property
161
- * named "__proto__", which story state cannot hold.
162
- */
163
- function serialize(value) {
164
- const seen = /* @__PURE__ */ new Set();
165
- function ser(val) {
166
- if (val === void 0) return tagged("__Undefined__", {});
167
- if (typeof val === "number") {
168
- const name = specialNumberName(val);
169
- return name === void 0 ? val : tagged("__Number__", { value: name });
170
- }
171
- if (typeof val === "bigint") return tagged("__BigInt__", { value: val.toString() });
172
- if (val === null || typeof val !== "object") return val;
173
- if (seen.has(val)) throw new Error("spindle: Cannot serialize circular references");
174
- seen.add(val);
175
- try {
176
- return serObject(val);
177
- } finally {
178
- seen.delete(val);
179
- }
180
- }
181
- function serKeys(obj) {
182
- const data = {};
183
- for (const key of Object.keys(obj)) {
184
- if (key === "__proto__") throw new Error("spindle: Cannot save a property named \"__proto__\"");
185
- data[key] = ser(obj[key]);
186
- }
187
- return data;
188
- }
189
- function serObject(val) {
190
- if (val instanceof Date) return tagged("__Date__", { iso: !Number.isNaN(val.getTime()) ? val.toISOString() : null });
191
- if (val instanceof RegExp) return tagged("__RegExp__", {
192
- source: val.source,
193
- flags: val.flags
194
- });
195
- if (Array.isArray(val)) {
196
- const result = [];
197
- for (let i = 0; i < val.length; i++) result.push(ser(val[i]));
198
- return result;
199
- }
200
- if (val instanceof Map) return tagged("__Map__", { entries: [...val].map(([k, v]) => [ser(k), ser(v)]) });
201
- if (val instanceof Set) return tagged("__Set__", { entries: [...val].map((v) => ser(v)) });
202
- const name = registeredClassName(val);
203
- if (name !== void 0) return tagged(name, serKeys(val));
204
- const data = serKeys(val);
205
- return hasOwn(val, CLASS_TAG) ? tagged("__Object__", data) : data;
206
- }
207
- return ser(value);
208
- }
209
- function deserialize(value) {
210
- function deserKeys(target, data) {
211
- for (const key of Object.keys(data)) setOwn(target, key, deser(data[key]));
212
- return target;
213
- }
214
- function deser(val) {
215
- if (val === null || typeof val !== "object") return val;
216
- if (Array.isArray(val)) return val.map((item) => deser(item));
217
- const obj = val;
218
- if (CLASS_TAG in obj && DATA_TAG in obj) {
219
- const name = obj[CLASS_TAG];
220
- const data = obj[DATA_TAG];
221
- switch (name) {
222
- case "__Date__": return new Date(data.iso === null ? NaN : data.iso);
223
- case "__RegExp__": return new RegExp(data.source, data.flags);
224
- case "__Map__": {
225
- const entries = data.entries;
226
- return new Map(entries.map(([k, v]) => [deser(k), deser(v)]));
227
- }
228
- case "__Set__": return new Set(data.entries.map((v) => deser(v)));
229
- case "__Undefined__": return;
230
- case "__Number__": return SPECIAL_NUMBERS[data.value];
231
- case "__BigInt__": return BigInt(data.value);
232
- case "__Object__": return deserKeys({}, data);
233
- }
234
- const ctor = registry$1.get(name);
235
- if (!ctor) {
236
- console.warn(`spindle: Class "${name}" not registered. Falling back to plain object.`);
237
- return deserKeys({}, data);
238
- }
239
- return deserKeys(Object.create(ctor.prototype), data);
240
- }
241
- if (val instanceof Date || val instanceof RegExp || val instanceof Map || val instanceof Set) return val;
242
- if (registeredClassName(obj) !== void 0) return val;
243
- return deserKeys({}, obj);
244
- }
245
- return deser(value);
246
- }
247
- function isDataRecord(value) {
248
- return typeof value === "object" && value !== null && !Array.isArray(value);
249
- }
250
- /**
251
- * Whether `value` is serialized data that deserialize() can restore: every
252
- * tagged value has the shape serialize() writes for its tag (a valid ISO
253
- * date or null, a compilable RegExp, Map entries as `[key, value]` pairs,
254
- * Set entries as an array, a special number's name, a bigint in decimal,
255
- * class data as an object) and no object has a "__proto__" key, at any
256
- * depth. Use it on data from outside the running story, such as an
257
- * imported save, before storing it. Tags of unregistered classes pass;
258
- * they load as plain objects.
259
- */
260
- function isDeserializable(value) {
261
- const path = /* @__PURE__ */ new Set();
262
- function check(val) {
263
- if (val === null || typeof val !== "object") return true;
264
- if (path.has(val)) return false;
265
- path.add(val);
266
- const ok = checkObject(val);
267
- path.delete(val);
268
- return ok;
269
- }
270
- const checkKeys = (obj) => Object.keys(obj).every((key) => key !== "__proto__" && check(obj[key]));
271
- function checkObject(val) {
272
- if (Array.isArray(val)) return Array.from(val).every(check);
273
- const obj = val;
274
- if (!(CLASS_TAG in obj && DATA_TAG in obj)) return checkKeys(obj);
275
- const name = obj[CLASS_TAG];
276
- const data = obj[DATA_TAG];
277
- if (typeof name !== "string" || !isDataRecord(data)) return false;
278
- switch (name) {
279
- case "__Date__": return data.iso === null || typeof data.iso === "string" && !Number.isNaN(new Date(data.iso).getTime());
280
- case "__RegExp__":
281
- if (typeof data.source !== "string" || typeof data.flags !== "string") return false;
282
- try {
283
- new RegExp(data.source, data.flags);
284
- return true;
285
- } catch {
286
- return false;
287
- }
288
- case "__Map__": return Array.isArray(data.entries) && Array.from(data.entries).every((entry) => Array.isArray(entry) && entry.length === 2 && check(entry[0]) && check(entry[1]));
289
- case "__Set__": return Array.isArray(data.entries) && Array.from(data.entries).every(check);
290
- case "__Undefined__": return true;
291
- case "__Number__": return typeof data.value === "string" && hasOwn(SPECIAL_NUMBERS, data.value);
292
- case "__BigInt__": return typeof data.value === "string" && /^-?\d+$/.test(data.value);
293
- default: return checkKeys(data);
294
- }
295
- }
296
- return check(value);
297
- }
298
- //#endregion
299
- //#region src/saves/types.ts
300
- function isSaveExport(value) {
301
- if (typeof value !== "object" || value === null) return false;
302
- const obj = value;
303
- if (obj.version !== 1 || typeof obj.ifid !== "string") return false;
304
- if (typeof obj.save !== "object" || obj.save === null) return false;
305
- const save = obj.save;
306
- if (typeof save.meta !== "object" || save.meta === null) return false;
307
- if (typeof save.payload !== "object" || save.payload === null) return false;
308
- const meta = save.meta;
309
- if (typeof meta.id !== "string" || typeof meta.passage !== "string") return false;
310
- if (typeof meta.ifid !== "string") return false;
311
- if (typeof meta.playthroughId !== "string") return false;
312
- if (typeof meta.createdAt !== "string") return false;
313
- if (typeof meta.updatedAt !== "string") return false;
314
- if (typeof meta.title !== "string") return false;
315
- return isImportablePayload(save.payload);
316
- }
317
- function isRecord(value) {
318
- return typeof value === "object" && value !== null && !Array.isArray(value);
319
- }
320
- /** Absent, null, or a `{ seed, pull }` PRNG snapshot. */
321
- function isOptionalPRNGSnapshot(value) {
322
- if (value == null) return true;
323
- return isRecord(value) && typeof value.seed === "string" && typeof value.pull === "number" && Number.isInteger(value.pull) && value.pull >= 0;
324
- }
325
- /** A variable container whose values deserialize() can restore. */
326
- function isSerializedVariables(value) {
327
- return isRecord(value) && isDeserializable(value);
328
- }
329
- function isSaveHistoryMoment(value) {
330
- return isRecord(value) && typeof value.passage === "string" && isSerializedVariables(value.variables) && typeof value.timestamp === "number" && isOptionalPRNGSnapshot(value.prng);
331
- }
332
- /**
333
- * Full check of a payload from outside the running story (an imported save):
334
- * every history moment is well formed, `historyIndex` is an integer that
335
- * points at a moment of the payload's passage, and the encoded values (Map,
336
- * Set, Date, RegExp, class instances) in the live variables and every
337
- * moment's variables have the shape deserialize() expects. Anything that
338
- * passes can be stored and later loaded.
339
- */
340
- function isImportablePayload(value) {
341
- if (!isRecord(value)) return false;
342
- if (typeof value.passage !== "string") return false;
343
- if (!isSerializedVariables(value.variables)) return false;
344
- const { history, historyIndex } = value;
345
- if (!Array.isArray(history) || history.length === 0) return false;
346
- if (!Array.from(history).every(isSaveHistoryMoment)) return false;
347
- if (typeof historyIndex !== "number" || !Number.isInteger(historyIndex) || historyIndex < 0 || historyIndex >= history.length) return false;
348
- if (history[historyIndex].passage !== value.passage) return false;
349
- if (value.visitCounts !== void 0 && !isRecord(value.visitCounts)) return false;
350
- if (value.renderCounts !== void 0 && !isRecord(value.renderCounts)) return false;
351
- return isOptionalPRNGSnapshot(value.prng);
352
- }
353
- function isSavePayload(value) {
354
- if (typeof value !== "object" || value === null) return false;
355
- const obj = value;
356
- if (typeof obj.passage !== "string") return false;
357
- if (typeof obj.variables !== "object" || obj.variables === null) return false;
358
- if (!Array.isArray(obj.history) || obj.history.length === 0) return false;
359
- if (typeof obj.historyIndex !== "number") return false;
360
- return true;
361
- }
362
- /** Estimated byte size of a serialized save payload. */
363
- function estimatePayloadBytes(payload) {
364
- return JSON.stringify(payload).length;
365
- }
366
- //#endregion
367
35
  //#region src/js-lexer.ts
368
36
  /** Transient name after `%`: an identifier, so `%3` is never a reference. */
369
37
  var TRANS_NAME_RE = /[A-Za-z_]\w*/y;
@@ -2660,6 +2328,89 @@ function getPRNGPull() {
2660
2328
  return currentPull;
2661
2329
  }
2662
2330
  //#endregion
2331
+ //#region src/utils/namespace.ts
2332
+ /** The variable name no namespace can hold. */
2333
+ var RESERVED_NAME = "__proto__";
2334
+ /**
2335
+ * Why `name` (without its sigil) cannot be a variable name, or undefined if
2336
+ * it can. `label` is the name as the author wrote it (e.g. `$__proto__`).
2337
+ */
2338
+ function variableNameError(name, label = name) {
2339
+ return name === "__proto__" ? `"${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
2340
+ }
2341
+ /** Throw a TypeError if `name` cannot be a variable name. */
2342
+ function checkVariableName(name, label = name) {
2343
+ const error = variableNameError(name, label);
2344
+ if (error) throw new TypeError(`spindle: ${error}`);
2345
+ }
2346
+ /** Whether `obj` holds `key` as an own property. */
2347
+ var hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
2348
+ /** `ns[key]` if `ns` holds it as an own property, else undefined. */
2349
+ var ownValue = (ns, key) => ns && hasOwn(ns, key) ? ns[key] : void 0;
2350
+ /**
2351
+ * Set an own enumerable property. A "__proto__" key is defined as an own
2352
+ * property (as JSON.parse does) instead of replacing the prototype.
2353
+ */
2354
+ function setOwn(target, key, value) {
2355
+ if (key === "__proto__") Object.defineProperty(target, key, {
2356
+ value,
2357
+ enumerable: true,
2358
+ writable: true,
2359
+ configurable: true
2360
+ });
2361
+ else target[key] = value;
2362
+ }
2363
+ /**
2364
+ * A record with no prototype holding the own entries of `sources`, or only
2365
+ * those whose value passes `keep`.
2366
+ */
2367
+ function ownEntries(sources, keep = () => true) {
2368
+ const record = Object.create(null);
2369
+ for (const source of sources) {
2370
+ if (!source) continue;
2371
+ for (const key of Object.keys(source)) {
2372
+ const value = source[key];
2373
+ if (keep(value)) record[key] = value;
2374
+ }
2375
+ }
2376
+ return record;
2377
+ }
2378
+ /** A namespace with no prototype holding the own entries of `sources`. */
2379
+ var createNamespace = (...sources) => ownEntries(sources);
2380
+ /** Whether `value` is an object with no prototype. */
2381
+ var isNamespace = (value) => Object.getPrototypeOf(value) === null;
2382
+ /** `ns` itself if it has no prototype, else a namespace copy of it. */
2383
+ var asNamespace = (ns) => isNamespace(ns) ? ns : createNamespace(ns);
2384
+ /** A namespace copy of `ns` with `key` set to `value`. */
2385
+ function withEntry(ns, key, value) {
2386
+ const next = createNamespace(ns);
2387
+ next[key] = value;
2388
+ return next;
2389
+ }
2390
+ /** An empty namespace that cannot change. */
2391
+ var EMPTY_NAMESPACE = Object.freeze(createNamespace());
2392
+ /** The names whose own entries differ between `a` and `b`, `a`'s first. */
2393
+ function changedNames(a, b) {
2394
+ return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((name) => ownValue(a, name) !== ownValue(b, name));
2395
+ }
2396
+ var isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
2397
+ /** The count of `name` in `counts`: its own entry, or 0. */
2398
+ function countOf(counts, name) {
2399
+ const value = ownValue(counts, name);
2400
+ return isCount(value) ? value : 0;
2401
+ }
2402
+ /**
2403
+ * Counters with no prototype holding the own entries of `source` that are
2404
+ * counts, with `name` (if given) counted once more. Immer copies the
2405
+ * counters on every write anyway, so counting into a fresh copy costs no
2406
+ * more than counting in place.
2407
+ */
2408
+ function createCounts(source, name) {
2409
+ const counts = ownEntries([source], isCount);
2410
+ if (name !== void 0) counts[name] = countOf(source, name) + 1;
2411
+ return counts;
2412
+ }
2413
+ //#endregion
2663
2414
  //#region src/expression.ts
2664
2415
  var FN_CACHE_MAX = 500;
2665
2416
  var fnCache = /* @__PURE__ */ new Map();
@@ -2793,7 +2544,7 @@ function namespaceArg(ns) {
2793
2544
  return ns;
2794
2545
  }
2795
2546
  /** Run compiled code in `namespaces`. */
2796
- function run(fn, ...[variables, temporary, locals = EMPTY_NAMESPACE, transient = EMPTY_NAMESPACE]) {
2547
+ function run$1(fn, ...[variables, temporary, locals = EMPTY_NAMESPACE, transient = EMPTY_NAMESPACE]) {
2797
2548
  return fn(namespaceArg(variables), namespaceArg(temporary), namespaceArg(locals), buildExpressionFns(), namespaceArg(transient));
2798
2549
  }
2799
2550
  /**
@@ -2802,7 +2553,7 @@ function run(fn, ...[variables, temporary, locals = EMPTY_NAMESPACE, transient =
2802
2553
  */
2803
2554
  function evaluate(expr, ...namespaces) {
2804
2555
  const body = `return (${transform(expr)}\n);`;
2805
- return run(getOrCompile(body, body), ...namespaces);
2556
+ return run$1(getOrCompile(body, body), ...namespaces);
2806
2557
  }
2807
2558
  /**
2808
2559
  * Execute statements (no return value).
@@ -2810,16 +2561,16 @@ function evaluate(expr, ...namespaces) {
2810
2561
  */
2811
2562
  function execute(code, ...namespaces) {
2812
2563
  const transformed = transform(code, "statements");
2813
- run(getOrCompile("exec:" + transformed, transformed), ...namespaces);
2564
+ run$1(getOrCompile("exec:" + transformed, transformed), ...namespaces);
2814
2565
  }
2815
2566
  //#endregion
2816
2567
  //#region src/registry.ts
2817
- var registry = new NameMap();
2568
+ var registry$1 = new NameMap();
2818
2569
  function registerMacro(name, component) {
2819
- registry.set(name, component);
2570
+ registry$1.set(name, component);
2820
2571
  }
2821
2572
  function getMacro(name) {
2822
- return registry.get(name);
2573
+ return registry$1.get(name);
2823
2574
  }
2824
2575
  var textRegistry = new NameMap();
2825
2576
  /** Register (or, with undefined, remove) the text form of a macro. */
@@ -3712,9 +3463,14 @@ function MacroError({ macro, error }) {
3712
3463
  children: `{${macro} error${currentSourceLocation()}: ${errorMessage(error)}}`
3713
3464
  });
3714
3465
  }
3715
- /** Log an error in the macro `{label}` (its name, or name and arguments). */
3716
- function logMacroError(label, error) {
3717
- console.error(`spindle: Error in {${label}}${currentSourceLocation()}:`, error);
3466
+ /**
3467
+ * Log an error in the macro `{label}` (its name, or name and arguments) at
3468
+ * `location`, the source location of the macro's passage. Take it before
3469
+ * running the code that failed (see currentSourceLocation): code that
3470
+ * navigates changes the current passage.
3471
+ */
3472
+ function logMacroError(label, error, location = currentSourceLocation()) {
3473
+ console.error(`spindle: Error in {${label}}${location}:`, error);
3718
3474
  }
3719
3475
  /**
3720
3476
  * Run `effect` once, during the macro's first render. If it throws, the
@@ -3725,11 +3481,12 @@ function useRunOnce(macro, rawArgs, effect) {
3725
3481
  const failure = useRef();
3726
3482
  if (failure.current === void 0) {
3727
3483
  failure.current = null;
3484
+ const location = currentSourceLocation();
3728
3485
  try {
3729
3486
  effect();
3730
3487
  } catch (error) {
3731
3488
  failure.current = { error };
3732
- logMacroError(`${macro} ${rawArgs}`, error);
3489
+ logMacroError(`${macro} ${rawArgs}`, error, location);
3733
3490
  }
3734
3491
  }
3735
3492
  if (!failure.current) return null;
@@ -4266,114 +4023,1209 @@ function renderSingleNode(node) {
4266
4023
  }
4267
4024
  }
4268
4025
  /**
4269
- * Render AST nodes without markdown processing.
4270
- * Used for inline containers (button labels, link text) where block-level
4271
- * markdown (lists, headers) would misinterpret content like "-" or "+".
4026
+ * Render AST nodes without markdown processing.
4027
+ * Used for inline containers (button labels, link text) where block-level
4028
+ * markdown (lists, headers) would misinterpret content like "-" or "+".
4029
+ */
4030
+ function renderInlineNodes(nodes) {
4031
+ if (nodes.length === 0) return null;
4032
+ return nodes.map((node) => renderSingleNode(node));
4033
+ }
4034
+ /**
4035
+ * Characters/patterns that trigger CommonMark or GFM transformations.
4036
+ * Any match → fall through to the full micromark pipeline.
4037
+ * False positives (e.g. `-` used as text, not list) just use the slower path.
4038
+ * Includes character references (`&amp;`, `&#123;`) and two-space hard line
4039
+ * breaks, which micromark decodes / turns into <br> (#171), and the openers
4040
+ * of raw HTML comments, processing instructions, CDATA sections and
4041
+ * declarations (`<!`, `<?`), which micromark passes through as HTML. A CR
4042
+ * line ending (`\r\n` or `\r`) also takes the full pipeline, which reads
4043
+ * it as micromark does.
4044
+ */
4045
+ var MARKDOWN_SYNTAX_RE = /[*_`#|~\[>\\\-+=\r]|!\[|\d+[.)]|&#?[a-zA-Z0-9]+;| {2}\n|<[!?]/;
4046
+ /** Two line endings (LF, CRLF or CR) with only whitespace between them. */
4047
+ var BLANK_LINE_RE = /(?:\r\n|\r(?!\n)|\n)\s*[\r\n]/;
4048
+ var PLACEHOLDER_STRIP_RE = /<span data-tw=[0-9a-z]+:\d+><\/span>/g;
4049
+ /** Whitespace that markdown strips at the start and end of a paragraph. */
4050
+ var EDGE_WS = " \r\n";
4051
+ /**
4052
+ * Index of the first character from `from` on (`step` 1) or before `from`
4053
+ * (`step` -1) that is not in `chars`, or where the run of them ends. A loop,
4054
+ * not a regex: `/[ \t]*$/` and the like try each position of a whitespace
4055
+ * run, taking quadratic time on a long run in the middle of a passage.
4056
+ */
4057
+ function skipChars(s, from, step, chars) {
4058
+ let i = from;
4059
+ if (step === 1) while (i < s.length && chars.includes(s[i])) i++;
4060
+ else while (i > 0 && chars.includes(s[i - 1])) i--;
4061
+ return i;
4062
+ }
4063
+ /**
4064
+ * Drop the spaces and tabs around line endings, as markdown does within a
4065
+ * paragraph.
4066
+ */
4067
+ function trimLineEdges(text) {
4068
+ if (!text.includes("\n")) return text;
4069
+ return text.split("\n").map((line, k, lines) => {
4070
+ const start = k === 0 ? 0 : skipChars(line, 0, 1, " ");
4071
+ const end = k === lines.length - 1 ? line.length : skipChars(line, line.length, -1, " ");
4072
+ return line.slice(start, Math.max(start, end));
4073
+ }).join("\n");
4074
+ }
4075
+ /**
4076
+ * Build Preact vnodes from a combined string that contains only plain text
4077
+ * and placeholders. No micromark, no innerHTML. The text is the content of
4078
+ * one paragraph, so like micromark it drops spaces and tabs around line
4079
+ * endings; its edges are already split off by the caller.
4080
+ */
4081
+ function buildPlainTextVnodes(core, ph, unwrapParagraphs) {
4082
+ const children = expandPlaceholderText(trimLineEdges(core), ph).filter((part) => part !== "");
4083
+ return unwrapParagraphs ? /* @__PURE__ */ jsx(Fragment$1, { children }) : h("p", null, ...children);
4084
+ }
4085
+ /**
4086
+ * Render AST nodes with full CommonMark markdown support.
4087
+ *
4088
+ * Combines all nodes into a single markdown document, using placeholder
4089
+ * elements (see Placeholders) for non-text nodes (variables, macros, links,
4090
+ * HTML). This allows
4091
+ * markdown syntax to span across Twine tokens — e.g., markdown tables can
4092
+ * contain {$variables} and {macros} in their cells.
4093
+ *
4094
+ * After micromark processes the combined string, the HTML is parsed back into
4095
+ * Preact VNodes with placeholders replaced by the real rendered components.
4096
+ * Placeholders that micromark escaped as code text are swapped back too (see
4097
+ * PLACEHOLDER_TEXT_RE), so variables in code stay live components.
4098
+ */
4099
+ function renderNodes(nodes, options) {
4100
+ if (nodes.length === 0) return null;
4101
+ if (options?.raw) return renderInlineNodes(nodes);
4102
+ if (!nodes.some((n) => n.type === "text" && (n.value.trim() !== "" || BLANK_LINE_RE.test(n.value)))) return nodes.map((node) => renderSingleNode(node));
4103
+ const components = [];
4104
+ const ph = {
4105
+ nonce: Math.random().toString(36).slice(2, 10) || "0",
4106
+ components,
4107
+ nodes: []
4108
+ };
4109
+ let combined = "";
4110
+ for (let i = 0; i < nodes.length; i++) {
4111
+ const node = nodes[i];
4112
+ if (node.type === "text") {
4113
+ combined += node.value;
4114
+ continue;
4115
+ }
4116
+ const phIdx = components.length;
4117
+ components.push(renderSingleNode(node));
4118
+ ph.nodes.push(node);
4119
+ if (combined.endsWith("\\")) combined += ESCAPE_GUARD;
4120
+ combined += placeholderHtml(ph.nonce, phIdx);
4121
+ }
4122
+ const unwrapParagraphs = !!(options?.nobr || options?.inline);
4123
+ const lead = combined.slice(0, skipChars(combined, 0, 1, EDGE_WS));
4124
+ const trail = lead.length === combined.length ? "" : combined.slice(skipChars(combined, combined.length, -1, EDGE_WS));
4125
+ const core = combined.slice(lead.length, combined.length - trail.length);
4126
+ const edges = (content) => unwrapParagraphs && (lead || trail) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
4127
+ lead,
4128
+ content,
4129
+ trail
4130
+ ] }) : content;
4131
+ const textOnly = combined.replace(PLACEHOLDER_STRIP_RE, "");
4132
+ if (!MARKDOWN_SYNTAX_RE.test(textOnly) && !BLANK_LINE_RE.test(textOnly)) return edges(buildPlainTextVnodes(core, ph, unwrapParagraphs));
4133
+ return edges(htmlToPreact(markdownToHtml(combined, { inline: options?.inline }), ph, unwrapParagraphs));
4134
+ }
4135
+ //#endregion
4136
+ //#region node_modules/devalue/src/constants.js
4137
+ var MAX_ARRAY_LEN = 2 ** 32 - 1;
4138
+ var MAX_ARRAY_INDEX = MAX_ARRAY_LEN - 1;
4139
+ //#endregion
4140
+ //#region node_modules/devalue/src/utils.js
4141
+ var DevalueError = class extends Error {
4142
+ /**
4143
+ * @param {string} message
4144
+ * @param {string[]} keys
4145
+ * @param {any} [value] - The value that failed to be serialized
4146
+ * @param {any} [root] - The root value being serialized
4147
+ */
4148
+ constructor(message, keys, value, root) {
4149
+ super(message);
4150
+ this.name = "DevalueError";
4151
+ this.path = keys.join("");
4152
+ this.value = value;
4153
+ this.root = root;
4154
+ }
4155
+ };
4156
+ /**
4157
+ * Marks a Map key on the path being serialized. The key itself is recorded
4158
+ * right after the marker, so nothing is formatted or allocated per entry.
4159
+ */
4160
+ var MAP_KEY = Symbol();
4161
+ /**
4162
+ * Formats the path recorded on the way to the value being serialized — a
4163
+ * property key, an array index, or `MAP_KEY` followed by a Map key — into
4164
+ * the segments a `DevalueError` joins. Only runs when an error is raised.
4165
+ * @param {any[]} keys
4166
+ * @param {(key: any) => string} format_map_key
4167
+ * @returns {string[]}
4168
+ */
4169
+ function format_path(keys, format_map_key) {
4170
+ /** @type {string[]} */
4171
+ const path = [];
4172
+ for (let i = 0; i < keys.length; i += 1) {
4173
+ const segment = keys[i];
4174
+ if (segment === MAP_KEY) path.push(`.get(${format_map_key(keys[++i])})`);
4175
+ else if (typeof segment === "number") path.push(`[${segment}]`);
4176
+ else path.push(stringify_key(segment));
4177
+ }
4178
+ return path;
4179
+ }
4180
+ var object_proto_names = /* @__PURE__ */ Object.getOwnPropertyNames(Object.prototype).sort().join("\0");
4181
+ /** @param {any} thing */
4182
+ function is_plain_object(thing) {
4183
+ const proto = Object.getPrototypeOf(thing);
4184
+ return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null || Object.getOwnPropertyNames(proto).sort().join("\0") === object_proto_names;
4185
+ }
4186
+ /** @param {any} thing */
4187
+ function get_type(thing) {
4188
+ return Object.prototype.toString.call(thing).slice(8, -1);
4189
+ }
4190
+ /** @param {any} thing */
4191
+ function is_buffer(thing) {
4192
+ return typeof Buffer !== "undefined" && Buffer.isBuffer(thing);
4193
+ }
4194
+ /** @param {string} char */
4195
+ function get_escaped_char(char) {
4196
+ switch (char) {
4197
+ case "\"": return "\\\"";
4198
+ case "<": return "\\u003C";
4199
+ case "\\": return "\\\\";
4200
+ case "\n": return "\\n";
4201
+ case "\r": return "\\r";
4202
+ case " ": return "\\t";
4203
+ case "\b": return "\\b";
4204
+ case "\f": return "\\f";
4205
+ case "\u2028": return "\\u2028";
4206
+ case "\u2029": return "\\u2029";
4207
+ default: return char < " " ? `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}` : "";
4208
+ }
4209
+ }
4210
+ var needs_escape = /["<\\\u0000-\u001f\u2028\u2029]/;
4211
+ /** @param {string} str */
4212
+ function stringify_string(str) {
4213
+ if (!needs_escape.test(str) && str.isWellFormed()) return `"${str}"`;
4214
+ let result = "";
4215
+ let last_pos = 0;
4216
+ const len = str.length;
4217
+ for (let i = 0; i < len; i += 1) {
4218
+ const code = str.charCodeAt(i);
4219
+ if (code >= 55296 && code <= 57343) {
4220
+ if (code <= 56319 && i + 1 < len && (str.charCodeAt(i + 1) & 64512) === 56320) {
4221
+ i += 1;
4222
+ continue;
4223
+ }
4224
+ result += str.slice(last_pos, i) + `\\u${code.toString(16)}`;
4225
+ last_pos = i + 1;
4226
+ continue;
4227
+ }
4228
+ const replacement = get_escaped_char(str[i]);
4229
+ if (replacement) {
4230
+ result += str.slice(last_pos, i) + replacement;
4231
+ last_pos = i + 1;
4232
+ }
4233
+ }
4234
+ return `"${last_pos === 0 ? str : result + str.slice(last_pos)}"`;
4235
+ }
4236
+ var MAX_CACHED_KEY_LENGTH = 64;
4237
+ /** @type {Map<string, string>} */
4238
+ var quoted_keys = /* @__PURE__ */ new Map();
4239
+ /** @param {string} key */
4240
+ function quote_key(key) {
4241
+ if (key.length > MAX_CACHED_KEY_LENGTH) return stringify_string(key);
4242
+ let quoted = quoted_keys.get(key);
4243
+ if (quoted === void 0) {
4244
+ if (quoted_keys.size >= 1024) quoted_keys.clear();
4245
+ quoted = stringify_string(key);
4246
+ quoted_keys.set(key, quoted);
4247
+ }
4248
+ return quoted;
4249
+ }
4250
+ /** @param {Record<string | symbol, any>} object */
4251
+ function enumerable_symbols(object) {
4252
+ const symbols = Object.getOwnPropertySymbols(object);
4253
+ return symbols.length === 0 ? symbols : symbols.filter((symbol) => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable);
4254
+ }
4255
+ var is_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
4256
+ /** @param {string} key */
4257
+ function stringify_key(key) {
4258
+ return is_identifier.test(key) ? "." + key : "[" + JSON.stringify(key) + "]";
4259
+ }
4260
+ /** @param {number} n */
4261
+ function is_valid_array_index(n) {
4262
+ if (!Number.isInteger(n)) return false;
4263
+ if (n < 0) return false;
4264
+ if (n > MAX_ARRAY_INDEX) return false;
4265
+ return true;
4266
+ }
4267
+ /** @param {number} n */
4268
+ function is_valid_array_len(n) {
4269
+ if (!Number.isInteger(n)) return false;
4270
+ if (n < 0) return false;
4271
+ if (n > MAX_ARRAY_LEN) return false;
4272
+ return true;
4273
+ }
4274
+ /** @param {string} s */
4275
+ function is_valid_array_index_string(s) {
4276
+ if (s.length === 0) return false;
4277
+ if (s.length > 1 && s.charCodeAt(0) === 48) return false;
4278
+ for (let i = 0; i < s.length; i++) {
4279
+ const c = s.charCodeAt(i);
4280
+ if (c < 48 || c > 57) return false;
4281
+ }
4282
+ return is_valid_array_index(+s);
4283
+ }
4284
+ /**
4285
+ * Returns the length of the leading run of valid array indices in `keys`.
4286
+ * @param {readonly string[]} keys
4287
+ */
4288
+ function array_index_cut(keys) {
4289
+ for (var i = keys.length - 1; i >= 0; i--) if (is_valid_array_index_string(keys[i])) break;
4290
+ return i + 1;
4291
+ }
4292
+ /**
4293
+ * Finds the populated indices of an array.
4294
+ * @param {unknown[]} array
4295
+ */
4296
+ function valid_array_indices(array) {
4297
+ const keys = Object.keys(array);
4298
+ keys.length = array_index_cut(keys);
4299
+ return keys;
4300
+ }
4301
+ //#endregion
4302
+ //#region node_modules/devalue/src/base64.js
4303
+ /** @type {(array_buffer: ArrayBuffer) => string} */
4304
+ function encode_native(array_buffer) {
4305
+ return new Uint8Array(array_buffer).toBase64();
4306
+ }
4307
+ /** @type {(base64: string) => ArrayBuffer} */
4308
+ function decode_native(base64) {
4309
+ return Uint8Array.fromBase64(base64).buffer;
4310
+ }
4311
+ /** @type {(array_buffer: ArrayBuffer) => string} */
4312
+ function encode_buffer(array_buffer) {
4313
+ return Buffer.from(array_buffer).toString("base64");
4314
+ }
4315
+ /** @type {(base64: string) => ArrayBuffer} */
4316
+ function decode_buffer(base64) {
4317
+ return Uint8Array.from(Buffer.from(base64, "base64")).buffer;
4318
+ }
4319
+ /** @type {(array_buffer: ArrayBuffer) => string} */
4320
+ function encode_legacy(array_buffer) {
4321
+ const array = new Uint8Array(array_buffer);
4322
+ let binary = "";
4323
+ const chunk_size = 32768;
4324
+ for (let i = 0; i < array.length; i += chunk_size) {
4325
+ const chunk = array.subarray(i, i + chunk_size);
4326
+ binary += String.fromCharCode.apply(null, chunk);
4327
+ }
4328
+ return btoa(binary);
4329
+ }
4330
+ /** @type {(base64: string) => ArrayBuffer} */
4331
+ function decode_legacy(base64) {
4332
+ const binary_string = atob(base64);
4333
+ const len = binary_string.length;
4334
+ const array = new Uint8Array(len);
4335
+ for (let i = 0; i < len; i++) array[i] = binary_string.charCodeAt(i);
4336
+ return array.buffer;
4337
+ }
4338
+ var native = typeof Uint8Array.fromBase64 === "function";
4339
+ var buffer = typeof process === "object" && process.versions?.node !== void 0;
4340
+ var encode64 = native ? encode_native : buffer ? encode_buffer : encode_legacy;
4341
+ var decode64 = native ? decode_native : buffer ? decode_buffer : decode_legacy;
4342
+ //#endregion
4343
+ //#region node_modules/devalue/src/operations.js
4344
+ /**
4345
+ * Merges caller-provided operation overrides over the defaults. Iterating the
4346
+ * default keys (rather than the override's own keys) means nullish members
4347
+ * fall back to the default, and inherited members — e.g. from a class
4348
+ * instance — are picked up.
4349
+ *
4350
+ * @template {Record<string, any>} T
4351
+ * @param {T} defaults
4352
+ * @param {Partial<T> | undefined} overrides
4353
+ * @returns {T}
4354
+ */
4355
+ function merge_operations(defaults, overrides) {
4356
+ if (!overrides) return defaults;
4357
+ const merged = {};
4358
+ for (const key of Object.keys(defaults)) merged[key] = overrides[key] ?? defaults[key];
4359
+ return merged;
4360
+ }
4361
+ /** @type {{ kind: 'not-plain' }} */
4362
+ var NOT_PLAIN = /* @__PURE__ */ Object.freeze({ kind: "not-plain" });
4363
+ /** @type {{ kind: 'symbol-keys' }} */
4364
+ var SYMBOL_KEYS = /* @__PURE__ */ Object.freeze({ kind: "symbol-keys" });
4365
+ var default_stringify_operations = /* @__PURE__ */ Object.freeze({
4366
+ identify: (value) => value,
4367
+ typeOf: (value) => value === null ? "null" : typeof value,
4368
+ toPrimitive: (value) => value,
4369
+ tagOf: (value) => get_type(value),
4370
+ isThenable: (value) => typeof value.then === "function",
4371
+ toPromise: (thenable) => Promise.resolve(thenable),
4372
+ unbox: (boxed) => boxed.valueOf(),
4373
+ toISOString: (date) => isNaN(date.getDate()) ? "" : date.toISOString(),
4374
+ toStringValue: (value) => value.toString(),
4375
+ regExpInfo: (regexp) => ({
4376
+ source: regexp.source,
4377
+ flags: regexp.flags
4378
+ }),
4379
+ valuesOf: (set) => set,
4380
+ entriesOf: (map) => map,
4381
+ viewInfo: (view) => {
4382
+ if (is_buffer(view)) view = new Uint8Array(view);
4383
+ return {
4384
+ buffer: view.buffer,
4385
+ byteOffset: view.byteOffset,
4386
+ byteLength: view.byteLength,
4387
+ length: view.length,
4388
+ bufferByteLength: view.buffer.byteLength
4389
+ };
4390
+ },
4391
+ toArrayBuffer: (buffer) => buffer,
4392
+ lengthOf: (array) => array.length,
4393
+ hasOwn: (value, key) => Object.hasOwn(value, key),
4394
+ indicesOf: (array) => valid_array_indices(array),
4395
+ shapeOf: (value) => {
4396
+ if (!is_plain_object(value)) return NOT_PLAIN;
4397
+ if (enumerable_symbols(value).length > 0) return SYMBOL_KEYS;
4398
+ return {
4399
+ kind: Object.getPrototypeOf(value) === null ? "null-proto" : "plain",
4400
+ keys: Object.keys(value)
4401
+ };
4402
+ },
4403
+ get: (value, key) => value[key]
4404
+ });
4405
+ var array_buffer_byte_length = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength")?.get;
4406
+ var shared_array_buffer_byte_length = typeof SharedArrayBuffer === "undefined" ? void 0 : Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, "byteLength")?.get;
4407
+ var default_parse_operations = /* @__PURE__ */ Object.freeze({
4408
+ fromPrimitive: (primitive) => primitive,
4409
+ fromISOString: (iso) => new Date(iso),
4410
+ fromStringValue: (tag, text) => {
4411
+ if (tag === "URL") return new URL(text);
4412
+ if (tag === "URLSearchParams") return new URLSearchParams(text);
4413
+ return Temporal[tag.slice(9)].from(text);
4414
+ },
4415
+ fromArrayBuffer: (buffer) => buffer,
4416
+ fromRegExpInfo: (source, flags) => new RegExp(source, flags),
4417
+ fromViewInfo: (tag, buffer, byteOffset, length) => {
4418
+ try {
4419
+ array_buffer_byte_length.call(buffer);
4420
+ } catch (error) {
4421
+ if (!shared_array_buffer_byte_length) throw error;
4422
+ shared_array_buffer_byte_length.call(buffer);
4423
+ }
4424
+ const Constructor = globalThis[tag];
4425
+ return byteOffset !== void 0 ? new Constructor(buffer, byteOffset, length) : new Constructor(buffer);
4426
+ },
4427
+ box: (value) => Object(value),
4428
+ createArray: (length) => new Array(length),
4429
+ createSparseArray: (length) => {
4430
+ /** @type {any[]} */
4431
+ const array = [];
4432
+ array[MAX_ARRAY_INDEX] = void 0;
4433
+ delete array[MAX_ARRAY_INDEX];
4434
+ array.length = length;
4435
+ return array;
4436
+ },
4437
+ createObject: () => ({}),
4438
+ createNullPrototypeObject: () => Object.create(null),
4439
+ createSet: () => /* @__PURE__ */ new Set(),
4440
+ createMap: () => /* @__PURE__ */ new Map(),
4441
+ set: (target, key, value) => {
4442
+ target[key] = value;
4443
+ },
4444
+ addValue: (set, value) => {
4445
+ set.add(value);
4446
+ },
4447
+ addEntry: (map, key, value) => {
4448
+ map.set(key, value);
4449
+ }
4450
+ });
4451
+ //#endregion
4452
+ //#region node_modules/devalue/src/parse.js
4453
+ function invalid$1() {
4454
+ throw new Error("Invalid input");
4455
+ }
4456
+ /**
4457
+ * Revive a value serialized with `devalue.stringify`
4458
+ * @param {string} serialized
4459
+ * @param {Record<string, (value: any) => any>} [revivers]
4460
+ * @param {import('./types.js').ParseOptions} [options]
4461
+ */
4462
+ function parse(serialized, revivers, options) {
4463
+ return unflatten(JSON.parse(serialized), revivers, options);
4464
+ }
4465
+ /**
4466
+ * Revive a value flattened with `devalue.stringify`
4467
+ * @param {number | any[]} parsed
4468
+ * @param {Record<string, (value: any) => any>} [revivers]
4469
+ * @param {import('./types.js').ParseOptions} [options]
4470
+ */
4471
+ function unflatten(parsed, revivers, options) {
4472
+ /** @type {import('./types.js').ParseOperations} */
4473
+ const ops = merge_operations(default_parse_operations, options?.operations);
4474
+ if (typeof parsed === "number") return hydrate(parsed, true);
4475
+ if (!Array.isArray(parsed) || parsed.length === 0) invalid$1();
4476
+ const values = parsed;
4477
+ const hydrated = Array(values.length);
4478
+ /**
4479
+ * A set of values currently being hydrated with custom revivers,
4480
+ * used to detect invalid cyclical dependencies
4481
+ * @type {Set<number> | null}
4482
+ */
4483
+ let hydrating = null;
4484
+ /**
4485
+ * @param {number} index
4486
+ * @returns {any}
4487
+ */
4488
+ function get_raw(index) {
4489
+ if (!is_valid_array_index(index) || index >= values.length) invalid$1();
4490
+ return values[index];
4491
+ }
4492
+ /**
4493
+ * @param {number} index
4494
+ * @returns {any}
4495
+ */
4496
+ function hydrate(index, standalone = false) {
4497
+ if (index === -1) return ops.fromPrimitive(void 0);
4498
+ if (index === -3) return ops.fromPrimitive(NaN);
4499
+ if (index === -4) return ops.fromPrimitive(Infinity);
4500
+ if (index === -5) return ops.fromPrimitive(-Infinity);
4501
+ if (index === -6) return ops.fromPrimitive(-0);
4502
+ if (standalone || typeof index !== "number") invalid$1();
4503
+ if (index in hydrated) return hydrated[index];
4504
+ if (index >= values.length) invalid$1();
4505
+ const value = values[index];
4506
+ if (!value || typeof value !== "object") hydrated[index] = ops.fromPrimitive(value);
4507
+ else if (Array.isArray(value)) if (typeof value[0] === "string") {
4508
+ const type = value[0];
4509
+ const reviver = revivers && Object.hasOwn(revivers, type) ? revivers[type] : void 0;
4510
+ if (reviver) {
4511
+ let i = value[1];
4512
+ if (typeof i !== "number") i = values.push(value[1]) - 1;
4513
+ if (Object.hasOwn(hydrated, i)) return hydrated[index] = reviver(hydrated[i]);
4514
+ hydrating ??= /* @__PURE__ */ new Set();
4515
+ if (hydrating.has(i)) throw new Error("Invalid circular reference");
4516
+ hydrating.add(i);
4517
+ hydrated[index] = reviver(hydrate(i));
4518
+ hydrating.delete(i);
4519
+ return hydrated[index];
4520
+ }
4521
+ switch (type) {
4522
+ case "Date":
4523
+ hydrated[index] = ops.fromISOString(value[1]);
4524
+ break;
4525
+ case "Set":
4526
+ const set = ops.createSet();
4527
+ hydrated[index] = set;
4528
+ for (let i = 1; i < value.length; i += 1) ops.addValue(set, hydrate(value[i]));
4529
+ break;
4530
+ case "Map":
4531
+ const map = ops.createMap();
4532
+ hydrated[index] = map;
4533
+ for (let i = 1; i < value.length; i += 2) ops.addEntry(map, hydrate(value[i]), hydrate(value[i + 1]));
4534
+ break;
4535
+ case "RegExp":
4536
+ hydrated[index] = ops.fromRegExpInfo(value[1], value[2]);
4537
+ break;
4538
+ case "Object": {
4539
+ const wrapped_index = value[1];
4540
+ if (!(wrapped_index === -3 || wrapped_index === -4 || wrapped_index === -5 || wrapped_index === -6)) {
4541
+ const wrapped = get_raw(wrapped_index);
4542
+ const is_bigint = Array.isArray(wrapped) && wrapped[0] === "BigInt";
4543
+ if ((wrapped === void 0 || typeof wrapped === "object") && !is_bigint) invalid$1();
4544
+ }
4545
+ hydrated[index] = ops.box(hydrate(wrapped_index));
4546
+ break;
4547
+ }
4548
+ case "BigInt":
4549
+ hydrated[index] = ops.fromPrimitive(BigInt(value[1]));
4550
+ break;
4551
+ case "null":
4552
+ const obj = ops.createNullPrototypeObject();
4553
+ hydrated[index] = obj;
4554
+ for (let i = 1; i < value.length; i += 2) {
4555
+ const key = value[i];
4556
+ if (typeof key !== "string") throw new Error("Cannot parse an object with a non-string key");
4557
+ if (key === "__proto__") throw new Error("Cannot parse an object with a `__proto__` property");
4558
+ ops.set(obj, key, hydrate(value[i + 1]));
4559
+ }
4560
+ break;
4561
+ case "Int8Array":
4562
+ case "Uint8Array":
4563
+ case "Uint8ClampedArray":
4564
+ case "Int16Array":
4565
+ case "Uint16Array":
4566
+ case "Float16Array":
4567
+ case "Int32Array":
4568
+ case "Uint32Array":
4569
+ case "Float32Array":
4570
+ case "Float64Array":
4571
+ case "BigInt64Array":
4572
+ case "BigUint64Array":
4573
+ case "DataView": {
4574
+ const buffer_index = value[1];
4575
+ const raw = get_raw(buffer_index);
4576
+ if (!Array.isArray(raw) || raw[0] !== "ArrayBuffer") invalid$1();
4577
+ const buffer = hydrate(buffer_index);
4578
+ hydrated[index] = ops.fromViewInfo(type, buffer, value[2], value[3]);
4579
+ break;
4580
+ }
4581
+ case "ArrayBuffer": {
4582
+ const base64 = value[1];
4583
+ if (typeof base64 !== "string") throw new Error("Invalid ArrayBuffer encoding");
4584
+ hydrated[index] = ops.fromArrayBuffer(decode64(base64));
4585
+ break;
4586
+ }
4587
+ case "URL":
4588
+ case "URLSearchParams":
4589
+ case "Temporal.Duration":
4590
+ case "Temporal.Instant":
4591
+ case "Temporal.PlainDate":
4592
+ case "Temporal.PlainTime":
4593
+ case "Temporal.PlainDateTime":
4594
+ case "Temporal.PlainMonthDay":
4595
+ case "Temporal.PlainYearMonth":
4596
+ case "Temporal.ZonedDateTime":
4597
+ hydrated[index] = ops.fromStringValue(type, value[1]);
4598
+ break;
4599
+ default: throw new Error(`Unknown type ${type}`);
4600
+ }
4601
+ } else if (value[0] === -7) {
4602
+ const len = value[1];
4603
+ if (!is_valid_array_len(len)) invalid$1();
4604
+ const array = ops.createSparseArray(len);
4605
+ hydrated[index] = array;
4606
+ for (let i = 2; i < value.length; i += 2) {
4607
+ const idx = value[i];
4608
+ if (!is_valid_array_index(idx) || idx >= len) invalid$1();
4609
+ ops.set(array, idx, hydrate(value[i + 1]));
4610
+ }
4611
+ } else {
4612
+ const array = ops.createArray(value.length);
4613
+ hydrated[index] = array;
4614
+ for (let i = 0; i < value.length; i += 1) {
4615
+ const n = value[i];
4616
+ if (n === -2) continue;
4617
+ ops.set(array, i, hydrate(n));
4618
+ }
4619
+ }
4620
+ else {
4621
+ const object = ops.createObject();
4622
+ hydrated[index] = object;
4623
+ for (const key of Object.keys(value)) {
4624
+ if (key === "__proto__") throw new Error("Cannot parse an object with a `__proto__` property");
4625
+ ops.set(object, key, hydrate(value[key]));
4626
+ }
4627
+ }
4628
+ return hydrated[index];
4629
+ }
4630
+ return hydrate(0);
4631
+ }
4632
+ //#endregion
4633
+ //#region node_modules/devalue/src/stringify.js
4634
+ /**
4635
+ * Turn a value into a JSON string that can be parsed with `devalue.parse`
4636
+ * @param {any} value
4637
+ * @param {Record<string, (value: any) => any>} [reducers]
4638
+ * @param {import('./types.js').StringifyOptions} [options]
4639
+ */
4640
+ function stringify(value, reducers, options) {
4641
+ const stringified = run(false, value, reducers, options);
4642
+ return typeof stringified === "string" ? stringified : `[${stringified.join(",")}]`;
4643
+ }
4644
+ /**
4645
+ * @param {boolean} async
4646
+ * @param {any} value
4647
+ * @param {Record<string, (value: any) => any>} [reducers]
4648
+ * @param {import('./types.js').StringifyOptions} [options]
4649
+ */
4650
+ function run(async, value, reducers, options) {
4651
+ const ops = merge_operations(default_stringify_operations, options?.operations);
4652
+ /** @type {any[]} */
4653
+ const stringified = [];
4654
+ /** @type {Map<any, number>} */
4655
+ const indexes = /* @__PURE__ */ new Map();
4656
+ /** @type {Array<{ key: string, fn: (value: any) => any }>} */
4657
+ const custom = [];
4658
+ if (reducers) for (const key of Object.getOwnPropertyNames(reducers)) custom.push({
4659
+ key,
4660
+ fn: reducers[key]
4661
+ });
4662
+ /** @type {any[]} */
4663
+ let keys = [];
4664
+ let p = 0;
4665
+ /**
4666
+ * @param {string} message
4667
+ * @param {any} thing
4668
+ * @returns {never}
4669
+ */
4670
+ function error(message, thing) {
4671
+ throw new DevalueError(message, format_path(keys, (key) => {
4672
+ const key_type = ops.typeOf(key);
4673
+ return key_type !== "object" && key_type !== "function" && key_type !== "symbol" ? stringify_primitive(ops.toPrimitive(key)) : "...";
4674
+ }), thing, value);
4675
+ }
4676
+ /**
4677
+ * @param {any} thing
4678
+ * @param {number} [index]
4679
+ */
4680
+ function flatten(thing, index) {
4681
+ const type = ops.typeOf(thing);
4682
+ if (type === "undefined") return -1;
4683
+ /** @type {number | undefined} */
4684
+ let number;
4685
+ if (type === "number") {
4686
+ number = ops.toPrimitive(thing);
4687
+ if (Number.isNaN(number)) return -3;
4688
+ if (number === Infinity) return -4;
4689
+ if (number === -Infinity) return -5;
4690
+ if (number === 0 && 1 / number < 0) return -6;
4691
+ }
4692
+ const id = ops.identify(thing);
4693
+ if (indexes.has(id)) return indexes.get(id);
4694
+ index ??= p++;
4695
+ indexes.set(id, index);
4696
+ for (const { key, fn } of custom) {
4697
+ const value = fn(thing);
4698
+ if (value) {
4699
+ stringified[index] = `["${key}",${flatten(value)}]`;
4700
+ return index;
4701
+ }
4702
+ }
4703
+ if (type === "function") error(`Cannot stringify a function`, thing);
4704
+ else if (type === "symbol") error(`Cannot stringify a Symbol primitive`, thing);
4705
+ /** @type {string | Promise<any>} */
4706
+ let str = "";
4707
+ if (type !== "object") str = stringify_primitive(type === "number" ? number : ops.toPrimitive(thing));
4708
+ else if (ops.isThenable(thing)) {
4709
+ if (!async) error(`Cannot stringify a Promise or thenable — use stringifyAsync instead`, thing);
4710
+ const prev_keys = keys.slice();
4711
+ str = ops.toPromise(thing).then((value) => {
4712
+ keys = prev_keys;
4713
+ const i = flatten(value, index);
4714
+ if (i < 0) stringified[index] = i;
4715
+ });
4716
+ str.catch(() => {});
4717
+ } else {
4718
+ const tag = ops.tagOf(thing);
4719
+ switch (tag) {
4720
+ case "Number":
4721
+ case "String":
4722
+ case "Boolean":
4723
+ case "BigInt":
4724
+ str = `["Object",${flatten(ops.unbox(thing))}]`;
4725
+ break;
4726
+ case "Date":
4727
+ str = `["Date","${ops.toISOString(thing)}"]`;
4728
+ break;
4729
+ case "URL":
4730
+ str = `["URL",${stringify_string(ops.toStringValue(thing))}]`;
4731
+ break;
4732
+ case "URLSearchParams":
4733
+ str = `["URLSearchParams",${stringify_string(ops.toStringValue(thing))}]`;
4734
+ break;
4735
+ case "RegExp":
4736
+ const { source, flags } = ops.regExpInfo(thing);
4737
+ str = flags ? `["RegExp",${stringify_string(source)},"${flags}"]` : `["RegExp",${stringify_string(source)}]`;
4738
+ break;
4739
+ case "Array": {
4740
+ let mostly_dense = false;
4741
+ const length = ops.lengthOf(thing);
4742
+ str = "[";
4743
+ for (let i = 0; i < length; i += 1) {
4744
+ if (i > 0) str += ",";
4745
+ if (ops.hasOwn(thing, i)) {
4746
+ keys.push(i);
4747
+ str += flatten(ops.get(thing, i));
4748
+ keys.pop();
4749
+ } else if (mostly_dense) str += -2;
4750
+ else {
4751
+ const populated_keys = ops.indicesOf(thing);
4752
+ const population = populated_keys.length;
4753
+ const d = String(length).length;
4754
+ if ((length - population) * 3 > 4 + d + population * (d + 1)) {
4755
+ str = "[-7," + length;
4756
+ for (let j = 0; j < populated_keys.length; j++) {
4757
+ const key = populated_keys[j];
4758
+ keys.push(+key);
4759
+ str += "," + key + "," + flatten(ops.get(thing, key));
4760
+ keys.pop();
4761
+ }
4762
+ break;
4763
+ } else {
4764
+ mostly_dense = true;
4765
+ str += -2;
4766
+ }
4767
+ }
4768
+ }
4769
+ str += "]";
4770
+ break;
4771
+ }
4772
+ case "Set":
4773
+ str = "[\"Set\"";
4774
+ for (const value of ops.valuesOf(thing)) str += `,${flatten(value)}`;
4775
+ str += "]";
4776
+ break;
4777
+ case "Map":
4778
+ str = "[\"Map\"";
4779
+ for (const [key, value] of ops.entriesOf(thing)) {
4780
+ keys.push(MAP_KEY, key);
4781
+ str += `,${flatten(key)},${flatten(value)}`;
4782
+ keys.pop();
4783
+ keys.pop();
4784
+ }
4785
+ str += "]";
4786
+ break;
4787
+ case "Int8Array":
4788
+ case "Uint8Array":
4789
+ case "Uint8ClampedArray":
4790
+ case "Int16Array":
4791
+ case "Uint16Array":
4792
+ case "Float16Array":
4793
+ case "Int32Array":
4794
+ case "Uint32Array":
4795
+ case "Float32Array":
4796
+ case "Float64Array":
4797
+ case "BigInt64Array":
4798
+ case "BigUint64Array": {
4799
+ const info = ops.viewInfo(thing);
4800
+ str = "[\"" + tag + "\"," + flatten(info.buffer);
4801
+ if (info.byteLength !== info.bufferByteLength) str += `,${info.byteOffset},${info.length}`;
4802
+ str += "]";
4803
+ break;
4804
+ }
4805
+ case "DataView": {
4806
+ const info = ops.viewInfo(thing);
4807
+ str = "[\"" + tag + "\"," + flatten(info.buffer);
4808
+ if (info.byteLength !== info.bufferByteLength) str += `,${info.byteOffset},${info.byteLength}`;
4809
+ str += "]";
4810
+ break;
4811
+ }
4812
+ case "ArrayBuffer":
4813
+ str = `["ArrayBuffer","${encode64(ops.toArrayBuffer(thing))}"]`;
4814
+ break;
4815
+ case "Temporal.Duration":
4816
+ case "Temporal.Instant":
4817
+ case "Temporal.PlainDate":
4818
+ case "Temporal.PlainTime":
4819
+ case "Temporal.PlainDateTime":
4820
+ case "Temporal.PlainMonthDay":
4821
+ case "Temporal.PlainYearMonth":
4822
+ case "Temporal.ZonedDateTime":
4823
+ str = `["${tag}",${stringify_string(ops.toStringValue(thing))}]`;
4824
+ break;
4825
+ default: {
4826
+ const shape = ops.shapeOf(thing);
4827
+ if (shape.kind === "not-plain") error(`Cannot stringify arbitrary non-POJOs`, thing);
4828
+ if (shape.kind === "symbol-keys") error(`Cannot stringify POJOs with symbolic keys`, thing);
4829
+ if (shape.kind === "null-proto") {
4830
+ str = "[\"null\"";
4831
+ for (const key of shape.keys) {
4832
+ if (key === "__proto__") error(`Cannot stringify objects with __proto__ keys`, thing);
4833
+ keys.push(key);
4834
+ str += `,${quote_key(key)},${flatten(ops.get(thing, key))}`;
4835
+ keys.pop();
4836
+ }
4837
+ str += "]";
4838
+ } else {
4839
+ str = "{";
4840
+ let started = false;
4841
+ for (const key of shape.keys) {
4842
+ if (key === "__proto__") error(`Cannot stringify objects with __proto__ keys`, thing);
4843
+ if (started) str += ",";
4844
+ started = true;
4845
+ keys.push(key);
4846
+ str += `${quote_key(key)}:${flatten(ops.get(thing, key))}`;
4847
+ keys.pop();
4848
+ }
4849
+ str += "}";
4850
+ }
4851
+ }
4852
+ }
4853
+ }
4854
+ stringified[index] = str;
4855
+ return index;
4856
+ }
4857
+ const index = flatten(value);
4858
+ if (index < 0) return `${index}`;
4859
+ return stringified;
4860
+ }
4861
+ /**
4862
+ * @param {any} thing
4863
+ * @returns {string}
4864
+ */
4865
+ function stringify_primitive(thing) {
4866
+ const type = typeof thing;
4867
+ if (type === "string") return stringify_string(thing);
4868
+ if (thing === void 0) return (-1).toString();
4869
+ if (thing === 0 && 1 / thing < 0) return (-6).toString();
4870
+ if (type === "bigint") return `["BigInt","${thing}"]`;
4871
+ return String(thing);
4872
+ }
4873
+ //#endregion
4874
+ //#region src/class-registry.ts
4875
+ var registry = /* @__PURE__ */ new Map();
4876
+ var ctorToName = /* @__PURE__ */ new Map();
4877
+ function registerClass(name, ctor) {
4878
+ registry.set(name, ctor);
4879
+ ctorToName.set(ctor, name);
4880
+ }
4881
+ /**
4882
+ * The registered name of the class `value` is an instance of, or undefined.
4883
+ * Looks the constructor up on the prototype, so an own property named
4884
+ * "constructor" (plain data) cannot hide or fake the class.
4885
+ */
4886
+ function registeredClassName(value) {
4887
+ const proto = Object.getPrototypeOf(value);
4888
+ return proto === null ? void 0 : ctorToName.get(proto.constructor);
4889
+ }
4890
+ var CLASS_PREFIX = "c:";
4891
+ var ERROR_PREFIX = "E:";
4892
+ var SYMBOL_TAG = "S";
4893
+ var KEYS_TAG = "K";
4894
+ var BUILTIN_ERRORS = new Map([
4895
+ Error,
4896
+ EvalError,
4897
+ RangeError,
4898
+ ReferenceError,
4899
+ SyntaxError,
4900
+ TypeError,
4901
+ URIError,
4902
+ globalThis.AggregateError
4903
+ ].map((ctor) => [ctor.prototype, ctor.name]));
4904
+ var ERROR_CTORS = new Map([...BUILTIN_ERRORS].map(([proto, name]) => [name, proto.constructor]));
4905
+ /** Error keys that are own but not enumerable on a live error. */
4906
+ var ERROR_HIDDEN_KEYS$1 = [
4907
+ "message",
4908
+ "cause",
4909
+ "errors"
4910
+ ];
4911
+ var isObject = (v) => typeof v === "object" && v !== null;
4912
+ var defineData = (target, key, value) => {
4913
+ Object.defineProperty(target, key, {
4914
+ value,
4915
+ enumerable: true,
4916
+ writable: true,
4917
+ configurable: true
4918
+ });
4919
+ };
4920
+ /**
4921
+ * Own enumerable keys of `value`, as a plain object; for an error also its
4922
+ * message, cause and (AggregateError) errors, which are not enumerable.
4923
+ */
4924
+ function ownData(value) {
4925
+ const data = {};
4926
+ if (value instanceof Error) {
4927
+ for (const key of ERROR_HIDDEN_KEYS$1) if (hasOwn(value, key)) defineData(data, key, value[key]);
4928
+ }
4929
+ for (const key of Object.keys(value)) defineData(data, key, value[key]);
4930
+ return data;
4931
+ }
4932
+ /** Make an error's message, cause and errors non-enumerable again. */
4933
+ function hideErrorKeys(value) {
4934
+ for (const key of ERROR_HIDDEN_KEYS$1) if (hasOwn(value, key)) Object.defineProperty(value, key, { enumerable: false });
4935
+ }
4936
+ /** Whether JSON.stringify writes `key` as a one-character escape sequence. */
4937
+ var isEscapedOneCharKey = (key) => key.length === 1 && JSON.stringify(key).length > 3;
4938
+ function reducers() {
4939
+ const out = {};
4940
+ for (const [name, ctor] of registry) out[CLASS_PREFIX + name] = (v) => isObject(v) && Object.getPrototypeOf(v) === ctor.prototype && ownData(v);
4941
+ for (const [proto, name] of BUILTIN_ERRORS) out[ERROR_PREFIX + name] = (v) => isObject(v) && Object.getPrototypeOf(v) === proto && ownData(v);
4942
+ out[SYMBOL_TAG] = (v) => {
4943
+ if (typeof v !== "symbol") return false;
4944
+ const key = Symbol.keyFor(v);
4945
+ return key !== void 0 && [key];
4946
+ };
4947
+ out[KEYS_TAG] = (v) => {
4948
+ if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
4949
+ const keys = Object.keys(v);
4950
+ if (!keys.some(isEscapedOneCharKey)) return false;
4951
+ if (keys.includes("__proto__")) throw new TypeError("spindle: Cannot save a property named \"__proto__\"");
4952
+ return keys.flatMap((k) => [k, v[k]]);
4953
+ };
4954
+ return out;
4955
+ }
4956
+ /** Restore a registered class instance in place, so cycles through it hold. */
4957
+ function reviveClass(name) {
4958
+ return (data) => {
4959
+ if (!isObject(data) || Object.getPrototypeOf(data) !== Object.prototype) {
4960
+ if (isObject(data) && registeredClassName(data) === name) return data;
4961
+ throw new TypeError(`spindle: Malformed data for class "${name}"`);
4962
+ }
4963
+ const ctor = registry.get(name);
4964
+ if (!ctor) throw new TypeError(`spindle: The save holds an instance of class "${name}", which is not registered`);
4965
+ Object.setPrototypeOf(data, ctor.prototype);
4966
+ if (data instanceof Error) hideErrorKeys(data);
4967
+ return data;
4968
+ };
4969
+ }
4970
+ /** Restore a built-in error in place, like a class instance. */
4971
+ function reviveError(name) {
4972
+ const ctor = ERROR_CTORS.get(name);
4973
+ if (!ctor) return void 0;
4974
+ return (data) => {
4975
+ if (Object.getPrototypeOf(data) === ctor.prototype) return data;
4976
+ if (!isObject(data) || Object.getPrototypeOf(data) !== Object.prototype) throw new TypeError(`spindle: Malformed data for "${name}"`);
4977
+ Object.setPrototypeOf(data, ctor.prototype);
4978
+ hideErrorKeys(data);
4979
+ return data;
4980
+ };
4981
+ }
4982
+ /**
4983
+ * The object of a `K` entry list. A cycle through the object revives it
4984
+ * before its list is complete (devalue holds unread entries as holes), so
4985
+ * the object is kept per list and filled with what the list holds so far.
4986
+ */
4987
+ var keyedObjects = /* @__PURE__ */ new WeakMap();
4988
+ function reviveKeyed(entries) {
4989
+ if (!Array.isArray(entries) || entries.length % 2 !== 0) throw new TypeError("spindle: Malformed object entries");
4990
+ let obj = keyedObjects.get(entries);
4991
+ if (!obj) keyedObjects.set(entries, obj = {});
4992
+ for (let i = 0; i < entries.length; i += 2) {
4993
+ if (!hasOwn(entries, i)) break;
4994
+ const key = entries[i];
4995
+ if (typeof key !== "string" || key === "__proto__") throw new TypeError("spindle: Malformed object entries");
4996
+ defineData(obj, key, hasOwn(entries, i + 1) ? entries[i + 1] : void 0);
4997
+ }
4998
+ return obj;
4999
+ }
5000
+ /** Revivers for every tag a save may hold. */
5001
+ var revivers = new Proxy({}, {
5002
+ getOwnPropertyDescriptor(_, key) {
5003
+ const fn = reviverFor(key);
5004
+ return fn && {
5005
+ value: fn,
5006
+ enumerable: true,
5007
+ configurable: true
5008
+ };
5009
+ },
5010
+ get: (_, key) => reviverFor(key)
5011
+ });
5012
+ function reviverFor(key) {
5013
+ if (typeof key !== "string") return void 0;
5014
+ if (key === SYMBOL_TAG) return (data) => {
5015
+ if (!Array.isArray(data) || typeof data[0] !== "string") throw new TypeError("spindle: Malformed symbol");
5016
+ return Symbol.for(data[0]);
5017
+ };
5018
+ if (key === KEYS_TAG) return reviveKeyed;
5019
+ if (key.startsWith(CLASS_PREFIX)) return reviveClass(key.slice(2));
5020
+ if (key.startsWith(ERROR_PREFIX)) return reviveError(key.slice(2));
5021
+ }
5022
+ /** A clearer message for devalue's refusals, with the path to the value. */
5023
+ function saveError(err, value) {
5024
+ if (!(err instanceof Error) || err.name !== "DevalueError") return err instanceof Error ? err : new Error(String(err));
5025
+ const { path, value: what } = err;
5026
+ const at = path ? ` (at ${path})` : "";
5027
+ let reason = err.message;
5028
+ if (typeof what === "function") reason = "Cannot save a function";
5029
+ else if (isObject(what) && /non-POJO/.test(err.message)) reason = `Cannot save an instance of class "${Object.getPrototypeOf(what)?.constructor?.name ?? "?"}", which is not registered (see Story.registerClass)`;
5030
+ else if (typeof what === "symbol") reason = "Cannot save a unique symbol (only Symbol.for() symbols)";
5031
+ else if (/symbolic keys/.test(err.message)) reason = "Cannot save an object with symbol keys";
5032
+ else if (/__proto__/.test(err.message)) reason = "Cannot save a property named \"__proto__\"";
5033
+ return /* @__PURE__ */ new TypeError(`spindle: ${reason}${at}`);
5034
+ }
5035
+ /**
5036
+ * Turn a story value into JSON text that deserialize() restores (saves,
5037
+ * exports, the session). Throws, naming the path to the value, on what a
5038
+ * save cannot hold: functions, unique symbols, symbol keys, instances of
5039
+ * unregistered classes, and a property named "__proto__".
5040
+ */
5041
+ function serialize(value) {
5042
+ try {
5043
+ return stringify(value, reducers());
5044
+ } catch (err) {
5045
+ throw saveError(err, value);
5046
+ }
5047
+ }
5048
+ /**
5049
+ * Restore a value serialize() wrote. Throws on malformed text, and on an
5050
+ * instance of a class that is not registered.
4272
5051
  */
4273
- function renderInlineNodes(nodes) {
4274
- if (nodes.length === 0) return null;
4275
- return nodes.map((node) => renderSingleNode(node));
5052
+ function deserialize(text) {
5053
+ return parse(text, revivers);
4276
5054
  }
5055
+ /** A save, export or session from a version of Spindle this one cannot read. */
5056
+ var IncompatibleSaveError = class extends Error {
5057
+ constructor() {
5058
+ super("This save was made by an incompatible version of Spindle");
5059
+ this.name = "IncompatibleSaveError";
5060
+ }
5061
+ };
4277
5062
  /**
4278
- * Characters/patterns that trigger CommonMark or GFM transformations.
4279
- * Any match → fall through to the full micromark pipeline.
4280
- * False positives (e.g. `-` used as text, not list) just use the slower path.
4281
- * Includes character references (`&amp;`, `&#123;`) and two-space hard line
4282
- * breaks, which micromark decodes / turns into <br> (#171), and the openers
4283
- * of raw HTML comments, processing instructions, CDATA sections and
4284
- * declarations (`<!`, `<?`), which micromark passes through as HTML. A CR
4285
- * line ending (`\r\n` or `\r`) also takes the full pipeline, which reads
4286
- * it as micromark does.
5063
+ * Migrations of the stored data: MIGRATIONS[v] turns the `data` of format
5064
+ * version v into that of version v + 1. Version 1 is the first versioned
5065
+ * format; data from before it has no version and is refused.
4287
5066
  */
4288
- var MARKDOWN_SYNTAX_RE = /[*_`#|~\[>\\\-+=\r]|!\[|\d+[.)]|&#?[a-zA-Z0-9]+;| {2}\n|<[!?]/;
4289
- /** Two line endings (LF, CRLF or CR) with only whitespace between them. */
4290
- var BLANK_LINE_RE = /(?:\r\n|\r(?!\n)|\n)\s*[\r\n]/;
4291
- var PLACEHOLDER_STRIP_RE = /<span data-tw=[0-9a-z]+:\d+><\/span>/g;
4292
- /** Whitespace that markdown strips at the start and end of a paragraph. */
4293
- var EDGE_WS = " \r\n";
5067
+ var MIGRATIONS = {};
5068
+ /** Throw an IncompatibleSaveError unless this build reads format `version`. */
5069
+ function checkFormatVersion(version) {
5070
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1 || version > 1) throw new IncompatibleSaveError();
5071
+ }
5072
+ /** Bring `data` of format `version` up to SAVE_FORMAT_VERSION. */
5073
+ function migrate(version, data) {
5074
+ checkFormatVersion(version);
5075
+ let current = data;
5076
+ for (let v = version; v < 1; v++) {
5077
+ const step = MIGRATIONS[v];
5078
+ if (!step) throw new IncompatibleSaveError();
5079
+ current = step(current);
5080
+ }
5081
+ return current;
5082
+ }
4294
5083
  /**
4295
- * Index of the first character from `from` on (`step` 1) or before `from`
4296
- * (`step` -1) that is not in `chars`, or where the run of them ends. A loop,
4297
- * not a regex: `/[ \t]*$/` and the like try each position of a whitespace
4298
- * run, taking quadratic time on a long run in the middle of a passage.
5084
+ * The passage counters as a Map: a passage may be named "__proto__", which
5085
+ * serialize() refuses as an object key (as every story-state key).
4299
5086
  */
4300
- function skipChars(s, from, step, chars) {
4301
- let i = from;
4302
- if (step === 1) while (i < s.length && chars.includes(s[i])) i++;
4303
- else while (i > 0 && chars.includes(s[i - 1])) i--;
4304
- return i;
4305
- }
5087
+ var countsToMap = (counts) => counts && new Map(Object.entries(counts));
4306
5088
  /**
4307
- * Drop the spaces and tabs around line endings, as markdown does within a
4308
- * paragraph.
5089
+ * The error of serialize() with its path told as a story variable:
5090
+ * `(at .variables.inv[0])` becomes `(at $inv[0])`, and a value only an
5091
+ * earlier history moment holds `(at $inv[0] in history moment 3)`.
4309
5092
  */
4310
- function trimLineEdges(text) {
4311
- if (!text.includes("\n")) return text;
4312
- return text.split("\n").map((line, k, lines) => {
4313
- const start = k === 0 ? 0 : skipChars(line, 0, 1, " ");
4314
- const end = k === lines.length - 1 ? line.length : skipChars(line, line.length, -1, " ");
4315
- return line.slice(start, Math.max(start, end));
4316
- }).join("\n");
5093
+ function variablePathError(err) {
5094
+ if (!(err instanceof TypeError)) return err;
5095
+ const message = err.message.replace(/\(at \.variables(?=[.[]|\))/, "(at $").replace(/\(at \.history\[(\d+)\]\.variables(.*)\)$/, (_, i, path) => `(at $${path} in history moment ${i})`).replace(/\(at \$\.?/, "(at $");
5096
+ return message === err.message ? err : new TypeError(message);
4317
5097
  }
4318
5098
  /**
4319
- * Build Preact vnodes from a combined string that contains only plain text
4320
- * and placeholders. No micromark, no innerHTML. The text is the content of
4321
- * one paragraph, so like micromark it drops spaces and tabs around line
4322
- * endings; its edges are already split off by the caller.
5099
+ * The stored form of `payload`. Throws, naming the variable, when the
5100
+ * payload holds a value a save cannot (see serialize()).
4323
5101
  */
4324
- function buildPlainTextVnodes(core, ph, unwrapParagraphs) {
4325
- const children = expandPlaceholderText(trimLineEdges(core), ph).filter((part) => part !== "");
4326
- return unwrapParagraphs ? /* @__PURE__ */ jsx(Fragment$1, { children }) : h("p", null, ...children);
5102
+ function encodePayload(payload) {
5103
+ try {
5104
+ return {
5105
+ formatVersion: 1,
5106
+ data: serialize({
5107
+ ...payload,
5108
+ visitCounts: countsToMap(payload.visitCounts),
5109
+ renderCounts: countsToMap(payload.renderCounts)
5110
+ })
5111
+ };
5112
+ } catch (err) {
5113
+ throw variablePathError(err);
5114
+ }
5115
+ }
5116
+ /** Thrown for stored data that is not a payload this format can hold. */
5117
+ var invalid = () => /* @__PURE__ */ new Error("Invalid save data");
5118
+ /** A plain object or one without a prototype (a variable namespace). */
5119
+ function isRecord$1(value) {
5120
+ if (typeof value !== "object" || value === null) return false;
5121
+ const proto = Object.getPrototypeOf(value);
5122
+ return proto === Object.prototype || proto === null;
5123
+ }
5124
+ /** Absent, null, or a `{ seed, pull }` PRNG snapshot. */
5125
+ function isOptionalPRNGSnapshot(value) {
5126
+ if (value == null) return true;
5127
+ return isRecord$1(value) && typeof value.seed === "string" && typeof value.pull === "number" && Number.isInteger(value.pull) && value.pull >= 0;
4327
5128
  }
5129
+ var isMoment = (value) => isRecord$1(value) && typeof value.passage === "string" && isRecord$1(value.variables) && typeof value.timestamp === "number" && isOptionalPRNGSnapshot(value.prng);
5130
+ var isCountsMap = (value) => value === void 0 || value instanceof Map && [...value.keys()].every((k) => typeof k === "string");
4328
5131
  /**
4329
- * Render AST nodes with full CommonMark markdown support.
4330
- *
4331
- * Combines all nodes into a single markdown document, using placeholder
4332
- * elements (see Placeholders) for non-text nodes (variables, macros, links,
4333
- * HTML). This allows
4334
- * markdown syntax to span across Twine tokens — e.g., markdown tables can
4335
- * contain {$variables} and {macros} in their cells.
4336
- *
4337
- * After micromark processes the combined string, the HTML is parsed back into
4338
- * Preact VNodes with placeholders replaced by the real rendered components.
4339
- * Placeholders that micromark escaped as code text are swapped back too (see
4340
- * PLACEHOLDER_TEXT_RE), so variables in code stay live components.
5132
+ * Whether a deserialized payload is well formed: every history moment is,
5133
+ * and `historyIndex` is an integer that points at a moment of the payload's
5134
+ * passage.
4341
5135
  */
4342
- function renderNodes(nodes, options) {
4343
- if (nodes.length === 0) return null;
4344
- if (options?.raw) return renderInlineNodes(nodes);
4345
- if (!nodes.some((n) => n.type === "text" && (n.value.trim() !== "" || BLANK_LINE_RE.test(n.value)))) return nodes.map((node) => renderSingleNode(node));
4346
- const components = [];
4347
- const ph = {
4348
- nonce: Math.random().toString(36).slice(2, 10) || "0",
4349
- components,
4350
- nodes: []
5136
+ function isPayload(value) {
5137
+ if (!isRecord$1(value)) return false;
5138
+ if (typeof value.passage !== "string" || !isRecord$1(value.variables)) return false;
5139
+ const { history, historyIndex } = value;
5140
+ if (!Array.isArray(history) || history.length === 0) return false;
5141
+ if (!Array.from(history).every(isMoment)) return false;
5142
+ if (typeof historyIndex !== "number" || !Number.isInteger(historyIndex) || historyIndex < 0 || historyIndex >= history.length || history[historyIndex].passage !== value.passage) return false;
5143
+ return isCountsMap(value.visitCounts) && isCountsMap(value.renderCounts) && isOptionalPRNGSnapshot(value.prng);
5144
+ }
5145
+ var mapToCounts = (counts) => counts instanceof Map ? createCounts(Object.fromEntries(counts)) : void 0;
5146
+ /**
5147
+ * The live payload of a stored one: class instances and built-ins
5148
+ * restored. Throws an IncompatibleSaveError when `encoded` has no format
5149
+ * version or one this build cannot read (saves from before versioning, or
5150
+ * from a newer Spindle), and an error when its data is malformed.
5151
+ */
5152
+ function decodePayload(encoded) {
5153
+ if (typeof encoded !== "object" || encoded === null) throw invalid();
5154
+ const { formatVersion, data } = encoded;
5155
+ checkFormatVersion(formatVersion);
5156
+ if (typeof data !== "string") throw invalid();
5157
+ const body = deserialize(migrate(formatVersion, data));
5158
+ if (!isPayload(body)) throw invalid();
5159
+ const payload = {
5160
+ ...body,
5161
+ visitCounts: mapToCounts(body.visitCounts),
5162
+ renderCounts: mapToCounts(body.renderCounts)
4351
5163
  };
4352
- let combined = "";
4353
- for (let i = 0; i < nodes.length; i++) {
4354
- const node = nodes[i];
4355
- if (node.type === "text") {
4356
- combined += node.value;
4357
- continue;
4358
- }
4359
- const phIdx = components.length;
4360
- components.push(renderSingleNode(node));
4361
- ph.nodes.push(node);
4362
- if (combined.endsWith("\\")) combined += ESCAPE_GUARD;
4363
- combined += placeholderHtml(ph.nonce, phIdx);
5164
+ if (payload.visitCounts === void 0) delete payload.visitCounts;
5165
+ if (payload.renderCounts === void 0) delete payload.renderCounts;
5166
+ return payload;
5167
+ }
5168
+ //#endregion
5169
+ //#region src/saves/types.ts
5170
+ var INVALID_FILE = "Invalid save file format";
5171
+ function isRecord(value) {
5172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5173
+ }
5174
+ var META_STRINGS = [
5175
+ "id",
5176
+ "passage",
5177
+ "ifid",
5178
+ "playthroughId",
5179
+ "createdAt",
5180
+ "updatedAt",
5181
+ "title"
5182
+ ];
5183
+ /** decodePayload(), without the warnings for unregistered classes. */
5184
+ function decodeQuietly(encoded) {
5185
+ const warn = console.warn;
5186
+ console.warn = () => {};
5187
+ try {
5188
+ decodePayload(encoded);
5189
+ } finally {
5190
+ console.warn = warn;
4364
5191
  }
4365
- const unwrapParagraphs = !!(options?.nobr || options?.inline);
4366
- const lead = combined.slice(0, skipChars(combined, 0, 1, EDGE_WS));
4367
- const trail = lead.length === combined.length ? "" : combined.slice(skipChars(combined, combined.length, -1, EDGE_WS));
4368
- const core = combined.slice(lead.length, combined.length - trail.length);
4369
- const edges = (content) => unwrapParagraphs && (lead || trail) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
4370
- lead,
4371
- content,
4372
- trail
4373
- ] }) : content;
4374
- const textOnly = combined.replace(PLACEHOLDER_STRIP_RE, "");
4375
- if (!MARKDOWN_SYNTAX_RE.test(textOnly) && !BLANK_LINE_RE.test(textOnly)) return edges(buildPlainTextVnodes(core, ph, unwrapParagraphs));
4376
- return edges(htmlToPreact(markdownToHtml(combined, { inline: options?.inline }), ph, unwrapParagraphs));
5192
+ }
5193
+ /**
5194
+ * Check data from outside the running story (an imported save) in full:
5195
+ * the export's and the payload's format versions, the record's metadata,
5196
+ * and that the payload decodes (see decodePayload). Throws an
5197
+ * IncompatibleSaveError for a missing or unknown format version, and an
5198
+ * "Invalid save file format" error for anything else that is wrong.
5199
+ * Anything that passes can be stored and later loaded.
5200
+ */
5201
+ function checkSaveExport(value) {
5202
+ if (!isRecord(value)) throw new Error(INVALID_FILE);
5203
+ if (!hasOwn(value, "formatVersion")) throw value.version === 1 ? new IncompatibleSaveError() : new Error(INVALID_FILE);
5204
+ checkFormatVersion(value.formatVersion);
5205
+ if (typeof value.ifid !== "string") throw new Error(INVALID_FILE);
5206
+ const save = value.save;
5207
+ if (!isRecord(save) || !isRecord(save.meta)) throw new Error(INVALID_FILE);
5208
+ const meta = save.meta;
5209
+ if (META_STRINGS.some((key) => typeof meta[key] !== "string")) throw new Error(INVALID_FILE);
5210
+ try {
5211
+ decodeQuietly(save.payload);
5212
+ } catch (err) {
5213
+ if (err instanceof IncompatibleSaveError) throw err;
5214
+ throw new Error(INVALID_FILE);
5215
+ }
5216
+ }
5217
+ /** Whether checkSaveExport() accepts `value`. */
5218
+ function isSaveExport(value) {
5219
+ try {
5220
+ checkSaveExport(value);
5221
+ return true;
5222
+ } catch {
5223
+ return false;
5224
+ }
5225
+ }
5226
+ /** Estimated byte size of a stored payload. */
5227
+ function estimatePayloadBytes(payload) {
5228
+ return JSON.stringify(payload).length;
4377
5229
  }
4378
5230
  //#endregion
4379
5231
  //#region src/saves/storage.ts
@@ -4674,6 +5526,28 @@ function getBackendType() {
4674
5526
  return _backend?.type ?? "memory";
4675
5527
  }
4676
5528
  //#endregion
5529
+ //#region src/utils/value-kinds.ts
5530
+ var toStringTag = (value) => Object.prototype.toString.call(value).slice(8, -1);
5531
+ /** Temporal values are immutable: copies may share them. */
5532
+ var isTemporal = (value) => toStringTag(value).startsWith("Temporal.");
5533
+ /** new Number(), new String(), new Boolean(), Object(1n). */
5534
+ var isBoxed = (value) => value instanceof Number || value instanceof String || value instanceof Boolean || toStringTag(value) === "BigInt";
5535
+ /**
5536
+ * Values compared and copied as a whole, by their content: they have no
5537
+ * own keys to merge by, or their keys are not the data (a property written
5538
+ * into one would be dropped by clones and saves, and by Immer for Map and
5539
+ * Set).
5540
+ */
5541
+ function isAtomic(value) {
5542
+ return value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof URL || value instanceof URLSearchParams || value instanceof Error || isBoxed(value) || isTemporal(value);
5543
+ }
5544
+ /** "a Map", "an Error", ... for an atomic value (see isAtomic), else undefined. */
5545
+ function atomicName(value) {
5546
+ if (!isAtomic(value)) return void 0;
5547
+ const tag = value instanceof Error ? "Error" : toStringTag(value);
5548
+ return `${/^[AEIOU]/.test(tag) ? "an" : "a"} ${tag}`;
5549
+ }
5550
+ //#endregion
4677
5551
  //#region src/utils/object-path.ts
4678
5552
  /**
4679
5553
  * Traverse dot-path segments on an object and return the nested value.
@@ -4697,7 +5571,7 @@ var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
4697
5571
  * Built-ins whose content is not their properties: a property written into
4698
5572
  * one would be dropped by clones and saves (and by Immer, for Map and Set).
4699
5573
  */
4700
- 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;
5574
+ var builtinName = atomicName;
4701
5575
  /**
4702
5576
  * Shallow copy that keeps the prototype, so a registered class instance
4703
5577
  * stays an instance of its class (with the same own keys deepClone copies).
@@ -4749,6 +5623,13 @@ function deleteByPath(root, segments) {
4749
5623
  }
4750
5624
  if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
4751
5625
  const parent = walkToParent(root, segments, false);
5626
+ if (Array.isArray(parent) && isDraft(parent) && segments.length > 1) {
5627
+ const copy = current(parent).slice();
5628
+ delete copy[Number(last)];
5629
+ const holder = walkToParent(root, segments.slice(0, -1), false);
5630
+ holder[segments[segments.length - 2]] = copy;
5631
+ return;
5632
+ }
4752
5633
  delete parent[last];
4753
5634
  }
4754
5635
  /** Refuse a segment that would reach a prototype instead of story state. */
@@ -4794,66 +5675,199 @@ function isPlainObject(value) {
4794
5675
  const proto = Object.getPrototypeOf(value);
4795
5676
  return proto === Object.prototype || proto === null;
4796
5677
  }
5678
+ /** Own keys of an error that are not enumerable but are data. */
5679
+ var ERROR_HIDDEN_KEYS = [
5680
+ "message",
5681
+ "cause",
5682
+ "errors"
5683
+ ];
4797
5684
  function deepClone(value, options = {}) {
4798
5685
  const seen = /* @__PURE__ */ new Map();
5686
+ /** Record `copy` as the copy of `obj` before its contents are copied. */
5687
+ const keep = (obj, copy) => {
5688
+ seen.set(obj, copy);
5689
+ return copy;
5690
+ };
5691
+ /** Copy the own (enumerable) keys of `obj` into `copy`. */
5692
+ function copyKeys(obj, copy) {
5693
+ for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
5694
+ return copy;
5695
+ }
5696
+ function cloneBuiltin(val) {
5697
+ if (val instanceof Date) return keep(val, new Date(val.getTime()));
5698
+ if (val instanceof RegExp) return keep(val, new RegExp(val.source, val.flags));
5699
+ if (val instanceof ArrayBuffer) return keep(val, val.slice(0));
5700
+ if (ArrayBuffer.isView(val)) {
5701
+ const buffer = clone(val.buffer);
5702
+ if (val instanceof DataView) return keep(val, new DataView(buffer, val.byteOffset, val.byteLength));
5703
+ const ctor = val.constructor;
5704
+ const length = val.length;
5705
+ return keep(val, new ctor(buffer, val.byteOffset, length));
5706
+ }
5707
+ if (val instanceof URL) return keep(val, new URL(val.href));
5708
+ if (val instanceof URLSearchParams) return keep(val, new URLSearchParams(val));
5709
+ if (isBoxed(val)) return keep(val, Object(val.valueOf()));
5710
+ if (isTemporal(val)) return val;
5711
+ if (val instanceof Error) {
5712
+ const copy = keep(val, Object.create(Object.getPrototypeOf(val)));
5713
+ for (const key of [...ERROR_HIDDEN_KEYS, "stack"]) if (hasOwn(val, key)) Object.defineProperty(copy, key, {
5714
+ value: clone(val[key]),
5715
+ writable: true,
5716
+ configurable: true
5717
+ });
5718
+ return copyKeys(val, copy);
5719
+ }
5720
+ }
4799
5721
  function clone(val) {
4800
5722
  if (val === null || typeof val !== "object") return val;
4801
5723
  const obj = val;
4802
5724
  if (seen.has(obj)) return seen.get(obj);
4803
- if (val instanceof Date) return new Date(val.getTime());
4804
- if (val instanceof RegExp) return new RegExp(val.source, val.flags);
4805
5725
  if (Array.isArray(val)) {
4806
- const arr = [];
4807
- seen.set(obj, arr);
4808
- for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
5726
+ const arr = keep(obj, new Array(val.length));
5727
+ for (let i = 0; i < val.length; i++) if (i in val) arr[i] = clone(val[i]);
4809
5728
  return arr;
4810
5729
  }
4811
5730
  if (val instanceof Map) {
4812
- const copy = /* @__PURE__ */ new Map();
4813
- seen.set(obj, copy);
5731
+ const copy = keep(obj, /* @__PURE__ */ new Map());
4814
5732
  for (const [k, v] of val) copy.set(clone(k), clone(v));
4815
5733
  return copy;
4816
5734
  }
4817
5735
  if (val instanceof Set) {
4818
- const copy = /* @__PURE__ */ new Set();
4819
- seen.set(obj, copy);
5736
+ const copy = keep(obj, /* @__PURE__ */ new Set());
4820
5737
  for (const v of val) copy.add(clone(v));
4821
5738
  return copy;
4822
5739
  }
5740
+ const builtin = cloneBuiltin(obj);
5741
+ if (builtin !== void 0) return builtin;
4823
5742
  const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
4824
5743
  if (!keepsProto && options.keepUnregistered) return val;
4825
- const copy = keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {};
4826
- seen.set(obj, copy);
4827
- for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
4828
- return copy;
5744
+ return copyKeys(obj, keep(obj, keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {}));
4829
5745
  }
4830
5746
  return clone(value);
4831
5747
  }
4832
5748
  /**
4833
- * Structural equality over the value types deepClone() supports: primitives,
4834
- * arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
4835
- * at any depth). Map and Set entries are compared in insertion order. Arrays
4836
- * are compared by length and index, so a hole equals an undefined element
4837
- * (deepClone() and save/load turn holes into undefined elements).
5749
+ * Structural equality over the value types deepClone() supports, nested at
5750
+ * any depth and with cycles. Objects must have the same prototype. Map and
5751
+ * Set entries are compared in insertion order, so reordering one is a
5752
+ * change. Arrays are compared by length and index; a hole differs from an
5753
+ * undefined element. Errors are compared by class, message, cause, errors
5754
+ * and own keys (not their stack).
4838
5755
  */
4839
5756
  function deepEqual(a, b) {
4840
- return equal(a, b, /* @__PURE__ */ new Map());
5757
+ return equal(a, b, loosePairs());
5758
+ }
5759
+ /**
5760
+ * Pairs for deepEqual(). Every object may pair with several others, since
5761
+ * cycles of different lengths can still unfold to the same value, and
5762
+ * shared references may equal distinct copies.
5763
+ */
5764
+ function loosePairs() {
5765
+ const assumed = /* @__PURE__ */ new Map();
5766
+ return (a, b) => {
5767
+ const pairs = assumed.get(a);
5768
+ if (pairs?.has(b)) return true;
5769
+ if (pairs) pairs.add(b);
5770
+ else assumed.set(a, new Set([b]));
5771
+ };
4841
5772
  }
4842
5773
  /**
4843
- * `assumed` holds the pairs already being compared: meeting one again (a
4844
- * cycle) assumes it equal. Every object may pair with several others, since
4845
- * cycles of different lengths can still unfold to the same value.
5774
+ * Pairs that also require the same reference structure: each object of one
5775
+ * value pairs with exactly one of the other, so shared references and
5776
+ * cycles are shared at the same places.
4846
5777
  */
5778
+ function strictPairs() {
5779
+ const ab = /* @__PURE__ */ new Map();
5780
+ const ba = /* @__PURE__ */ new Map();
5781
+ const pairs = (a, b) => {
5782
+ const known = ab.get(a);
5783
+ if (known !== void 0) return known === b;
5784
+ if (ba.has(b)) return false;
5785
+ ab.set(a, b);
5786
+ ba.set(b, a);
5787
+ };
5788
+ pairs.strict = true;
5789
+ return pairs;
5790
+ }
5791
+ /**
5792
+ * Whether `a` and `b` are equal as deepEqual() has it and also share
5793
+ * references at the same places (a save of one loads as the other).
5794
+ */
5795
+ function deepEqualStrict(a, b) {
5796
+ return equal(a, b, strictPairs());
5797
+ }
5798
+ /**
5799
+ * `curr` with each part that equals (see deepEqualStrict) the part of
5800
+ * `prev` at the same place replaced by `prev`'s: the history moments of a
5801
+ * save then share what did not change between them, which a save stores
5802
+ * once. Containers on the way to a change are copied; neither value is
5803
+ * changed. Shared references and cycles within `curr` stay so.
5804
+ */
5805
+ function shareEqual(prev, curr) {
5806
+ const done = /* @__PURE__ */ new Map();
5807
+ function share(p, c) {
5808
+ if (p === c || !isObjectValue(c) || !isObjectValue(p)) return c;
5809
+ if (done.has(c)) return done.get(c);
5810
+ if (deepEqualStrict(p, c)) {
5811
+ done.set(c, p);
5812
+ return p;
5813
+ }
5814
+ if (c instanceof Map && p instanceof Map) {
5815
+ const out = /* @__PURE__ */ new Map();
5816
+ done.set(c, out);
5817
+ for (const [k, v] of c) out.set(k, p.has(k) ? share(p.get(k), v) : v);
5818
+ return out;
5819
+ }
5820
+ if (Array.isArray(c) && Array.isArray(p)) {
5821
+ const out = new Array(c.length);
5822
+ done.set(c, out);
5823
+ for (let i = 0; i < c.length; i++) if (i in c) out[i] = i < p.length ? share(p[i], c[i]) : c[i];
5824
+ return out;
5825
+ }
5826
+ if (mergesWith(p, c, false)) {
5827
+ const out = Object.create(Object.getPrototypeOf(c));
5828
+ done.set(c, out);
5829
+ const cr = c;
5830
+ for (const key of Object.keys(cr)) setOwn(out, key, hasOwn(p, key) ? share(p[key], cr[key]) : cr[key]);
5831
+ return out;
5832
+ }
5833
+ done.set(c, c);
5834
+ return c;
5835
+ }
5836
+ return share(prev, curr);
5837
+ }
5838
+ var isObjectValue = (v) => typeof v === "object" && v !== null;
5839
+ function equalBytes(a, b) {
5840
+ const x = ArrayBuffer.isView(a) ? new Uint8Array(a.buffer, a.byteOffset, a.byteLength) : new Uint8Array(a);
5841
+ const y = ArrayBuffer.isView(b) ? new Uint8Array(b.buffer, b.byteOffset, b.byteLength) : new Uint8Array(b);
5842
+ if (x.length !== y.length) return false;
5843
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
5844
+ return true;
5845
+ }
5846
+ /** Equality of the content of atomic built-ins (see isAtomic) but Map/Set. */
5847
+ function equalBuiltin(a, b, assumed) {
5848
+ if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
5849
+ if (a instanceof RegExp) return String(a) === String(b);
5850
+ if (ArrayBuffer.isView(a) || a instanceof ArrayBuffer) return equalBytes(a, b);
5851
+ if (a instanceof URL || a instanceof URLSearchParams) return String(a) === String(b);
5852
+ if (isBoxed(a)) return Object.is(a.valueOf(), b.valueOf());
5853
+ if (isTemporal(a)) return String(a) === String(b);
5854
+ if (a instanceof Error) return ERROR_HIDDEN_KEYS.every((key) => hasOwn(a, key) === hasOwn(b, key) && equal(a[key], b[key], assumed)) && equalKeys(a, b, assumed);
5855
+ }
5856
+ function equalKeys(a, b, assumed) {
5857
+ const ao = a;
5858
+ const bo = b;
5859
+ const keys = Object.keys(ao);
5860
+ if (keys.length !== Object.keys(bo).length) return false;
5861
+ for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
5862
+ return true;
5863
+ }
5864
+ /** `assumed` tracks the pairs of objects met (see Pairs). */
4847
5865
  function equal(a, b, assumed) {
4848
- if (Object.is(a, b)) return true;
5866
+ if (Object.is(a, b) && !(assumed.strict && isObjectValue(a))) return true;
4849
5867
  if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
4850
5868
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
4851
- const pairs = assumed.get(a);
4852
- if (pairs?.has(b)) return true;
4853
- if (pairs) pairs.add(b);
4854
- else assumed.set(a, new Set([b]));
4855
- if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
4856
- if (a instanceof RegExp) return String(a) === String(b);
5869
+ const met = assumed(a, b);
5870
+ if (met !== void 0) return met;
4857
5871
  if (a instanceof Map || a instanceof Set) {
4858
5872
  const bc = b;
4859
5873
  if (a.size !== bc.size) return false;
@@ -4862,27 +5876,25 @@ function equal(a, b, assumed) {
4862
5876
  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;
4863
5877
  return true;
4864
5878
  }
5879
+ const builtin = equalBuiltin(a, b, assumed);
5880
+ if (builtin !== void 0) return builtin;
4865
5881
  if (Array.isArray(a)) {
4866
5882
  const bc = b;
4867
5883
  if (a.length !== bc.length) return false;
4868
- for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
5884
+ for (let i = 0; i < a.length; i++) if (i in a !== i in bc || !equal(a[i], bc[i], assumed)) return false;
4869
5885
  return true;
4870
5886
  }
4871
- const ao = a;
4872
- const bo = b;
4873
- const keys = Object.keys(ao);
4874
- if (keys.length !== Object.keys(bo).length) return false;
4875
- for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
4876
- return true;
5887
+ return equalKeys(a, b, assumed);
4877
5888
  }
4878
5889
  /**
4879
5890
  * Objects merged property by property: plain objects and (registered)
4880
- * class instances. Arrays, Map, Set, Date and RegExp are values that are
4881
- * replaced as a whole, since their elements have no stable identity to
4882
- * merge by (a shift moves every index).
5891
+ * class instances. Arrays and atomic built-ins (Map, Set, Date, typed
5892
+ * arrays, errors..., see isAtomic) are values that are replaced as a whole,
5893
+ * since their elements have no stable identity to merge by (a shift moves
5894
+ * every index) or their content is not in their keys.
4883
5895
  */
4884
5896
  function isMergeable(value) {
4885
- return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
5897
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !isAtomic(value);
4886
5898
  }
4887
5899
  /**
4888
5900
  * Whether changes between `a` and `b` merge key by key: both are objects of
@@ -5021,13 +6033,13 @@ var VALID_EVENTS = new Set([
5021
6033
  "passagerender",
5022
6034
  "dialogrender"
5023
6035
  ]);
5024
- var listeners = /* @__PURE__ */ new Map();
6036
+ var listeners$1 = /* @__PURE__ */ new Map();
5025
6037
  function on(event, cb) {
5026
6038
  if (!VALID_EVENTS.has(event)) throw new Error(`spindle: Unknown event "${event}".`);
5027
- let set = listeners.get(event);
6039
+ let set = listeners$1.get(event);
5028
6040
  if (!set) {
5029
6041
  set = /* @__PURE__ */ new Set();
5030
- listeners.set(event, set);
6042
+ listeners$1.set(event, set);
5031
6043
  }
5032
6044
  set.add(cb);
5033
6045
  return () => {
@@ -5036,7 +6048,7 @@ function on(event, cb) {
5036
6048
  }
5037
6049
  /** Call `call` with each listener of `event`. */
5038
6050
  function forEachListener(event, call) {
5039
- const set = listeners.get(event);
6051
+ const set = listeners$1.get(event);
5040
6052
  if (!set) return;
5041
6053
  for (const cb of [...set]) call(cb);
5042
6054
  }
@@ -5225,29 +6237,15 @@ function saveWithHooks(slot, custom, beginCapture, write) {
5225
6237
  return Promise.reject(err);
5226
6238
  }
5227
6239
  }
5228
- /**
5229
- * `payload` with `convert` applied to its live variables and to those of
5230
- * every history moment.
5231
- */
5232
- function mapVariables(payload, convert) {
5233
- return {
5234
- ...payload,
5235
- variables: convert(payload.variables),
5236
- history: payload.history.map((m) => ({
5237
- ...m,
5238
- variables: convert(m.variables)
5239
- }))
5240
- };
5241
- }
5242
- /** The record to store for a save: `payload` serialized, and its size. */
6240
+ /** The record to store for a save: `payload` encoded, and its size. */
5243
6241
  function storedRecord(meta, payload) {
5244
- const serialized = mapVariables(deepClone(payload), serialize);
6242
+ const encoded = encodePayload(payload);
5245
6243
  return {
5246
6244
  meta: {
5247
6245
  ...meta,
5248
- estimatedBytes: estimatePayloadBytes(serialized)
6246
+ estimatedBytes: estimatePayloadBytes(encoded)
5249
6247
  },
5250
- payload: serialized
6248
+ payload: encoded
5251
6249
  };
5252
6250
  }
5253
6251
  var createSave = queued(createSaveNow);
@@ -5301,15 +6299,13 @@ async function overwriteSaveNow(saveId, payload, custom, playthroughId) {
5301
6299
  return updated;
5302
6300
  }
5303
6301
  /**
5304
- * Turn a stored (serialized) payload into a live one, restoring class
5305
- * instances and built-ins. Returns a new payload; the stored one is left
5306
- * untouched. Payloads handed to the store's `loadFromPayload()` must be live.
6302
+ * The live payload of a stored save, if there is one (see decodePayload:
6303
+ * throws for a save of an incompatible format). Payloads handed to the
6304
+ * store's `loadFromPayload()` must be live.
5307
6305
  */
5308
- function deserializePayload(payload) {
5309
- return mapVariables(payload, deserialize);
5310
- }
5311
- /** The live payload of a stored save, if there is one. */
5312
- var livePayload = (record) => record && deserializePayload(record.payload);
6306
+ var livePayload = (record) => record && decodePayload(record.payload);
6307
+ /** The live payload of a stored record's payload (see decodePayload). */
6308
+ var decodeSavePayload = (payload) => decodePayload(payload);
5313
6309
  readingSave(livePayload);
5314
6310
  /**
5315
6311
  * Delete a save record. If the default slot or a named slot holds it, that
@@ -5478,7 +6474,7 @@ function loadSlotSave(ifid, slot) {
5478
6474
  return inOrder(async () => {
5479
6475
  const record = await slotRecord(ifid, slot);
5480
6476
  if (!record) return void 0;
5481
- const payload = deserializePayload(record.payload);
6477
+ const payload = decodePayload(record.payload);
5482
6478
  const { playthroughId } = record.meta;
5483
6479
  if (playthroughId) await adoptPlaythroughNow(ifid, playthroughId);
5484
6480
  return {
@@ -5528,22 +6524,32 @@ var deleteSlotSave = queued(async (ifid, slot) => {
5528
6524
  });
5529
6525
  var SESSION_KEY_PREFIX = "spindle.session.";
5530
6526
  /**
5531
- * Write a pre-serialized session payload to sessionStorage.
5532
- * Callers are responsible for serializing variables (see persistSession in store.ts).
6527
+ * Write the session payload to sessionStorage, encoded like a save (see
6528
+ * encodePayload). Throws, like a save, when the state holds a value a save
6529
+ * cannot (a function, an instance of an unregistered class, a unique
6530
+ * symbol); the session then keeps its previous copy.
5533
6531
  */
5534
- function saveSession(ifid, data) {
6532
+ function saveSession(ifid, payload) {
6533
+ const text = JSON.stringify(encodePayload(payload));
5535
6534
  try {
5536
- sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`, JSON.stringify(data));
6535
+ sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`, text);
5537
6536
  } catch {}
5538
6537
  }
6538
+ /**
6539
+ * The live payload of the stored session, if there is one that loads. A
6540
+ * session written by an incompatible version of Spindle is dropped with a
6541
+ * warning, and the story starts fresh.
6542
+ */
5539
6543
  function loadSession(ifid) {
5540
6544
  try {
5541
6545
  const raw = sessionStorage.getItem(`${SESSION_KEY_PREFIX}${ifid}`);
5542
6546
  if (!raw) return void 0;
5543
- const parsed = JSON.parse(raw);
5544
- if (!isSavePayload(parsed)) return void 0;
5545
- return deserializePayload(parsed);
5546
- } catch {
6547
+ return decodePayload(JSON.parse(raw));
6548
+ } catch (err) {
6549
+ if (err instanceof IncompatibleSaveError) {
6550
+ console.warn(`spindle: ${err.message}; the session was not restored.`);
6551
+ clearSession(ifid);
6552
+ }
5547
6553
  return;
5548
6554
  }
5549
6555
  }
@@ -5556,7 +6562,7 @@ function clearSession(ifid) {
5556
6562
  function toExport(record) {
5557
6563
  if (!record) return void 0;
5558
6564
  return {
5559
- version: 1,
6565
+ formatVersion: 1,
5560
6566
  ifid: record.meta.ifid,
5561
6567
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
5562
6568
  save: record
@@ -5569,9 +6575,8 @@ var exportSave = readingSave(toExport);
5569
6575
  * The returned record is not stored yet.
5570
6576
  */
5571
6577
  async function prepareImport(data, ifid) {
5572
- if (data.version !== 1) throw new Error(`Unsupported save version: ${data.version}`);
6578
+ checkSaveExport(data);
5573
6579
  if (data.ifid !== ifid) throw new Error(`Save is from a different story (expected IFID ${ifid}, got ${data.ifid})`);
5574
- if (!isSaveExport(data)) throw new Error("Invalid save file format");
5575
6580
  const record = deepClone(data.save);
5576
6581
  record.meta.id = crypto.randomUUID();
5577
6582
  record.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -6220,6 +7225,48 @@ function isDialogShowing() {
6220
7225
  return dialogHostCallbacks?.isOpen() ?? false;
6221
7226
  }
6222
7227
  //#endregion
7228
+ //#region src/runtime-errors.ts
7229
+ var errors = [];
7230
+ var nextId = 1;
7231
+ var listeners = /* @__PURE__ */ new Set();
7232
+ function update(next) {
7233
+ errors = next;
7234
+ for (const listener of listeners) listener();
7235
+ }
7236
+ /**
7237
+ * Show `error` on the page, after `context` (what failed). The same error
7238
+ * again, while it is shown, counts up instead of adding an entry. Logging
7239
+ * it to the console is up to the caller.
7240
+ */
7241
+ function showRuntimeError(context, error) {
7242
+ const message = errorMessage(error).replace(/^spindle: /, "");
7243
+ const same = errors.find((e) => e.context === context && e.message === message);
7244
+ update(same ? errors.map((e) => e === same ? {
7245
+ ...e,
7246
+ count: e.count + 1
7247
+ } : e) : [...errors, {
7248
+ id: nextId++,
7249
+ context,
7250
+ message,
7251
+ count: 1
7252
+ }]);
7253
+ }
7254
+ /** Remove the shown error `id`. */
7255
+ function dismissRuntimeError(id) {
7256
+ update(errors.filter((e) => e.id !== id));
7257
+ }
7258
+ /** The errors shown now, oldest first. */
7259
+ function getRuntimeErrors() {
7260
+ return errors;
7261
+ }
7262
+ /** Call `listener` whenever the shown errors change; returns unsubscribe. */
7263
+ function subscribeRuntimeErrors(listener) {
7264
+ listeners.add(listener);
7265
+ return () => {
7266
+ listeners.delete(listener);
7267
+ };
7268
+ }
7269
+ //#endregion
6223
7270
  //#region src/store.ts
6224
7271
  enablePatches();
6225
7272
  enableMapSet();
@@ -6245,24 +7292,39 @@ var variableBase = createNamespace();
6245
7292
  var patchEntries = [];
6246
7293
  /** Immer-produced reference to variables right after the last navigation. */
6247
7294
  var lastNavigationVars = createNamespace();
6248
- /** Deep-clone patch values so they are independent of future mutations. */
6249
- function clonePatches(patches) {
6250
- return patches.map((p) => ({
6251
- ...p,
6252
- value: p.value !== void 0 ? deepClone(p.value) : void 0
6253
- }));
6254
- }
6255
- /** Compute forward + inverse patches that transform `prev` into `curr`. */
7295
+ /**
7296
+ * The variables that differ between `prev` and `curr`: added, replaced (by
7297
+ * reference: store values are immutable, so an unchanged variable keeps its
7298
+ * object) or deleted. A recorded value is a deep copy, so that changing a
7299
+ * live value in place (a class instance is not frozen) cannot change
7300
+ * history; instances of unregistered classes are kept, as the store keeps
7301
+ * them (a save then refuses them).
7302
+ */
6256
7303
  function computeVarPatches(prev, curr) {
6257
- const [, forward, inverse] = produceWithPatches(prev, (draft) => {
6258
- const d = draft;
6259
- for (const key of Object.keys(d)) if (!hasOwn(curr, key)) delete d[key];
6260
- for (const [key, val] of Object.entries(curr)) d[key] = val;
7304
+ const forward = [];
7305
+ for (const key of Object.keys(prev)) if (!hasOwn(curr, key)) forward.push({
7306
+ key,
7307
+ deleted: true
6261
7308
  });
6262
- return {
6263
- forward: clonePatches(forward),
6264
- inverse: clonePatches(inverse)
6265
- };
7309
+ for (const key of Object.keys(curr)) if (!hasOwn(prev, key) || !Object.is(prev[key], curr[key])) forward.push({
7310
+ key,
7311
+ deleted: false,
7312
+ value: deepClone(curr[key], { keepUnregistered: true })
7313
+ });
7314
+ return { forward };
7315
+ }
7316
+ /**
7317
+ * The variables of the moment after `vars`, by `changes`. The recorded
7318
+ * values are used as they are, not copied, so values the moments share
7319
+ * stay shared, and cycles and shared references in them hold (Immer's
7320
+ * applyPatches copies each value, and recurses forever on a cycle).
7321
+ */
7322
+ function applyVarPatches(vars, changes) {
7323
+ const next = Object.create(Object.getPrototypeOf(vars));
7324
+ for (const key of Object.keys(vars)) setOwn(next, key, vars[key]);
7325
+ for (const change of changes) if (change.deleted) delete next[change.key];
7326
+ else setOwn(next, change.key, change.value);
7327
+ return next;
6266
7328
  }
6267
7329
  /**
6268
7330
  * Replace the variable snapshot recorded for the newest history moment
@@ -6280,7 +7342,7 @@ function rerecordNewestMoment(vars) {
6280
7342
  function forEachRecorded(count, fn) {
6281
7343
  let vars = variableBase;
6282
7344
  for (let i = 0; i < count; i++) {
6283
- if (i > 0) vars = applyPatches(vars, patchEntries[i - 1].forward);
7345
+ if (i > 0) vars = applyVarPatches(vars, patchEntries[i - 1].forward);
6284
7346
  fn(vars, i);
6285
7347
  }
6286
7348
  }
@@ -6296,30 +7358,65 @@ function savedMoment(moment, variables) {
6296
7358
  /** Reconstruct variables at a given history moment by replaying patches. */
6297
7359
  function reconstructVarsAt(index) {
6298
7360
  let vars = variableBase;
6299
- for (let i = 0; i < index; i++) vars = applyPatches(vars, patchEntries[i].forward);
7361
+ for (let i = 0; i < index; i++) vars = applyVarPatches(vars, patchEntries[i].forward);
6300
7362
  return vars;
6301
7363
  }
6302
- var serializedHistory = [];
7364
+ /**
7365
+ * The history moments the session holds, with the variables recorded for
7366
+ * each. Store state is immutable, so holding the recorded objects keeps
7367
+ * them as they were; moments share the values that did not change between
7368
+ * them, and the session stores those once (see encodePayload).
7369
+ */
7370
+ var sessionMoments = [];
7371
+ /**
7372
+ * Write the game to the session: the whole payload, encoded in one piece
7373
+ * on every navigation.
7374
+ */
6303
7375
  function persistSession(get) {
6304
7376
  const { storyData, currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
6305
7377
  if (!storyData) return;
6306
- if (serializedHistory.length > history.length) serializedHistory.length = history.length;
6307
- if (serializedHistory.length < history.length) if (history.length - serializedHistory.length === 1) {
7378
+ if (sessionMoments.length > history.length) sessionMoments.length = history.length;
7379
+ if (sessionMoments.length < history.length) if (history.length - sessionMoments.length === 1) {
6308
7380
  const i = history.length - 1;
6309
- serializedHistory[i] = savedMoment(history[i], serialize(variables));
7381
+ sessionMoments[i] = savedMoment(history[i], variables);
6310
7382
  } else forEachRecorded(history.length, (vars, i) => {
6311
- if (i >= serializedHistory.length) serializedHistory[i] = savedMoment(history[i], serialize(vars));
7383
+ if (i >= sessionMoments.length) sessionMoments[i] = savedMoment(history[i], vars);
6312
7384
  });
6313
7385
  saveSession(storyData.ifid, {
6314
7386
  passage: currentPassage,
6315
- variables: serialize(variables),
6316
- history: serializedHistory,
7387
+ variables,
7388
+ history: sessionMoments,
6317
7389
  historyIndex,
6318
7390
  visitCounts,
6319
7391
  renderCounts,
6320
7392
  prng: snapshotPRNG()
6321
7393
  });
6322
7394
  }
7395
+ /** What the page shows before a session write error (see RuntimeErrors). */
7396
+ var SESSION_ERROR_CONTEXT = "The game could not be saved for a page reload; a reload goes back to the last passage it could save:";
7397
+ /**
7398
+ * Write the session, then run `after` (the rest of the operation: its
7399
+ * events and queued navigations), then throw the session's error if writing
7400
+ * it failed: a value a save cannot hold (a function, an instance of an
7401
+ * unregistered class, a unique symbol) fails the operation that put it in
7402
+ * the state's history, as it fails a save, but only after the operation is
7403
+ * complete, so the story is not left half-way through it. The error is also
7404
+ * shown on the page, and the session keeps its last good copy.
7405
+ */
7406
+ function persistSessionThen(get, after) {
7407
+ let failure;
7408
+ try {
7409
+ persistSession(get);
7410
+ } catch (error) {
7411
+ failure = { error };
7412
+ showRuntimeError(SESSION_ERROR_CONTEXT, error);
7413
+ }
7414
+ try {
7415
+ after?.();
7416
+ } finally {
7417
+ if (failure) throw failure.error;
7418
+ }
7419
+ }
6323
7420
  /**
6324
7421
  * Trim history to `state.maxHistory` moments: keep the newest moments that
6325
7422
  * include the current one. After a navigation (the current moment is the
@@ -6333,10 +7430,10 @@ function trimHistory(state) {
6333
7430
  if (excess <= 0) return false;
6334
7431
  const start = Math.min(state.historyIndex, excess);
6335
7432
  const end = start + state.maxHistory;
6336
- for (let i = 0; i < start; i++) variableBase = applyPatches(variableBase, patchEntries[i].forward);
7433
+ for (let i = 0; i < start; i++) variableBase = applyVarPatches(variableBase, patchEntries[i].forward);
6337
7434
  state.history = state.history.slice(start, end);
6338
7435
  patchEntries = patchEntries.slice(start, end - 1);
6339
- serializedHistory = serializedHistory.slice(start, end);
7436
+ sessionMoments = sessionMoments.slice(start, end);
6340
7437
  state.historyIndex -= start;
6341
7438
  return true;
6342
7439
  }
@@ -6369,16 +7466,19 @@ function finishEnteredMoment(get, set) {
6369
7466
  });
6370
7467
  }
6371
7468
  /**
6372
- * A deep copy of a variable namespace as save data: a plain object, as a
6373
- * loaded save holds it (the store turns it back into a namespace on load).
7469
+ * A variable namespace as save data: a plain object, as a loaded save holds
7470
+ * it (the store turns it back into a namespace on load). The values are the
7471
+ * store's own, immutable ones, not copies: the moments of a payload share
7472
+ * the values that did not change between them, and a save stores those
7473
+ * once (see encodePayload).
6374
7474
  */
6375
- var plainCopy = (ns) => ({ ...deepClone(ns) });
7475
+ var plainCopy = (ns) => ({ ...ns });
6376
7476
  /** Reset all module-level state (called on init, restart, loadFromPayload). */
6377
7477
  function resetModuleState(base) {
6378
7478
  variableBase = base;
6379
7479
  patchEntries = [];
6380
7480
  lastNavigationVars = base;
6381
- serializedHistory = [];
7481
+ sessionMoments = [];
6382
7482
  }
6383
7483
  /**
6384
7484
  * Fresh variables from `defaults` for a game that starts over (init,
@@ -6429,8 +7529,7 @@ function moveInHistory(get, set, step) {
6429
7529
  reinitTriggerState();
6430
7530
  lastNavigationVars = get().variables;
6431
7531
  restorePRNGFromMoment(get().history[get().historyIndex]);
6432
- persistSession(get);
6433
- emit("afternavigate", targetPassage, previousPassage);
7532
+ persistSessionThen(get, () => emit("afternavigate", targetPassage, previousPassage));
6434
7533
  }
6435
7534
  /**
6436
7535
  * Record the state StoryInit left behind as the start moment: its variable
@@ -6603,7 +7702,8 @@ function loadedEntryMoment(payload) {
6603
7702
  * the `beforesave` hooks) into the payload's snapshot of the saved moment. A
6604
7703
  * load restores that snapshot, the state on entering the passage, and runs
6605
7704
  * the passage again, so data a hook adds to a save would otherwise be lost
6606
- * on load (#227). Only the property paths the hooks changed are written: a
7705
+ * on load (#227). Only the property paths the hooks changed are written (into
7706
+ * copies of the objects on those paths, the rest staying shared): a
6607
7707
  * whole variable would bring along what the passage did to the rest of it,
6608
7708
  * which the passage then does again on load (#232). Only the payload's copy
6609
7709
  * changes: the live history keeps the recorded snapshot (#159).
@@ -6625,7 +7725,9 @@ function mergeHookWrites(target, before, after, ancestors) {
6625
7725
  ancestors.add(after);
6626
7726
  mergeKeys(target, before, after, (key, b, a) => {
6627
7727
  if (!hasOwn(target, key) || !mergesWith(a, target[key], true) || ancestors.has(a)) return false;
6628
- mergeHookWrites(target[key], b, a, ancestors);
7728
+ const copy = shallowCopy(target[key]);
7729
+ target[key] = copy;
7730
+ mergeHookWrites(copy, b, a, ancestors);
6629
7731
  return true;
6630
7732
  });
6631
7733
  ancestors.delete(after);
@@ -6790,7 +7892,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
6790
7892
  state.maxHistory = Math.max(1, Math.round(limit));
6791
7893
  trimmed = trimHistory(state);
6792
7894
  });
6793
- if (trimmed) persistSession(get);
7895
+ if (trimmed) persistSessionThen(get);
6794
7896
  },
6795
7897
  setQuickSaveKey: (key) => {
6796
7898
  set((state) => {
@@ -6851,7 +7953,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
6851
7953
  state.navigationId++;
6852
7954
  state.history = state.history.slice(0, state.historyIndex + 1);
6853
7955
  patchEntries.length = state.historyIndex;
6854
- if (serializedHistory.length > state.historyIndex + 1) serializedHistory.length = state.historyIndex + 1;
7956
+ if (sessionMoments.length > state.historyIndex + 1) sessionMoments.length = state.historyIndex + 1;
6855
7957
  patchEntries.push(patchEntry);
6856
7958
  state.history.push({
6857
7959
  passage: passageName,
@@ -6876,9 +7978,10 @@ var useStoryStore = create()(storyStore((set, get) => ({
6876
7978
  const deferred = deferredNavigations;
6877
7979
  deferredNavigations = [];
6878
7980
  finishEnteredMoment(get, set);
6879
- persistSession(get);
6880
- emit("afternavigate", passageName, previousPassage);
6881
- for (const next of deferred) get().navigate(next);
7981
+ persistSessionThen(get, () => {
7982
+ emit("afternavigate", passageName, previousPassage);
7983
+ for (const next of deferred) get().navigate(next);
7984
+ });
6882
7985
  },
6883
7986
  goBack: () => moveInHistory(get, set, -1),
6884
7987
  goForward: () => moveInHistory(get, set, 1),
@@ -7019,7 +8122,6 @@ var useStoryStore = create()(storyStore((set, get) => ({
7019
8122
  importSave: async (data, slot) => {
7020
8123
  const { storyData } = get();
7021
8124
  if (!storyData) throw new Error("spindle: Story is not initialized.");
7022
- if (!isSaveExport(data)) throw new Error("Invalid save file format");
7023
8125
  const info = await importSlotSave(data, storyData.ifid, slot);
7024
8126
  recordKnownSave(set, slot, true);
7025
8127
  return info;
@@ -7054,11 +8156,12 @@ var useStoryStore = create()(storyStore((set, get) => ({
7054
8156
  const { currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
7055
8157
  const saveHistory = [];
7056
8158
  forEachRecorded(history.length, (vars, i) => {
7057
- saveHistory.push(savedMoment(history[i], plainCopy(vars)));
8159
+ const recorded = plainCopy(vars);
8160
+ saveHistory.push(savedMoment(history[i], i === 0 ? recorded : shareEqual(saveHistory[i - 1].variables, recorded)));
7058
8161
  });
7059
8162
  return {
7060
8163
  passage: currentPassage,
7061
- variables: plainCopy(variables),
8164
+ variables: shareEqual(saveHistory[historyIndex]?.variables, plainCopy(variables)),
7062
8165
  history: saveHistory,
7063
8166
  historyIndex,
7064
8167
  visitCounts: { ...visitCounts },
@@ -7086,17 +8189,10 @@ var useStoryStore = create()(storyStore((set, get) => ({
7086
8189
  const ifid = get().storyData?.ifid;
7087
8190
  if (playthroughId && ifid) switchToLoadedPlaythrough(ifid, playthroughId);
7088
8191
  const entry = loadedEntryMoment(payload);
7089
- const base = createNamespace(deepClone(payload.history[0]?.variables ?? {}));
7090
- const newPatchEntries = [];
7091
- let prevVars = base;
7092
- for (let i = 1; i < payload.history.length; i++) {
7093
- const currVars = createNamespace(deepClone(payload.history[i].variables));
7094
- newPatchEntries.push(computeVarPatches(prevVars, currVars));
7095
- prevVars = currVars;
7096
- }
7097
- variableBase = deepClone(base);
7098
- patchEntries = newPatchEntries;
7099
- serializedHistory = payload.history.map((m) => savedMoment(m, serialize(m.variables)));
8192
+ const snapshots = deepClone(payload.history.map((m) => m.variables)).map((vars) => createNamespace(vars));
8193
+ variableBase = deepClone(snapshots[0]);
8194
+ patchEntries = snapshots.slice(1).map((curr, i) => computeVarPatches(snapshots[i], curr));
8195
+ sessionMoments = payload.history.map((m, i) => savedMoment(m, snapshots[i]));
7100
8196
  set((state) => {
7101
8197
  state.currentPassage = payload.passage;
7102
8198
  state.navigationId++;
@@ -7116,8 +8212,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
7116
8212
  reinitTriggerState();
7117
8213
  lastNavigationVars = reconstructVarsAt(get().historyIndex);
7118
8214
  restorePRNGFrom(entry?.prng !== void 0 ? entry.prng : payload.prng);
7119
- persistSession(get);
7120
- withoutDraws(() => emit("afterload", slot));
8215
+ persistSessionThen(get, () => withoutDraws(() => emit("afterload", slot)));
7121
8216
  },
7122
8217
  getHistoryVariables: (index) => {
7123
8218
  return deepClone(reconstructVarsAt(index));
@@ -7358,6 +8453,50 @@ function TriggerDialogHost() {
7358
8453
  }, `${dialog.passageName}-${i}`)) });
7359
8454
  }
7360
8455
  //#endregion
8456
+ //#region src/components/RuntimeErrors.tsx
8457
+ /**
8458
+ * The runtime errors shown on the page (see showRuntimeError): one banner
8459
+ * each, announced to screen readers (role="alert"), until the player
8460
+ * dismisses it. Style them with the `spindle-error-banner` classes.
8461
+ */
8462
+ function RuntimeErrors() {
8463
+ const [errors, setErrors] = useState(getRuntimeErrors);
8464
+ useEffect(() => {
8465
+ setErrors(getRuntimeErrors());
8466
+ return subscribeRuntimeErrors(() => setErrors(getRuntimeErrors()));
8467
+ }, []);
8468
+ if (errors.length === 0) return null;
8469
+ return /* @__PURE__ */ jsx("div", {
8470
+ class: "spindle-error-banners",
8471
+ children: errors.map((e) => /* @__PURE__ */ jsxs("div", {
8472
+ class: "spindle-error-banner",
8473
+ role: "alert",
8474
+ children: [
8475
+ /* @__PURE__ */ jsx("span", {
8476
+ class: "spindle-error-banner-context",
8477
+ children: e.context
8478
+ }),
8479
+ " ",
8480
+ /* @__PURE__ */ jsx("span", {
8481
+ class: "spindle-error-banner-message",
8482
+ children: e.message
8483
+ }),
8484
+ e.count > 1 && /* @__PURE__ */ jsx("span", {
8485
+ class: "spindle-error-banner-count",
8486
+ children: ` (×${e.count})`
8487
+ }),
8488
+ /* @__PURE__ */ jsx("button", {
8489
+ type: "button",
8490
+ class: "spindle-error-banner-dismiss",
8491
+ "aria-label": "Dismiss",
8492
+ onClick: () => dismissRuntimeError(e.id),
8493
+ children: "✕"
8494
+ })
8495
+ ]
8496
+ }, e.id))
8497
+ });
8498
+ }
8499
+ //#endregion
7361
8500
  //#region src/components/App.tsx
7362
8501
  function App() {
7363
8502
  const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
@@ -7379,7 +8518,11 @@ function App() {
7379
8518
  class: "loading",
7380
8519
  children: "Loading..."
7381
8520
  });
7382
- const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(StoryInterface, {}), /* @__PURE__ */ jsx(TriggerDialogHost, {})] });
8521
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
8522
+ /* @__PURE__ */ jsx(StoryInterface, {}),
8523
+ /* @__PURE__ */ jsx(TriggerDialogHost, {}),
8524
+ /* @__PURE__ */ jsx(RuntimeErrors, {})
8525
+ ] });
7383
8526
  return nobr ? /* @__PURE__ */ jsx(NobrContext.Provider, {
7384
8527
  value: true,
7385
8528
  children: content
@@ -8800,12 +9943,13 @@ function assignmentArgs(rawArgs, args) {
8800
9943
  * and later recomputations read the same source.
8801
9944
  */
8802
9945
  function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRef, localsUpdate) {
9946
+ const location = currentSourceLocation();
8803
9947
  let newValue;
8804
9948
  try {
8805
9949
  const { variables, temporary, transient } = readState();
8806
9950
  newValue = evaluate(expr, variables, temporary, getLocals(), transient);
8807
9951
  } catch (err) {
8808
- logMacroError(`computed ${rawArgs}`, err);
9952
+ logMacroError(`computed ${rawArgs}`, err, location);
8809
9953
  return;
8810
9954
  }
8811
9955
  if (!deepEqual(prevRef.current, newValue)) {
@@ -8813,7 +9957,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
8813
9957
  if (isLocal) try {
8814
9958
  localsUpdate(name, newValue);
8815
9959
  } catch (err) {
8816
- logMacroError(`computed ${rawArgs}`, err);
9960
+ logMacroError(`computed ${rawArgs}`, err, location);
8817
9961
  }
8818
9962
  else {
8819
9963
  const state = useStoryStore.getState();
@@ -9137,10 +10281,11 @@ defineMacro({
9137
10281
  render({ children = [] }, ctx) {
9138
10282
  const code = ctx.collectText(children);
9139
10283
  ctx.hooks.useLayoutEffect(() => {
10284
+ const location = ctx.sourceLocation();
9140
10285
  try {
9141
10286
  ctx.mutate(code);
9142
10287
  } catch (err) {
9143
- logMacroError("do", err);
10288
+ logMacroError("do", err, location);
9144
10289
  }
9145
10290
  }, []);
9146
10291
  return null;
@@ -9748,7 +10893,7 @@ function SaveManagerContent() {
9748
10893
  };
9749
10894
  const handleLoad = async (save) => {
9750
10895
  try {
9751
- loadFromPayload(deserializePayload(save.payload), void 0, save.meta.playthroughId);
10896
+ loadFromPayload(decodeSavePayload(save.payload), void 0, save.meta.playthroughId);
9752
10897
  showStatus("Game loaded");
9753
10898
  if (closeDialog) closeTimer.current = window.setTimeout(closeDialog, 500);
9754
10899
  await refresh();
@@ -10088,7 +11233,12 @@ defineMacro({
10088
11233
  }],
10089
11234
  render(_props, ctx) {
10090
11235
  ctx.hooks.useLayoutEffect(() => {
10091
- useStoryStore.getState().navigate(evaluatePassageName(ctx.args.passage, ctx.evaluate));
11236
+ const location = ctx.sourceLocation();
11237
+ try {
11238
+ useStoryStore.getState().navigate(evaluatePassageName(ctx.args.passage, ctx.evaluate));
11239
+ } catch (err) {
11240
+ logMacroError("goto", err, location);
11241
+ }
10092
11242
  }, []);
10093
11243
  return null;
10094
11244
  }
@@ -10702,7 +11852,7 @@ defineMacro({
10702
11852
  });
10703
11853
  //#endregion
10704
11854
  //#region src/styles.css?inline
10705
- var styles_default = "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 16px;\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n padding: 0;\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.75;\n color: #e0e0e0;\n background-color: #1a1a2e;\n min-height: 100vh;\n}\n\ntw-storydata {\n display: none !important;\n}\n\n#story {\n max-width: 42em;\n margin: 0 auto;\n padding: 2em 1.5em;\n}\n\n/* Passage transitions */\n\n.passage-container {\n position: relative;\n}\n\n.passage-container--crossfading {\n display: grid;\n}\n\n.passage-container--crossfading > * {\n grid-area: 1 / 1;\n}\n\n.passage {\n animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;\n}\n\n.passage[data-transition='none'] {\n animation: none;\n}\n\n.passage-snapshot {\n animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;\n pointer-events: none;\n user-select: none;\n}\n\n@keyframes passage-fade-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes passage-fade-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(-8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .passage,\n .passage-snapshot {\n animation-duration: 0.01s !important;\n }\n}\n\n.macro-button {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n font-size: 0.95em;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-button:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n.macro-button:active {\n background: rgba(100, 181, 246, 0.35);\n}\n\n.error {\n color: #ef5350;\n font-family: monospace;\n padding: 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.loading {\n text-align: center;\n padding: 2em;\n opacity: 0.6;\n}\n\n/* Menu bar */\n\n.story-menubar {\n position: sticky;\n top: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.5em 1.5em;\n background: #16213e;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-family: system-ui, sans-serif;\n font-size: 0.9em;\n}\n\n.story-title {\n flex: 1;\n font-weight: 600;\n color: #e0e0e0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.menubar-button {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n padding: 0.35em 0.7em;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n transition:\n color 0.2s,\n background 0.2s;\n}\n\n.menubar-button:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.menubar-button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.menubar-button:disabled:hover {\n color: #9e9e9e;\n background: transparent;\n}\n\n/* Generic dialog */\n\n.dialog-overlay {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n}\n\n.dialog-panel {\n background: #16213e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n width: 90%;\n max-width: 500px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n color: #e0e0e0;\n font-family: system-ui, sans-serif;\n position: relative;\n}\n\n/* Focused programmatically when the dialog has no focusable content */\n.dialog-panel:focus {\n outline: none;\n}\n\n.dialog-saves {\n max-width: 550px;\n}\n\n.dialog-settings {\n max-width: 400px;\n}\n\n.dialog-close {\n position: absolute;\n top: 0.75em;\n right: 0.75em;\n z-index: 1;\n background: transparent;\n border: none;\n color: #9e9e9e;\n font-size: 1.1em;\n cursor: pointer;\n padding: 0.25em;\n line-height: 1;\n}\n\n.dialog-close:hover {\n color: #e0e0e0;\n}\n\n.dialog-body {\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Settings controls */\n\n.settings-controls {\n padding: 1em 1.25em;\n display: flex;\n flex-direction: column;\n gap: 1em;\n}\n\n.settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1em;\n}\n\n.settings-row select,\n.settings-row input[type='range'] {\n accent-color: #64b5f6;\n}\n\n.settings-row select {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.settings-row input[type='checkbox'] {\n accent-color: #64b5f6;\n width: 1.1em;\n height: 1.1em;\n}\n\n/* Save manager */\n\n.save-manager {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.saves-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1em 1.25em;\n padding-right: 3em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-weight: 600;\n flex-shrink: 0;\n}\n\n.saves-header-left {\n display: flex;\n align-items: center;\n gap: 0.75em;\n}\n\n.saves-mode-toggle {\n display: flex;\n gap: 0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.saves-mode-toggle button {\n background: transparent;\n color: #9e9e9e;\n border: none;\n padding: 0.3em 0.8em;\n font: inherit;\n font-size: 0.85em;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-mode-toggle button.active {\n background: rgba(100, 181, 246, 0.2);\n color: #64b5f6;\n}\n\n.saves-mode-toggle button:hover:not(.active) {\n color: #e0e0e0;\n}\n\n.saves-body {\n padding: 0.75em 1.25em;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.saves-empty {\n text-align: center;\n padding: 2em 1em;\n opacity: 0.5;\n font-size: 0.9em;\n}\n\n.saves-toolbar {\n display: flex;\n gap: 0.5em;\n padding: 0.5em 1.25em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n}\n\n.saves-toolbar-button {\n background: rgba(255, 255, 255, 0.05);\n color: #9e9e9e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0.3em 0.7em;\n font: inherit;\n font-size: 0.8em;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-toolbar-button:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #e0e0e0;\n}\n\n.playthrough-group {\n margin-bottom: 0.75em;\n}\n\n.playthrough-header {\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.4em 0;\n cursor: pointer;\n user-select: none;\n font-size: 0.85em;\n color: #9e9e9e;\n transition: color 0.2s;\n}\n\n.playthrough-header:hover {\n color: #e0e0e0;\n}\n\n.playthrough-chevron {\n display: inline-block;\n font-size: 0.7em;\n transition: transform 0.2s;\n}\n\n.playthrough-chevron.open {\n transform: rotate(90deg);\n}\n\n.playthrough-label {\n font-weight: 600;\n}\n\n.playthrough-date {\n opacity: 0.6;\n font-size: 0.9em;\n}\n\n.playthrough-saves {\n display: flex;\n flex-direction: column;\n gap: 0.4em;\n padding-left: 0.25em;\n}\n\n.save-slot {\n display: flex;\n align-items: center;\n gap: 0.75em;\n padding: 0.5em 0.6em;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.save-slot:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n\n.save-slot-info {\n flex: 1;\n min-width: 0;\n}\n\n.save-slot-title {\n font-weight: 500;\n font-size: 0.9em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.save-slot-meta {\n font-size: 0.75em;\n color: #9e9e9e;\n display: flex;\n gap: 0.75em;\n}\n\n.save-slot-actions {\n display: flex;\n gap: 0.3em;\n flex-shrink: 0;\n}\n\n.save-slot-action {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 0.2em 0.5em;\n font: inherit;\n font-size: 0.75em;\n cursor: pointer;\n transition:\n color 0.2s,\n background 0.2s;\n white-space: nowrap;\n}\n\n.save-slot-action:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.save-slot-action.primary {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n}\n\n.save-slot-action.primary:hover {\n background: rgba(100, 181, 246, 0.15);\n}\n\n.save-slot-action.danger:hover {\n color: #ef5350;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.save-slot-new {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5em;\n margin-top: 0.25em;\n border: 1px dashed rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n color: #9e9e9e;\n font: inherit;\n font-size: 0.8em;\n background: transparent;\n cursor: pointer;\n width: 100%;\n transition:\n color 0.2s,\n border-color 0.2s,\n background 0.2s;\n}\n\n.save-slot-new:hover {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n background: rgba(100, 181, 246, 0.05);\n}\n\n.saves-status {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #81c784;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n text-align: center;\n}\n\n.saves-status.error {\n color: #ef5350;\n border: none;\n border-radius: 0;\n padding: 0.5em 1.25em;\n font-family: inherit;\n background: transparent;\n}\n\n.save-rename-input {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 3px;\n padding: 0.15em 0.4em;\n font: inherit;\n font-size: 0.9em;\n width: 100%;\n}\n\n.save-rename-input:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n/* Macro: link */\n\n.macro-link {\n color: #64b5f6;\n text-decoration: none;\n border-bottom: 1px solid rgba(100, 181, 246, 0.3);\n transition:\n color 0.2s,\n border-color 0.2s;\n cursor: pointer;\n}\n\n.macro-link:hover {\n color: #90caf9;\n border-bottom-color: #90caf9;\n}\n\n.macro-link:active {\n color: #42a5f5;\n}\n\n/* Macro: form inputs */\n\n.macro-textbox,\n.macro-numberbox,\n.macro-textarea {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.macro-textbox:focus,\n.macro-numberbox:focus,\n.macro-textarea:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n.macro-textarea {\n min-height: 3em;\n resize: vertical;\n}\n\n.macro-checkbox,\n.macro-radiobutton {\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.3em;\n}\n\n.macro-checkbox input,\n.macro-radiobutton input {\n accent-color: #64b5f6;\n}\n\n/* Macro: listbox */\n\n.macro-listbox {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n/* Macro: cycle */\n\n.macro-cycle {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-cycle:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n/* Macro: meter */\n\n.macro-meter {\n position: relative;\n width: 100%;\n height: 1.4em;\n background: #2a2a3e;\n border-radius: 0.3em;\n overflow: hidden;\n border: 1px solid #444;\n}\n\n.macro-meter-fill {\n height: 100%;\n background: #64b5f6;\n transition: width 0.3s ease;\n}\n\n.macro-meter-label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.85em;\n color: #e0e0e0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n/* Markdown wrapper — invisible to layout */\n\n.md {\n display: contents;\n}\n\n/* Markdown block elements inside passages */\n\n.passage p {\n margin: 0.6em 0;\n}\n\n.passage p:first-child {\n margin-top: 0;\n}\n\n.passage p:last-child {\n margin-bottom: 0;\n}\n\n.passage h1,\n.passage h2,\n.passage h3,\n.passage h4,\n.passage h5,\n.passage h6 {\n margin: 1em 0 0.4em;\n line-height: 1.3;\n color: #f0f0f0;\n}\n\n.passage h1 {\n font-size: 1.8em;\n}\n.passage h2 {\n font-size: 1.5em;\n}\n.passage h3 {\n font-size: 1.25em;\n}\n.passage h4 {\n font-size: 1.1em;\n}\n.passage h5 {\n font-size: 1em;\n}\n.passage h6 {\n font-size: 0.9em;\n opacity: 0.8;\n}\n\n.passage code {\n font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;\n font-size: 0.9em;\n background: rgba(255, 255, 255, 0.08);\n padding: 0.15em 0.35em;\n border-radius: 3px;\n}\n\n.passage pre {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 6px;\n padding: 1em;\n overflow-x: auto;\n margin: 0.8em 0;\n}\n\n.passage pre code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.passage blockquote {\n border-left: 3px solid rgba(100, 181, 246, 0.5);\n margin: 0.8em 0;\n padding: 0.4em 1em;\n color: #b0b0b0;\n}\n\n.passage blockquote p {\n margin: 0.3em 0;\n}\n\n.passage ul,\n.passage ol {\n margin: 0.6em 0;\n padding-left: 1.8em;\n}\n\n.passage li {\n margin: 0.2em 0;\n}\n\n.passage hr {\n border: none;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 1.2em 0;\n}\n\n.passage table {\n border-collapse: collapse;\n width: 100%;\n margin: 0.8em 0;\n}\n\n.passage th,\n.passage td {\n border: 1px solid rgba(255, 255, 255, 0.12);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.passage th {\n background: rgba(255, 255, 255, 0.06);\n font-weight: 600;\n}\n\n.passage tr:nth-child(even) {\n background: rgba(255, 255, 255, 0.02);\n}\n\n.passage del {\n opacity: 0.5;\n}\n\n.passage strong {\n color: #f0f0f0;\n}\n\n/* Macro: type (typewriter) */\n\n.macro-type-cursor::after {\n content: '\\2588';\n animation: type-blink 0.7s step-end infinite;\n color: #64b5f6;\n}\n\n.macro-type-done .macro-type-cursor::after {\n display: none;\n}\n\n@keyframes type-blink {\n 50% {\n opacity: 0;\n }\n}\n";
11855
+ var styles_default = "*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-size: 16px;\n -webkit-text-size-adjust: 100%;\n}\n\nbody {\n margin: 0;\n padding: 0;\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.75;\n color: #e0e0e0;\n background-color: #1a1a2e;\n min-height: 100vh;\n}\n\ntw-storydata {\n display: none !important;\n}\n\n#story {\n max-width: 42em;\n margin: 0 auto;\n padding: 2em 1.5em;\n}\n\n/* Passage transitions */\n\n.passage-container {\n position: relative;\n}\n\n.passage-container--crossfading {\n display: grid;\n}\n\n.passage-container--crossfading > * {\n grid-area: 1 / 1;\n}\n\n.passage {\n animation: passage-fade-in var(--passage-in-duration, 0.3s) ease;\n}\n\n.passage[data-transition='none'] {\n animation: none;\n}\n\n.passage-snapshot {\n animation: passage-fade-out var(--passage-out-duration, 0.3s) ease forwards;\n pointer-events: none;\n user-select: none;\n}\n\n@keyframes passage-fade-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes passage-fade-out {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(-8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .passage,\n .passage-snapshot {\n animation-duration: 0.01s !important;\n }\n}\n\n.macro-button {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n font-size: 0.95em;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-button:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n.macro-button:active {\n background: rgba(100, 181, 246, 0.35);\n}\n\n.error {\n color: #ef5350;\n font-family: monospace;\n padding: 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: rgba(239, 83, 80, 0.1);\n}\n\n/* Runtime errors shown on the page (see src/components/RuntimeErrors.tsx) */\n.spindle-error-banners {\n position: fixed;\n top: 0.75em;\n left: 50%;\n z-index: 300;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n width: min(42em, calc(100% - 1.5em));\n transform: translateX(-50%);\n}\n\n.spindle-error-banner {\n position: relative;\n padding: 0.75em 2.75em 0.75em 1em;\n border: 1px solid #ef5350;\n border-radius: 4px;\n background: #2b1a1e;\n color: #ffcdd2;\n font-size: 0.9em;\n line-height: 1.5;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n}\n\n.spindle-error-banner-message {\n font-family: monospace;\n color: #ef9a9a;\n}\n\n.spindle-error-banner-dismiss {\n position: absolute;\n top: 0.5em;\n right: 0.5em;\n background: transparent;\n border: none;\n color: inherit;\n font-size: 1em;\n cursor: pointer;\n}\n\n.spindle-error-banner-dismiss:hover {\n color: #fff;\n}\n\n.loading {\n text-align: center;\n padding: 2em;\n opacity: 0.6;\n}\n\n/* Menu bar */\n\n.story-menubar {\n position: sticky;\n top: 0;\n z-index: 100;\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.5em 1.5em;\n background: #16213e;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-family: system-ui, sans-serif;\n font-size: 0.9em;\n}\n\n.story-title {\n flex: 1;\n font-weight: 600;\n color: #e0e0e0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.menubar-button {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n padding: 0.35em 0.7em;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n transition:\n color 0.2s,\n background 0.2s;\n}\n\n.menubar-button:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.menubar-button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.menubar-button:disabled:hover {\n color: #9e9e9e;\n background: transparent;\n}\n\n/* Generic dialog */\n\n.dialog-overlay {\n position: fixed;\n inset: 0;\n z-index: 200;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n}\n\n.dialog-panel {\n background: #16213e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 8px;\n width: 90%;\n max-width: 500px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n color: #e0e0e0;\n font-family: system-ui, sans-serif;\n position: relative;\n}\n\n/* Focused programmatically when the dialog has no focusable content */\n.dialog-panel:focus {\n outline: none;\n}\n\n.dialog-saves {\n max-width: 550px;\n}\n\n.dialog-settings {\n max-width: 400px;\n}\n\n.dialog-close {\n position: absolute;\n top: 0.75em;\n right: 0.75em;\n z-index: 1;\n background: transparent;\n border: none;\n color: #9e9e9e;\n font-size: 1.1em;\n cursor: pointer;\n padding: 0.25em;\n line-height: 1;\n}\n\n.dialog-close:hover {\n color: #e0e0e0;\n}\n\n.dialog-body {\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n/* Settings controls */\n\n.settings-controls {\n padding: 1em 1.25em;\n display: flex;\n flex-direction: column;\n gap: 1em;\n}\n\n.settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1em;\n}\n\n.settings-row select,\n.settings-row input[type='range'] {\n accent-color: #64b5f6;\n}\n\n.settings-row select {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.settings-row input[type='checkbox'] {\n accent-color: #64b5f6;\n width: 1.1em;\n height: 1.1em;\n}\n\n/* Save manager */\n\n.save-manager {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.saves-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1em 1.25em;\n padding-right: 3em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n font-weight: 600;\n flex-shrink: 0;\n}\n\n.saves-header-left {\n display: flex;\n align-items: center;\n gap: 0.75em;\n}\n\n.saves-mode-toggle {\n display: flex;\n gap: 0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.saves-mode-toggle button {\n background: transparent;\n color: #9e9e9e;\n border: none;\n padding: 0.3em 0.8em;\n font: inherit;\n font-size: 0.85em;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-mode-toggle button.active {\n background: rgba(100, 181, 246, 0.2);\n color: #64b5f6;\n}\n\n.saves-mode-toggle button:hover:not(.active) {\n color: #e0e0e0;\n}\n\n.saves-body {\n padding: 0.75em 1.25em;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.saves-empty {\n text-align: center;\n padding: 2em 1em;\n opacity: 0.5;\n font-size: 0.9em;\n}\n\n.saves-toolbar {\n display: flex;\n gap: 0.5em;\n padding: 0.5em 1.25em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n}\n\n.saves-toolbar-button {\n background: rgba(255, 255, 255, 0.05);\n color: #9e9e9e;\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 0.3em 0.7em;\n font: inherit;\n font-size: 0.8em;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n}\n\n.saves-toolbar-button:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #e0e0e0;\n}\n\n.playthrough-group {\n margin-bottom: 0.75em;\n}\n\n.playthrough-header {\n display: flex;\n align-items: center;\n gap: 0.5em;\n padding: 0.4em 0;\n cursor: pointer;\n user-select: none;\n font-size: 0.85em;\n color: #9e9e9e;\n transition: color 0.2s;\n}\n\n.playthrough-header:hover {\n color: #e0e0e0;\n}\n\n.playthrough-chevron {\n display: inline-block;\n font-size: 0.7em;\n transition: transform 0.2s;\n}\n\n.playthrough-chevron.open {\n transform: rotate(90deg);\n}\n\n.playthrough-label {\n font-weight: 600;\n}\n\n.playthrough-date {\n opacity: 0.6;\n font-size: 0.9em;\n}\n\n.playthrough-saves {\n display: flex;\n flex-direction: column;\n gap: 0.4em;\n padding-left: 0.25em;\n}\n\n.save-slot {\n display: flex;\n align-items: center;\n gap: 0.75em;\n padding: 0.5em 0.6em;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.save-slot:hover {\n background: rgba(255, 255, 255, 0.06);\n}\n\n.save-slot-info {\n flex: 1;\n min-width: 0;\n}\n\n.save-slot-title {\n font-weight: 500;\n font-size: 0.9em;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.save-slot-meta {\n font-size: 0.75em;\n color: #9e9e9e;\n display: flex;\n gap: 0.75em;\n}\n\n.save-slot-actions {\n display: flex;\n gap: 0.3em;\n flex-shrink: 0;\n}\n\n.save-slot-action {\n background: transparent;\n color: #9e9e9e;\n border: 1px solid transparent;\n border-radius: 3px;\n padding: 0.2em 0.5em;\n font: inherit;\n font-size: 0.75em;\n cursor: pointer;\n transition:\n color 0.2s,\n background 0.2s;\n white-space: nowrap;\n}\n\n.save-slot-action:hover {\n color: #e0e0e0;\n background: rgba(255, 255, 255, 0.08);\n}\n\n.save-slot-action.primary {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n}\n\n.save-slot-action.primary:hover {\n background: rgba(100, 181, 246, 0.15);\n}\n\n.save-slot-action.danger:hover {\n color: #ef5350;\n background: rgba(239, 83, 80, 0.1);\n}\n\n.save-slot-new {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5em;\n margin-top: 0.25em;\n border: 1px dashed rgba(255, 255, 255, 0.12);\n border-radius: 6px;\n color: #9e9e9e;\n font: inherit;\n font-size: 0.8em;\n background: transparent;\n cursor: pointer;\n width: 100%;\n transition:\n color 0.2s,\n border-color 0.2s,\n background 0.2s;\n}\n\n.save-slot-new:hover {\n color: #64b5f6;\n border-color: rgba(100, 181, 246, 0.3);\n background: rgba(100, 181, 246, 0.05);\n}\n\n.saves-status {\n padding: 0.5em 1.25em;\n font-size: 0.8em;\n color: #81c784;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n flex-shrink: 0;\n text-align: center;\n}\n\n.saves-status.error {\n color: #ef5350;\n border: none;\n border-radius: 0;\n padding: 0.5em 1.25em;\n font-family: inherit;\n background: transparent;\n}\n\n.save-rename-input {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 3px;\n padding: 0.15em 0.4em;\n font: inherit;\n font-size: 0.9em;\n width: 100%;\n}\n\n.save-rename-input:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n/* Macro: link */\n\n.macro-link {\n color: #64b5f6;\n text-decoration: none;\n border-bottom: 1px solid rgba(100, 181, 246, 0.3);\n transition:\n color 0.2s,\n border-color 0.2s;\n cursor: pointer;\n}\n\n.macro-link:hover {\n color: #90caf9;\n border-bottom-color: #90caf9;\n}\n\n.macro-link:active {\n color: #42a5f5;\n}\n\n/* Macro: form inputs */\n\n.macro-textbox,\n.macro-numberbox,\n.macro-textarea {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n.macro-textbox:focus,\n.macro-numberbox:focus,\n.macro-textarea:focus {\n outline: none;\n border-color: #64b5f6;\n}\n\n.macro-textarea {\n min-height: 3em;\n resize: vertical;\n}\n\n.macro-checkbox,\n.macro-radiobutton {\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.3em;\n}\n\n.macro-checkbox input,\n.macro-radiobutton input {\n accent-color: #64b5f6;\n}\n\n/* Macro: listbox */\n\n.macro-listbox {\n background: #1a1a2e;\n color: #e0e0e0;\n border: 1px solid rgba(255, 255, 255, 0.15);\n border-radius: 4px;\n padding: 0.3em 0.5em;\n font: inherit;\n}\n\n/* Macro: cycle */\n\n.macro-cycle {\n background: rgba(100, 181, 246, 0.15);\n color: #64b5f6;\n border: 1px solid rgba(100, 181, 246, 0.4);\n border-radius: 4px;\n padding: 0.25em 0.75em;\n font: inherit;\n cursor: pointer;\n transition:\n background 0.2s,\n border-color 0.2s;\n}\n\n.macro-cycle:hover {\n background: rgba(100, 181, 246, 0.25);\n border-color: #90caf9;\n}\n\n/* Macro: meter */\n\n.macro-meter {\n position: relative;\n width: 100%;\n height: 1.4em;\n background: #2a2a3e;\n border-radius: 0.3em;\n overflow: hidden;\n border: 1px solid #444;\n}\n\n.macro-meter-fill {\n height: 100%;\n background: #64b5f6;\n transition: width 0.3s ease;\n}\n\n.macro-meter-label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 0.85em;\n color: #e0e0e0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n/* Markdown wrapper — invisible to layout */\n\n.md {\n display: contents;\n}\n\n/* Markdown block elements inside passages */\n\n.passage p {\n margin: 0.6em 0;\n}\n\n.passage p:first-child {\n margin-top: 0;\n}\n\n.passage p:last-child {\n margin-bottom: 0;\n}\n\n.passage h1,\n.passage h2,\n.passage h3,\n.passage h4,\n.passage h5,\n.passage h6 {\n margin: 1em 0 0.4em;\n line-height: 1.3;\n color: #f0f0f0;\n}\n\n.passage h1 {\n font-size: 1.8em;\n}\n.passage h2 {\n font-size: 1.5em;\n}\n.passage h3 {\n font-size: 1.25em;\n}\n.passage h4 {\n font-size: 1.1em;\n}\n.passage h5 {\n font-size: 1em;\n}\n.passage h6 {\n font-size: 0.9em;\n opacity: 0.8;\n}\n\n.passage code {\n font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;\n font-size: 0.9em;\n background: rgba(255, 255, 255, 0.08);\n padding: 0.15em 0.35em;\n border-radius: 3px;\n}\n\n.passage pre {\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 6px;\n padding: 1em;\n overflow-x: auto;\n margin: 0.8em 0;\n}\n\n.passage pre code {\n background: none;\n padding: 0;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.passage blockquote {\n border-left: 3px solid rgba(100, 181, 246, 0.5);\n margin: 0.8em 0;\n padding: 0.4em 1em;\n color: #b0b0b0;\n}\n\n.passage blockquote p {\n margin: 0.3em 0;\n}\n\n.passage ul,\n.passage ol {\n margin: 0.6em 0;\n padding-left: 1.8em;\n}\n\n.passage li {\n margin: 0.2em 0;\n}\n\n.passage hr {\n border: none;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 1.2em 0;\n}\n\n.passage table {\n border-collapse: collapse;\n width: 100%;\n margin: 0.8em 0;\n}\n\n.passage th,\n.passage td {\n border: 1px solid rgba(255, 255, 255, 0.12);\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\n.passage th {\n background: rgba(255, 255, 255, 0.06);\n font-weight: 600;\n}\n\n.passage tr:nth-child(even) {\n background: rgba(255, 255, 255, 0.02);\n}\n\n.passage del {\n opacity: 0.5;\n}\n\n.passage strong {\n color: #f0f0f0;\n}\n\n/* Macro: type (typewriter) */\n\n.macro-type-cursor::after {\n content: '\\2588';\n animation: type-blink 0.7s step-end infinite;\n color: #64b5f6;\n}\n\n.macro-type-done .macro-type-cursor::after {\n display: none;\n}\n\n@keyframes type-blink {\n 50% {\n opacity: 0;\n }\n}\n";
10706
11856
  //#endregion
10707
11857
  //#region src/index.tsx
10708
11858
  function renderErrors(root, errors) {