@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.
- package/README.md +2 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +1731 -581
- package/package.json +5 -1
- package/src/class-registry.ts +252 -250
- package/src/components/App.tsx +2 -0
- package/src/components/RuntimeErrors.tsx +46 -0
- package/src/components/macros/Computed.tsx +5 -2
- package/src/components/macros/Do.tsx +3 -1
- package/src/components/macros/Goto.tsx +13 -3
- package/src/components/macros/MacroError.tsx +14 -7
- package/src/components/macros/SaveManager.tsx +2 -2
- package/src/runtime-errors.ts +65 -0
- package/src/saves/format.ts +199 -0
- package/src/saves/save-manager.ts +45 -63
- package/src/saves/types.ts +71 -88
- package/src/store.ts +167 -87
- package/src/structural.ts +266 -57
- package/src/styles.css +45 -0
- package/src/utils/object-path.ts +13 -12
- package/src/utils/stable-key.ts +2 -1
- package/src/utils/value-kinds.ts +45 -0
- package/types/index.d.ts +32 -8
package/dist/pkg/headless.js
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
3716
|
-
|
|
3717
|
-
|
|
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 (`&`, `{`) 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
|
|
4274
|
-
|
|
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
|
-
*
|
|
4279
|
-
*
|
|
4280
|
-
*
|
|
4281
|
-
* Includes character references (`&`, `{`) 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
|
|
4289
|
-
/**
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
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
|
-
*
|
|
4296
|
-
* (
|
|
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
|
-
|
|
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
|
-
*
|
|
4308
|
-
*
|
|
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
|
|
4311
|
-
if (!
|
|
4312
|
-
|
|
4313
|
-
|
|
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
|
-
*
|
|
4320
|
-
*
|
|
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
|
|
4325
|
-
|
|
4326
|
-
|
|
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
|
-
*
|
|
4330
|
-
*
|
|
4331
|
-
*
|
|
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
|
|
4343
|
-
if (
|
|
4344
|
-
if (
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
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
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
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
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
if (!
|
|
4376
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4834
|
-
*
|
|
4835
|
-
*
|
|
4836
|
-
* are compared by length and index
|
|
4837
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
4844
|
-
*
|
|
4845
|
-
* cycles
|
|
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
|
|
4852
|
-
if (
|
|
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
|
-
|
|
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
|
|
4881
|
-
*
|
|
4882
|
-
* merge by (a shift moves
|
|
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
|
|
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
|
|
6242
|
+
const encoded = encodePayload(payload);
|
|
5245
6243
|
return {
|
|
5246
6244
|
meta: {
|
|
5247
6245
|
...meta,
|
|
5248
|
-
estimatedBytes: estimatePayloadBytes(
|
|
6246
|
+
estimatedBytes: estimatePayloadBytes(encoded)
|
|
5249
6247
|
},
|
|
5250
|
-
payload:
|
|
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
|
-
*
|
|
5305
|
-
*
|
|
5306
|
-
*
|
|
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
|
-
|
|
5309
|
-
|
|
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 =
|
|
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
|
|
5532
|
-
*
|
|
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,
|
|
6532
|
+
function saveSession(ifid, payload) {
|
|
6533
|
+
const text = JSON.stringify(encodePayload(payload));
|
|
5535
6534
|
try {
|
|
5536
|
-
sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`,
|
|
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
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
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
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
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
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
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 =
|
|
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 =
|
|
7361
|
+
for (let i = 0; i < index; i++) vars = applyVarPatches(vars, patchEntries[i].forward);
|
|
6300
7362
|
return vars;
|
|
6301
7363
|
}
|
|
6302
|
-
|
|
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 (
|
|
6307
|
-
if (
|
|
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
|
-
|
|
7381
|
+
sessionMoments[i] = savedMoment(history[i], variables);
|
|
6310
7382
|
} else forEachRecorded(history.length, (vars, i) => {
|
|
6311
|
-
if (i >=
|
|
7383
|
+
if (i >= sessionMoments.length) sessionMoments[i] = savedMoment(history[i], vars);
|
|
6312
7384
|
});
|
|
6313
7385
|
saveSession(storyData.ifid, {
|
|
6314
7386
|
passage: currentPassage,
|
|
6315
|
-
variables
|
|
6316
|
-
history:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
6373
|
-
*
|
|
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) => ({ ...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
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
|
-
|
|
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
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
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
|
-
|
|
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: [
|
|
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(
|
|
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
|
-
|
|
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) {
|