@rohal12/spindle 0.52.8 → 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 +1886 -611
- 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/markup/render.tsx +117 -53
- package/src/parser.ts +119 -13
- 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;
|
|
@@ -3767,6 +3524,7 @@ var RawTextContext = createContext(false);
|
|
|
3767
3524
|
* no live form properties, so they are set as written (see splitAttributes).
|
|
3768
3525
|
*/
|
|
3769
3526
|
var SvgContext = createContext(false);
|
|
3527
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
3770
3528
|
/** Elements whose content is literal text, not markdown. */
|
|
3771
3529
|
var PREFORMATTED_ELEMENTS = new Set(["pre", "textarea"]);
|
|
3772
3530
|
var WidgetChildrenContext = createContext(null);
|
|
@@ -3811,20 +3569,42 @@ function expandPlaceholderText(text, ph) {
|
|
|
3811
3569
|
return splitPlaceholderText(text, ph).map((part) => typeof part === "number" ? ph.components[part] : part);
|
|
3812
3570
|
}
|
|
3813
3571
|
/**
|
|
3814
|
-
* An element whose attributes contain placeholders (a
|
|
3815
|
-
* text or a link title). Each such attribute is
|
|
3816
|
-
* the store and locals, so it follows its
|
|
3572
|
+
* An element of markdown output whose attributes contain placeholders (a
|
|
3573
|
+
* variable in image alt text or a link title). Each such attribute is
|
|
3574
|
+
* evaluated as text against the store and locals, so it follows its
|
|
3575
|
+
* variables as the component would.
|
|
3817
3576
|
*/
|
|
3818
|
-
function PlaceholderAttributes({ tag,
|
|
3577
|
+
function PlaceholderAttributes({ tag, attributes, placeholders, svg, children }) {
|
|
3819
3578
|
const scope = useTextScope();
|
|
3820
|
-
const resolved = { ...props };
|
|
3821
3579
|
const errors = [];
|
|
3822
|
-
|
|
3580
|
+
const { props, direct } = splitAttributes(attributes.map(([name, value, written]) => {
|
|
3581
|
+
const nodes = placeholders[name];
|
|
3582
|
+
if (!nodes) return [
|
|
3583
|
+
name,
|
|
3584
|
+
value,
|
|
3585
|
+
written
|
|
3586
|
+
];
|
|
3823
3587
|
const result = renderText(nodes, scope);
|
|
3824
|
-
resolved[name] = result.text;
|
|
3825
3588
|
for (const error of result.errors) errors.push([name, error]);
|
|
3826
|
-
|
|
3827
|
-
|
|
3589
|
+
return [
|
|
3590
|
+
name,
|
|
3591
|
+
result.text,
|
|
3592
|
+
written
|
|
3593
|
+
];
|
|
3594
|
+
}), svg);
|
|
3595
|
+
const ref = useDirectAttributes(direct);
|
|
3596
|
+
return withAttributeErrors(errors, h(tag, ref ? {
|
|
3597
|
+
...props,
|
|
3598
|
+
ref
|
|
3599
|
+
} : props, ...children));
|
|
3600
|
+
}
|
|
3601
|
+
/** An element of markdown output with attributes to set directly. */
|
|
3602
|
+
function DirectAttributes({ tag, props, direct, children }) {
|
|
3603
|
+
const ref = useDirectAttributes(direct);
|
|
3604
|
+
return h(tag, {
|
|
3605
|
+
...props,
|
|
3606
|
+
ref
|
|
3607
|
+
}, ...children);
|
|
3828
3608
|
}
|
|
3829
3609
|
/**
|
|
3830
3610
|
* Show the errors met in an element's attributes in front of it, the way a
|
|
@@ -3846,16 +3626,29 @@ function placeholderIndex(node, ph) {
|
|
|
3846
3626
|
return parseInt(value.slice(prefix.length), 10);
|
|
3847
3627
|
}
|
|
3848
3628
|
/**
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3629
|
+
* Parse HTML into nodes without side effects. The content of a `<template>`
|
|
3630
|
+
* belongs to a document with no browsing context, so parsing it fetches no
|
|
3631
|
+
* images and media, runs no inline event handlers (`<img onerror>`) and
|
|
3632
|
+
* constructs no custom elements. A detached `<div>` belongs to the page's
|
|
3633
|
+
* document, which does all of these while parsing.
|
|
3634
|
+
*/
|
|
3635
|
+
function parseHtmlInert(html) {
|
|
3636
|
+
const template = document.createElement("template");
|
|
3637
|
+
template.innerHTML = html;
|
|
3638
|
+
return template.content;
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Convert an HTML string (from micromark) to Preact VNodes,
|
|
3642
|
+
* replacing placeholder elements with pre-rendered components.
|
|
3643
|
+
* With `unwrapParagraphs` (nobr or inline content), top-level <p> wrappers are
|
|
3852
3644
|
* replaced by their children.
|
|
3853
3645
|
*/
|
|
3854
3646
|
function htmlToPreact(html, ph, unwrapParagraphs = false) {
|
|
3855
|
-
const
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3647
|
+
const content = parseHtmlInert(html.trim());
|
|
3648
|
+
if (unwrapParagraphs) {
|
|
3649
|
+
for (const child of Array.from(content.childNodes)) if (child.nodeType === Node.ELEMENT_NODE && child.nodeName === "P") child.replaceWith(...Array.from(child.childNodes));
|
|
3650
|
+
}
|
|
3651
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: Array.from(content.childNodes).map((child, i) => convertDomNode(child, i, ph)) });
|
|
3859
3652
|
}
|
|
3860
3653
|
function convertDomNode(node, key, ph) {
|
|
3861
3654
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -3869,25 +3662,36 @@ function convertDomNode(node, key, ph) {
|
|
|
3869
3662
|
const tag = el.localName;
|
|
3870
3663
|
const idx = placeholderIndex(el, ph);
|
|
3871
3664
|
if (idx !== -1) return ph.components[idx];
|
|
3872
|
-
const
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3665
|
+
const svg = el.namespaceURI === SVG_NAMESPACE;
|
|
3666
|
+
const attributes = [];
|
|
3667
|
+
let placeholders;
|
|
3668
|
+
for (const { name, value } of Array.from(el.attributes)) {
|
|
3669
|
+
attributes.push([
|
|
3670
|
+
name,
|
|
3671
|
+
value,
|
|
3672
|
+
value
|
|
3673
|
+
]);
|
|
3674
|
+
const parts = splitPlaceholderText(value, ph);
|
|
3675
|
+
if (parts.every((part) => typeof part === "string")) continue;
|
|
3676
|
+
placeholders ??= {};
|
|
3677
|
+
placeholders[name] = parts.map((part) => typeof part === "string" ? {
|
|
3882
3678
|
type: "text",
|
|
3883
3679
|
value: part
|
|
3884
3680
|
} : ph.nodes[part]);
|
|
3885
3681
|
}
|
|
3886
3682
|
const children = Array.from(el.childNodes).map((child, i) => convertDomNode(child, i, ph));
|
|
3887
|
-
if (
|
|
3683
|
+
if (placeholders) return /* @__PURE__ */ jsx(PlaceholderAttributes, {
|
|
3684
|
+
tag,
|
|
3685
|
+
attributes,
|
|
3686
|
+
placeholders,
|
|
3687
|
+
svg,
|
|
3688
|
+
children
|
|
3689
|
+
}, key);
|
|
3690
|
+
const { props, direct } = splitAttributes(attributes, svg);
|
|
3691
|
+
if (direct.length > 0) return /* @__PURE__ */ jsx(DirectAttributes, {
|
|
3888
3692
|
tag,
|
|
3889
3693
|
props,
|
|
3890
|
-
|
|
3694
|
+
direct,
|
|
3891
3695
|
children
|
|
3892
3696
|
}, key);
|
|
3893
3697
|
return h(tag, {
|
|
@@ -4002,9 +3806,7 @@ function setRawAttribute(el, name, value) {
|
|
|
4002
3806
|
el.setAttribute(name, value);
|
|
4003
3807
|
return;
|
|
4004
3808
|
}
|
|
4005
|
-
const
|
|
4006
|
-
template.innerHTML = `<i ${name}=""></i>`;
|
|
4007
|
-
const parsed = template.content.firstChild.attributes[0];
|
|
3809
|
+
const parsed = parseHtmlInert(`<i ${name}=""></i>`).firstChild.attributes[0];
|
|
4008
3810
|
if (!parsed) return;
|
|
4009
3811
|
const attr = parsed.cloneNode();
|
|
4010
3812
|
attr.value = value;
|
|
@@ -4042,6 +3844,23 @@ function splitAttributes(attributes, svg) {
|
|
|
4042
3844
|
direct
|
|
4043
3845
|
};
|
|
4044
3846
|
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Set the attributes `splitAttributes` leaves to set directly on the element
|
|
3849
|
+
* given the returned ref (none if there are none), and remove them again
|
|
3850
|
+
* when they change.
|
|
3851
|
+
*/
|
|
3852
|
+
function useDirectAttributes(direct) {
|
|
3853
|
+
const elementRef = useRef(null);
|
|
3854
|
+
useLayoutEffect(() => {
|
|
3855
|
+
const el = elementRef.current;
|
|
3856
|
+
if (!el || direct.length === 0) return;
|
|
3857
|
+
for (const [name, value] of direct) setRawAttribute(el, name, value);
|
|
3858
|
+
return () => {
|
|
3859
|
+
for (const [name] of direct) el.removeAttribute(name);
|
|
3860
|
+
};
|
|
3861
|
+
}, [JSON.stringify(direct)]);
|
|
3862
|
+
return direct.length > 0 ? elementRef : void 0;
|
|
3863
|
+
}
|
|
4045
3864
|
var decodedAttributeText = /* @__PURE__ */ new Map();
|
|
4046
3865
|
/**
|
|
4047
3866
|
* Decode character references (`&`, `{`) in attribute text the way
|
|
@@ -4051,9 +3870,7 @@ function decodeAttributeText(text) {
|
|
|
4051
3870
|
if (!text.includes("&")) return text;
|
|
4052
3871
|
let decoded = decodedAttributeText.get(text);
|
|
4053
3872
|
if (decoded === void 0) {
|
|
4054
|
-
|
|
4055
|
-
template.innerHTML = `<i title="${text.replace(/"/g, """)}"></i>`;
|
|
4056
|
-
decoded = template.content.firstChild.getAttribute("title") ?? text;
|
|
3873
|
+
decoded = parseHtmlInert(`<i title="${text.replace(/"/g, """)}"></i>`).firstChild.getAttribute("title") ?? text;
|
|
4057
3874
|
decodedAttributeText.set(text, decoded);
|
|
4058
3875
|
}
|
|
4059
3876
|
return decoded;
|
|
@@ -4118,16 +3935,8 @@ function HtmlNodeRenderer({ node }) {
|
|
|
4118
3935
|
resolveAttributeValue(k, v, scope, errors),
|
|
4119
3936
|
v
|
|
4120
3937
|
]), inSvg || isSvgRoot);
|
|
4121
|
-
const
|
|
4122
|
-
|
|
4123
|
-
const el = elementRef.current;
|
|
4124
|
-
if (!el || direct.length === 0) return;
|
|
4125
|
-
for (const [name, value] of direct) setRawAttribute(el, name, value);
|
|
4126
|
-
return () => {
|
|
4127
|
-
for (const [name] of direct) el.removeAttribute(name);
|
|
4128
|
-
};
|
|
4129
|
-
}, [JSON.stringify(direct)]);
|
|
4130
|
-
if (direct.length > 0) props.ref = elementRef;
|
|
3938
|
+
const ref = useDirectAttributes(direct);
|
|
3939
|
+
if (ref) props.ref = ref;
|
|
4131
3940
|
const isInline = INLINE_ELEMENTS.has(tag);
|
|
4132
3941
|
let children = void 0;
|
|
4133
3942
|
if (node.children.length > 0) if (inRaw || isRawRoot) children = renderInlineNodes(node.children);
|
|
@@ -4240,88 +4049,1183 @@ var PLACEHOLDER_STRIP_RE = /<span data-tw=[0-9a-z]+:\d+><\/span>/g;
|
|
|
4240
4049
|
/** Whitespace that markdown strips at the start and end of a paragraph. */
|
|
4241
4050
|
var EDGE_WS = " \r\n";
|
|
4242
4051
|
/**
|
|
4243
|
-
* Index of the first character from `from` on (`step` 1) or before `from`
|
|
4244
|
-
* (`step` -1) that is not in `chars`, or where the run of them ends. A loop,
|
|
4245
|
-
* not a regex: `/[ \t]*$/` and the like try each position of a whitespace
|
|
4246
|
-
* run, taking quadratic time on a long run in the middle of a passage.
|
|
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.
|
|
4247
5051
|
*/
|
|
4248
|
-
function
|
|
4249
|
-
|
|
4250
|
-
if (step === 1) while (i < s.length && chars.includes(s[i])) i++;
|
|
4251
|
-
else while (i > 0 && chars.includes(s[i - 1])) i--;
|
|
4252
|
-
return i;
|
|
5052
|
+
function deserialize(text) {
|
|
5053
|
+
return parse(text, revivers);
|
|
4253
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
|
+
};
|
|
4254
5062
|
/**
|
|
4255
|
-
*
|
|
4256
|
-
*
|
|
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.
|
|
4257
5066
|
*/
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
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;
|
|
4265
5082
|
}
|
|
4266
5083
|
/**
|
|
4267
|
-
*
|
|
4268
|
-
*
|
|
4269
|
-
* one paragraph, so like micromark it drops spaces and tabs around line
|
|
4270
|
-
* endings; its edges are already split off by the caller.
|
|
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).
|
|
4271
5086
|
*/
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
5087
|
+
var countsToMap = (counts) => counts && new Map(Object.entries(counts));
|
|
5088
|
+
/**
|
|
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)`.
|
|
5092
|
+
*/
|
|
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);
|
|
4275
5097
|
}
|
|
4276
5098
|
/**
|
|
4277
|
-
*
|
|
4278
|
-
*
|
|
4279
|
-
* Combines all nodes into a single markdown document, using placeholder
|
|
4280
|
-
* elements (see Placeholders) for non-text nodes (variables, macros, links,
|
|
4281
|
-
* HTML). This allows
|
|
4282
|
-
* markdown syntax to span across Twine tokens — e.g., markdown tables can
|
|
4283
|
-
* contain {$variables} and {macros} in their cells.
|
|
4284
|
-
*
|
|
4285
|
-
* After micromark processes the combined string, the HTML is parsed back into
|
|
4286
|
-
* Preact VNodes with placeholders replaced by the real rendered components.
|
|
4287
|
-
* Placeholders that micromark escaped as code text are swapped back too (see
|
|
4288
|
-
* PLACEHOLDER_TEXT_RE), so variables in code stay live components.
|
|
5099
|
+
* The stored form of `payload`. Throws, naming the variable, when the
|
|
5100
|
+
* payload holds a value a save cannot (see serialize()).
|
|
4289
5101
|
*/
|
|
4290
|
-
function
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
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;
|
|
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");
|
|
5131
|
+
/**
|
|
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.
|
|
5135
|
+
*/
|
|
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)
|
|
4299
5163
|
};
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
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;
|
|
4312
5191
|
}
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
if (!
|
|
4324
|
-
|
|
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;
|
|
4325
5229
|
}
|
|
4326
5230
|
//#endregion
|
|
4327
5231
|
//#region src/saves/storage.ts
|
|
@@ -4622,6 +5526,28 @@ function getBackendType() {
|
|
|
4622
5526
|
return _backend?.type ?? "memory";
|
|
4623
5527
|
}
|
|
4624
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
|
|
4625
5551
|
//#region src/utils/object-path.ts
|
|
4626
5552
|
/**
|
|
4627
5553
|
* Traverse dot-path segments on an object and return the nested value.
|
|
@@ -4645,7 +5571,7 @@ var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
|
4645
5571
|
* Built-ins whose content is not their properties: a property written into
|
|
4646
5572
|
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
4647
5573
|
*/
|
|
4648
|
-
var builtinName =
|
|
5574
|
+
var builtinName = atomicName;
|
|
4649
5575
|
/**
|
|
4650
5576
|
* Shallow copy that keeps the prototype, so a registered class instance
|
|
4651
5577
|
* stays an instance of its class (with the same own keys deepClone copies).
|
|
@@ -4697,6 +5623,13 @@ function deleteByPath(root, segments) {
|
|
|
4697
5623
|
}
|
|
4698
5624
|
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
4699
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
|
+
}
|
|
4700
5633
|
delete parent[last];
|
|
4701
5634
|
}
|
|
4702
5635
|
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
@@ -4742,66 +5675,199 @@ function isPlainObject(value) {
|
|
|
4742
5675
|
const proto = Object.getPrototypeOf(value);
|
|
4743
5676
|
return proto === Object.prototype || proto === null;
|
|
4744
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
|
+
];
|
|
4745
5684
|
function deepClone(value, options = {}) {
|
|
4746
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
|
+
}
|
|
4747
5721
|
function clone(val) {
|
|
4748
5722
|
if (val === null || typeof val !== "object") return val;
|
|
4749
5723
|
const obj = val;
|
|
4750
5724
|
if (seen.has(obj)) return seen.get(obj);
|
|
4751
|
-
if (val instanceof Date) return new Date(val.getTime());
|
|
4752
|
-
if (val instanceof RegExp) return new RegExp(val.source, val.flags);
|
|
4753
5725
|
if (Array.isArray(val)) {
|
|
4754
|
-
const arr =
|
|
4755
|
-
|
|
4756
|
-
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]);
|
|
4757
5728
|
return arr;
|
|
4758
5729
|
}
|
|
4759
5730
|
if (val instanceof Map) {
|
|
4760
|
-
const copy = /* @__PURE__ */ new Map();
|
|
4761
|
-
seen.set(obj, copy);
|
|
5731
|
+
const copy = keep(obj, /* @__PURE__ */ new Map());
|
|
4762
5732
|
for (const [k, v] of val) copy.set(clone(k), clone(v));
|
|
4763
5733
|
return copy;
|
|
4764
5734
|
}
|
|
4765
5735
|
if (val instanceof Set) {
|
|
4766
|
-
const copy = /* @__PURE__ */ new Set();
|
|
4767
|
-
seen.set(obj, copy);
|
|
5736
|
+
const copy = keep(obj, /* @__PURE__ */ new Set());
|
|
4768
5737
|
for (const v of val) copy.add(clone(v));
|
|
4769
5738
|
return copy;
|
|
4770
5739
|
}
|
|
5740
|
+
const builtin = cloneBuiltin(obj);
|
|
5741
|
+
if (builtin !== void 0) return builtin;
|
|
4771
5742
|
const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
|
|
4772
5743
|
if (!keepsProto && options.keepUnregistered) return val;
|
|
4773
|
-
|
|
4774
|
-
seen.set(obj, copy);
|
|
4775
|
-
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
4776
|
-
return copy;
|
|
5744
|
+
return copyKeys(obj, keep(obj, keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {}));
|
|
4777
5745
|
}
|
|
4778
5746
|
return clone(value);
|
|
4779
5747
|
}
|
|
4780
5748
|
/**
|
|
4781
|
-
* Structural equality over the value types deepClone() supports
|
|
4782
|
-
*
|
|
4783
|
-
*
|
|
4784
|
-
* are compared by length and index
|
|
4785
|
-
*
|
|
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).
|
|
4786
5755
|
*/
|
|
4787
5756
|
function deepEqual(a, b) {
|
|
4788
|
-
return equal(a, b,
|
|
5757
|
+
return equal(a, b, loosePairs());
|
|
4789
5758
|
}
|
|
4790
5759
|
/**
|
|
4791
|
-
*
|
|
4792
|
-
*
|
|
4793
|
-
*
|
|
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.
|
|
4794
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
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
/**
|
|
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.
|
|
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). */
|
|
4795
5865
|
function equal(a, b, assumed) {
|
|
4796
|
-
if (Object.is(a, b)) return true;
|
|
5866
|
+
if (Object.is(a, b) && !(assumed.strict && isObjectValue(a))) return true;
|
|
4797
5867
|
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
4798
5868
|
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
4799
|
-
const
|
|
4800
|
-
if (
|
|
4801
|
-
if (pairs) pairs.add(b);
|
|
4802
|
-
else assumed.set(a, new Set([b]));
|
|
4803
|
-
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
4804
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
5869
|
+
const met = assumed(a, b);
|
|
5870
|
+
if (met !== void 0) return met;
|
|
4805
5871
|
if (a instanceof Map || a instanceof Set) {
|
|
4806
5872
|
const bc = b;
|
|
4807
5873
|
if (a.size !== bc.size) return false;
|
|
@@ -4810,27 +5876,25 @@ function equal(a, b, assumed) {
|
|
|
4810
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;
|
|
4811
5877
|
return true;
|
|
4812
5878
|
}
|
|
5879
|
+
const builtin = equalBuiltin(a, b, assumed);
|
|
5880
|
+
if (builtin !== void 0) return builtin;
|
|
4813
5881
|
if (Array.isArray(a)) {
|
|
4814
5882
|
const bc = b;
|
|
4815
5883
|
if (a.length !== bc.length) return false;
|
|
4816
|
-
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
5884
|
+
for (let i = 0; i < a.length; i++) if (i in a !== i in bc || !equal(a[i], bc[i], assumed)) return false;
|
|
4817
5885
|
return true;
|
|
4818
5886
|
}
|
|
4819
|
-
|
|
4820
|
-
const bo = b;
|
|
4821
|
-
const keys = Object.keys(ao);
|
|
4822
|
-
if (keys.length !== Object.keys(bo).length) return false;
|
|
4823
|
-
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
4824
|
-
return true;
|
|
5887
|
+
return equalKeys(a, b, assumed);
|
|
4825
5888
|
}
|
|
4826
5889
|
/**
|
|
4827
5890
|
* Objects merged property by property: plain objects and (registered)
|
|
4828
|
-
* class instances. Arrays
|
|
4829
|
-
*
|
|
4830
|
-
* 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.
|
|
4831
5895
|
*/
|
|
4832
5896
|
function isMergeable(value) {
|
|
4833
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value
|
|
5897
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !isAtomic(value);
|
|
4834
5898
|
}
|
|
4835
5899
|
/**
|
|
4836
5900
|
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
@@ -4969,13 +6033,13 @@ var VALID_EVENTS = new Set([
|
|
|
4969
6033
|
"passagerender",
|
|
4970
6034
|
"dialogrender"
|
|
4971
6035
|
]);
|
|
4972
|
-
var listeners = /* @__PURE__ */ new Map();
|
|
6036
|
+
var listeners$1 = /* @__PURE__ */ new Map();
|
|
4973
6037
|
function on(event, cb) {
|
|
4974
6038
|
if (!VALID_EVENTS.has(event)) throw new Error(`spindle: Unknown event "${event}".`);
|
|
4975
|
-
let set = listeners.get(event);
|
|
6039
|
+
let set = listeners$1.get(event);
|
|
4976
6040
|
if (!set) {
|
|
4977
6041
|
set = /* @__PURE__ */ new Set();
|
|
4978
|
-
listeners.set(event, set);
|
|
6042
|
+
listeners$1.set(event, set);
|
|
4979
6043
|
}
|
|
4980
6044
|
set.add(cb);
|
|
4981
6045
|
return () => {
|
|
@@ -4984,7 +6048,7 @@ function on(event, cb) {
|
|
|
4984
6048
|
}
|
|
4985
6049
|
/** Call `call` with each listener of `event`. */
|
|
4986
6050
|
function forEachListener(event, call) {
|
|
4987
|
-
const set = listeners.get(event);
|
|
6051
|
+
const set = listeners$1.get(event);
|
|
4988
6052
|
if (!set) return;
|
|
4989
6053
|
for (const cb of [...set]) call(cb);
|
|
4990
6054
|
}
|
|
@@ -5173,29 +6237,15 @@ function saveWithHooks(slot, custom, beginCapture, write) {
|
|
|
5173
6237
|
return Promise.reject(err);
|
|
5174
6238
|
}
|
|
5175
6239
|
}
|
|
5176
|
-
/**
|
|
5177
|
-
* `payload` with `convert` applied to its live variables and to those of
|
|
5178
|
-
* every history moment.
|
|
5179
|
-
*/
|
|
5180
|
-
function mapVariables(payload, convert) {
|
|
5181
|
-
return {
|
|
5182
|
-
...payload,
|
|
5183
|
-
variables: convert(payload.variables),
|
|
5184
|
-
history: payload.history.map((m) => ({
|
|
5185
|
-
...m,
|
|
5186
|
-
variables: convert(m.variables)
|
|
5187
|
-
}))
|
|
5188
|
-
};
|
|
5189
|
-
}
|
|
5190
|
-
/** 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. */
|
|
5191
6241
|
function storedRecord(meta, payload) {
|
|
5192
|
-
const
|
|
6242
|
+
const encoded = encodePayload(payload);
|
|
5193
6243
|
return {
|
|
5194
6244
|
meta: {
|
|
5195
6245
|
...meta,
|
|
5196
|
-
estimatedBytes: estimatePayloadBytes(
|
|
6246
|
+
estimatedBytes: estimatePayloadBytes(encoded)
|
|
5197
6247
|
},
|
|
5198
|
-
payload:
|
|
6248
|
+
payload: encoded
|
|
5199
6249
|
};
|
|
5200
6250
|
}
|
|
5201
6251
|
var createSave = queued(createSaveNow);
|
|
@@ -5249,15 +6299,13 @@ async function overwriteSaveNow(saveId, payload, custom, playthroughId) {
|
|
|
5249
6299
|
return updated;
|
|
5250
6300
|
}
|
|
5251
6301
|
/**
|
|
5252
|
-
*
|
|
5253
|
-
*
|
|
5254
|
-
*
|
|
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.
|
|
5255
6305
|
*/
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
/** The live payload of a stored save, if there is one. */
|
|
5260
|
-
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);
|
|
5261
6309
|
readingSave(livePayload);
|
|
5262
6310
|
/**
|
|
5263
6311
|
* Delete a save record. If the default slot or a named slot holds it, that
|
|
@@ -5426,7 +6474,7 @@ function loadSlotSave(ifid, slot) {
|
|
|
5426
6474
|
return inOrder(async () => {
|
|
5427
6475
|
const record = await slotRecord(ifid, slot);
|
|
5428
6476
|
if (!record) return void 0;
|
|
5429
|
-
const payload =
|
|
6477
|
+
const payload = decodePayload(record.payload);
|
|
5430
6478
|
const { playthroughId } = record.meta;
|
|
5431
6479
|
if (playthroughId) await adoptPlaythroughNow(ifid, playthroughId);
|
|
5432
6480
|
return {
|
|
@@ -5476,22 +6524,32 @@ var deleteSlotSave = queued(async (ifid, slot) => {
|
|
|
5476
6524
|
});
|
|
5477
6525
|
var SESSION_KEY_PREFIX = "spindle.session.";
|
|
5478
6526
|
/**
|
|
5479
|
-
* Write
|
|
5480
|
-
*
|
|
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.
|
|
5481
6531
|
*/
|
|
5482
|
-
function saveSession(ifid,
|
|
6532
|
+
function saveSession(ifid, payload) {
|
|
6533
|
+
const text = JSON.stringify(encodePayload(payload));
|
|
5483
6534
|
try {
|
|
5484
|
-
sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`,
|
|
6535
|
+
sessionStorage.setItem(`${SESSION_KEY_PREFIX}${ifid}`, text);
|
|
5485
6536
|
} catch {}
|
|
5486
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
|
+
*/
|
|
5487
6543
|
function loadSession(ifid) {
|
|
5488
6544
|
try {
|
|
5489
6545
|
const raw = sessionStorage.getItem(`${SESSION_KEY_PREFIX}${ifid}`);
|
|
5490
6546
|
if (!raw) return void 0;
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
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
|
+
}
|
|
5495
6553
|
return;
|
|
5496
6554
|
}
|
|
5497
6555
|
}
|
|
@@ -5504,7 +6562,7 @@ function clearSession(ifid) {
|
|
|
5504
6562
|
function toExport(record) {
|
|
5505
6563
|
if (!record) return void 0;
|
|
5506
6564
|
return {
|
|
5507
|
-
|
|
6565
|
+
formatVersion: 1,
|
|
5508
6566
|
ifid: record.meta.ifid,
|
|
5509
6567
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5510
6568
|
save: record
|
|
@@ -5517,9 +6575,8 @@ var exportSave = readingSave(toExport);
|
|
|
5517
6575
|
* The returned record is not stored yet.
|
|
5518
6576
|
*/
|
|
5519
6577
|
async function prepareImport(data, ifid) {
|
|
5520
|
-
|
|
6578
|
+
checkSaveExport(data);
|
|
5521
6579
|
if (data.ifid !== ifid) throw new Error(`Save is from a different story (expected IFID ${ifid}, got ${data.ifid})`);
|
|
5522
|
-
if (!isSaveExport(data)) throw new Error("Invalid save file format");
|
|
5523
6580
|
const record = deepClone(data.save);
|
|
5524
6581
|
record.meta.id = crypto.randomUUID();
|
|
5525
6582
|
record.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -6168,6 +7225,48 @@ function isDialogShowing() {
|
|
|
6168
7225
|
return dialogHostCallbacks?.isOpen() ?? false;
|
|
6169
7226
|
}
|
|
6170
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
|
|
6171
7270
|
//#region src/store.ts
|
|
6172
7271
|
enablePatches();
|
|
6173
7272
|
enableMapSet();
|
|
@@ -6193,24 +7292,39 @@ var variableBase = createNamespace();
|
|
|
6193
7292
|
var patchEntries = [];
|
|
6194
7293
|
/** Immer-produced reference to variables right after the last navigation. */
|
|
6195
7294
|
var lastNavigationVars = createNamespace();
|
|
6196
|
-
/**
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
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
|
+
*/
|
|
6204
7303
|
function computeVarPatches(prev, curr) {
|
|
6205
|
-
const
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
7304
|
+
const forward = [];
|
|
7305
|
+
for (const key of Object.keys(prev)) if (!hasOwn(curr, key)) forward.push({
|
|
7306
|
+
key,
|
|
7307
|
+
deleted: true
|
|
6209
7308
|
});
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
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;
|
|
6214
7328
|
}
|
|
6215
7329
|
/**
|
|
6216
7330
|
* Replace the variable snapshot recorded for the newest history moment
|
|
@@ -6228,7 +7342,7 @@ function rerecordNewestMoment(vars) {
|
|
|
6228
7342
|
function forEachRecorded(count, fn) {
|
|
6229
7343
|
let vars = variableBase;
|
|
6230
7344
|
for (let i = 0; i < count; i++) {
|
|
6231
|
-
if (i > 0) vars =
|
|
7345
|
+
if (i > 0) vars = applyVarPatches(vars, patchEntries[i - 1].forward);
|
|
6232
7346
|
fn(vars, i);
|
|
6233
7347
|
}
|
|
6234
7348
|
}
|
|
@@ -6244,30 +7358,65 @@ function savedMoment(moment, variables) {
|
|
|
6244
7358
|
/** Reconstruct variables at a given history moment by replaying patches. */
|
|
6245
7359
|
function reconstructVarsAt(index) {
|
|
6246
7360
|
let vars = variableBase;
|
|
6247
|
-
for (let i = 0; i < index; i++) vars =
|
|
7361
|
+
for (let i = 0; i < index; i++) vars = applyVarPatches(vars, patchEntries[i].forward);
|
|
6248
7362
|
return vars;
|
|
6249
7363
|
}
|
|
6250
|
-
|
|
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
|
+
*/
|
|
6251
7375
|
function persistSession(get) {
|
|
6252
7376
|
const { storyData, currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
6253
7377
|
if (!storyData) return;
|
|
6254
|
-
if (
|
|
6255
|
-
if (
|
|
7378
|
+
if (sessionMoments.length > history.length) sessionMoments.length = history.length;
|
|
7379
|
+
if (sessionMoments.length < history.length) if (history.length - sessionMoments.length === 1) {
|
|
6256
7380
|
const i = history.length - 1;
|
|
6257
|
-
|
|
7381
|
+
sessionMoments[i] = savedMoment(history[i], variables);
|
|
6258
7382
|
} else forEachRecorded(history.length, (vars, i) => {
|
|
6259
|
-
if (i >=
|
|
7383
|
+
if (i >= sessionMoments.length) sessionMoments[i] = savedMoment(history[i], vars);
|
|
6260
7384
|
});
|
|
6261
7385
|
saveSession(storyData.ifid, {
|
|
6262
7386
|
passage: currentPassage,
|
|
6263
|
-
variables
|
|
6264
|
-
history:
|
|
7387
|
+
variables,
|
|
7388
|
+
history: sessionMoments,
|
|
6265
7389
|
historyIndex,
|
|
6266
7390
|
visitCounts,
|
|
6267
7391
|
renderCounts,
|
|
6268
7392
|
prng: snapshotPRNG()
|
|
6269
7393
|
});
|
|
6270
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
|
+
}
|
|
6271
7420
|
/**
|
|
6272
7421
|
* Trim history to `state.maxHistory` moments: keep the newest moments that
|
|
6273
7422
|
* include the current one. After a navigation (the current moment is the
|
|
@@ -6281,10 +7430,10 @@ function trimHistory(state) {
|
|
|
6281
7430
|
if (excess <= 0) return false;
|
|
6282
7431
|
const start = Math.min(state.historyIndex, excess);
|
|
6283
7432
|
const end = start + state.maxHistory;
|
|
6284
|
-
for (let i = 0; i < start; i++) variableBase =
|
|
7433
|
+
for (let i = 0; i < start; i++) variableBase = applyVarPatches(variableBase, patchEntries[i].forward);
|
|
6285
7434
|
state.history = state.history.slice(start, end);
|
|
6286
7435
|
patchEntries = patchEntries.slice(start, end - 1);
|
|
6287
|
-
|
|
7436
|
+
sessionMoments = sessionMoments.slice(start, end);
|
|
6288
7437
|
state.historyIndex -= start;
|
|
6289
7438
|
return true;
|
|
6290
7439
|
}
|
|
@@ -6317,16 +7466,19 @@ function finishEnteredMoment(get, set) {
|
|
|
6317
7466
|
});
|
|
6318
7467
|
}
|
|
6319
7468
|
/**
|
|
6320
|
-
* A
|
|
6321
|
-
*
|
|
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).
|
|
6322
7474
|
*/
|
|
6323
|
-
var plainCopy = (ns) => ({ ...
|
|
7475
|
+
var plainCopy = (ns) => ({ ...ns });
|
|
6324
7476
|
/** Reset all module-level state (called on init, restart, loadFromPayload). */
|
|
6325
7477
|
function resetModuleState(base) {
|
|
6326
7478
|
variableBase = base;
|
|
6327
7479
|
patchEntries = [];
|
|
6328
7480
|
lastNavigationVars = base;
|
|
6329
|
-
|
|
7481
|
+
sessionMoments = [];
|
|
6330
7482
|
}
|
|
6331
7483
|
/**
|
|
6332
7484
|
* Fresh variables from `defaults` for a game that starts over (init,
|
|
@@ -6377,8 +7529,7 @@ function moveInHistory(get, set, step) {
|
|
|
6377
7529
|
reinitTriggerState();
|
|
6378
7530
|
lastNavigationVars = get().variables;
|
|
6379
7531
|
restorePRNGFromMoment(get().history[get().historyIndex]);
|
|
6380
|
-
|
|
6381
|
-
emit("afternavigate", targetPassage, previousPassage);
|
|
7532
|
+
persistSessionThen(get, () => emit("afternavigate", targetPassage, previousPassage));
|
|
6382
7533
|
}
|
|
6383
7534
|
/**
|
|
6384
7535
|
* Record the state StoryInit left behind as the start moment: its variable
|
|
@@ -6551,7 +7702,8 @@ function loadedEntryMoment(payload) {
|
|
|
6551
7702
|
* the `beforesave` hooks) into the payload's snapshot of the saved moment. A
|
|
6552
7703
|
* load restores that snapshot, the state on entering the passage, and runs
|
|
6553
7704
|
* the passage again, so data a hook adds to a save would otherwise be lost
|
|
6554
|
-
* 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
|
|
6555
7707
|
* whole variable would bring along what the passage did to the rest of it,
|
|
6556
7708
|
* which the passage then does again on load (#232). Only the payload's copy
|
|
6557
7709
|
* changes: the live history keeps the recorded snapshot (#159).
|
|
@@ -6573,7 +7725,9 @@ function mergeHookWrites(target, before, after, ancestors) {
|
|
|
6573
7725
|
ancestors.add(after);
|
|
6574
7726
|
mergeKeys(target, before, after, (key, b, a) => {
|
|
6575
7727
|
if (!hasOwn(target, key) || !mergesWith(a, target[key], true) || ancestors.has(a)) return false;
|
|
6576
|
-
|
|
7728
|
+
const copy = shallowCopy(target[key]);
|
|
7729
|
+
target[key] = copy;
|
|
7730
|
+
mergeHookWrites(copy, b, a, ancestors);
|
|
6577
7731
|
return true;
|
|
6578
7732
|
});
|
|
6579
7733
|
ancestors.delete(after);
|
|
@@ -6738,7 +7892,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
6738
7892
|
state.maxHistory = Math.max(1, Math.round(limit));
|
|
6739
7893
|
trimmed = trimHistory(state);
|
|
6740
7894
|
});
|
|
6741
|
-
if (trimmed)
|
|
7895
|
+
if (trimmed) persistSessionThen(get);
|
|
6742
7896
|
},
|
|
6743
7897
|
setQuickSaveKey: (key) => {
|
|
6744
7898
|
set((state) => {
|
|
@@ -6799,7 +7953,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
6799
7953
|
state.navigationId++;
|
|
6800
7954
|
state.history = state.history.slice(0, state.historyIndex + 1);
|
|
6801
7955
|
patchEntries.length = state.historyIndex;
|
|
6802
|
-
if (
|
|
7956
|
+
if (sessionMoments.length > state.historyIndex + 1) sessionMoments.length = state.historyIndex + 1;
|
|
6803
7957
|
patchEntries.push(patchEntry);
|
|
6804
7958
|
state.history.push({
|
|
6805
7959
|
passage: passageName,
|
|
@@ -6824,9 +7978,10 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
6824
7978
|
const deferred = deferredNavigations;
|
|
6825
7979
|
deferredNavigations = [];
|
|
6826
7980
|
finishEnteredMoment(get, set);
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
7981
|
+
persistSessionThen(get, () => {
|
|
7982
|
+
emit("afternavigate", passageName, previousPassage);
|
|
7983
|
+
for (const next of deferred) get().navigate(next);
|
|
7984
|
+
});
|
|
6830
7985
|
},
|
|
6831
7986
|
goBack: () => moveInHistory(get, set, -1),
|
|
6832
7987
|
goForward: () => moveInHistory(get, set, 1),
|
|
@@ -6967,7 +8122,6 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
6967
8122
|
importSave: async (data, slot) => {
|
|
6968
8123
|
const { storyData } = get();
|
|
6969
8124
|
if (!storyData) throw new Error("spindle: Story is not initialized.");
|
|
6970
|
-
if (!isSaveExport(data)) throw new Error("Invalid save file format");
|
|
6971
8125
|
const info = await importSlotSave(data, storyData.ifid, slot);
|
|
6972
8126
|
recordKnownSave(set, slot, true);
|
|
6973
8127
|
return info;
|
|
@@ -7002,11 +8156,12 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
7002
8156
|
const { currentPassage, variables, history, historyIndex, visitCounts, renderCounts } = get();
|
|
7003
8157
|
const saveHistory = [];
|
|
7004
8158
|
forEachRecorded(history.length, (vars, i) => {
|
|
7005
|
-
|
|
8159
|
+
const recorded = plainCopy(vars);
|
|
8160
|
+
saveHistory.push(savedMoment(history[i], i === 0 ? recorded : shareEqual(saveHistory[i - 1].variables, recorded)));
|
|
7006
8161
|
});
|
|
7007
8162
|
return {
|
|
7008
8163
|
passage: currentPassage,
|
|
7009
|
-
variables: plainCopy(variables),
|
|
8164
|
+
variables: shareEqual(saveHistory[historyIndex]?.variables, plainCopy(variables)),
|
|
7010
8165
|
history: saveHistory,
|
|
7011
8166
|
historyIndex,
|
|
7012
8167
|
visitCounts: { ...visitCounts },
|
|
@@ -7034,17 +8189,10 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
7034
8189
|
const ifid = get().storyData?.ifid;
|
|
7035
8190
|
if (playthroughId && ifid) switchToLoadedPlaythrough(ifid, playthroughId);
|
|
7036
8191
|
const entry = loadedEntryMoment(payload);
|
|
7037
|
-
const
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
const currVars = createNamespace(deepClone(payload.history[i].variables));
|
|
7042
|
-
newPatchEntries.push(computeVarPatches(prevVars, currVars));
|
|
7043
|
-
prevVars = currVars;
|
|
7044
|
-
}
|
|
7045
|
-
variableBase = deepClone(base);
|
|
7046
|
-
patchEntries = newPatchEntries;
|
|
7047
|
-
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]));
|
|
7048
8196
|
set((state) => {
|
|
7049
8197
|
state.currentPassage = payload.passage;
|
|
7050
8198
|
state.navigationId++;
|
|
@@ -7064,8 +8212,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
7064
8212
|
reinitTriggerState();
|
|
7065
8213
|
lastNavigationVars = reconstructVarsAt(get().historyIndex);
|
|
7066
8214
|
restorePRNGFrom(entry?.prng !== void 0 ? entry.prng : payload.prng);
|
|
7067
|
-
|
|
7068
|
-
withoutDraws(() => emit("afterload", slot));
|
|
8215
|
+
persistSessionThen(get, () => withoutDraws(() => emit("afterload", slot)));
|
|
7069
8216
|
},
|
|
7070
8217
|
getHistoryVariables: (index) => {
|
|
7071
8218
|
return deepClone(reconstructVarsAt(index));
|
|
@@ -7306,6 +8453,50 @@ function TriggerDialogHost() {
|
|
|
7306
8453
|
}, `${dialog.passageName}-${i}`)) });
|
|
7307
8454
|
}
|
|
7308
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
|
|
7309
8500
|
//#region src/components/App.tsx
|
|
7310
8501
|
function App() {
|
|
7311
8502
|
const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
|
|
@@ -7327,7 +8518,11 @@ function App() {
|
|
|
7327
8518
|
class: "loading",
|
|
7328
8519
|
children: "Loading..."
|
|
7329
8520
|
});
|
|
7330
|
-
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
|
+
] });
|
|
7331
8526
|
return nobr ? /* @__PURE__ */ jsx(NobrContext.Provider, {
|
|
7332
8527
|
value: true,
|
|
7333
8528
|
children: content
|
|
@@ -7335,19 +8530,92 @@ function App() {
|
|
|
7335
8530
|
}
|
|
7336
8531
|
//#endregion
|
|
7337
8532
|
//#region src/parser.ts
|
|
8533
|
+
var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
8534
|
+
/** HTML elements written without content or end tag, as innerHTML does. */
|
|
8535
|
+
var VOID_ELEMENTS = new Set([
|
|
8536
|
+
"area",
|
|
8537
|
+
"base",
|
|
8538
|
+
"basefont",
|
|
8539
|
+
"bgsound",
|
|
8540
|
+
"br",
|
|
8541
|
+
"col",
|
|
8542
|
+
"embed",
|
|
8543
|
+
"frame",
|
|
8544
|
+
"hr",
|
|
8545
|
+
"img",
|
|
8546
|
+
"input",
|
|
8547
|
+
"keygen",
|
|
8548
|
+
"link",
|
|
8549
|
+
"meta",
|
|
8550
|
+
"param",
|
|
8551
|
+
"source",
|
|
8552
|
+
"track",
|
|
8553
|
+
"wbr"
|
|
8554
|
+
]);
|
|
8555
|
+
/**
|
|
8556
|
+
* The passage markup in the nodes of passage data.
|
|
8557
|
+
*
|
|
8558
|
+
* A compiler that HTML-escapes passage text (Twine, Tweego) leaves a text
|
|
8559
|
+
* node whose text is the markup; a no-break space in it is U+00A0, which
|
|
8560
|
+
* innerHTML writes as ` `. One that doesn't escape it leaves the
|
|
8561
|
+
* browser's parse of it, whose elements are written back as markup here.
|
|
8562
|
+
* innerHTML did that too, but its escaping can't be undone: a `"` in an
|
|
8563
|
+
* attribute value is written `"`, and decoding that ends the value
|
|
8564
|
+
* early, while text it leaves unescaped (in `<style>`, comments) would be
|
|
8565
|
+
* decoded wrongly.
|
|
8566
|
+
*/
|
|
8567
|
+
function passageMarkup(parent) {
|
|
8568
|
+
let markup = "";
|
|
8569
|
+
for (const node of Array.from(parent.childNodes)) if (node.nodeType === Node.TEXT_NODE) markup += node.data;
|
|
8570
|
+
else if (node.nodeType === Node.COMMENT_NODE) markup += `<!--${node.data}-->`;
|
|
8571
|
+
else if (node.nodeType === Node.ELEMENT_NODE) markup += elementMarkup(node);
|
|
8572
|
+
return markup;
|
|
8573
|
+
}
|
|
8574
|
+
/** An element of passage data as markup (see passageMarkup). */
|
|
8575
|
+
function elementMarkup(el) {
|
|
8576
|
+
const name = el.localName;
|
|
8577
|
+
let markup = `<${name}`;
|
|
8578
|
+
for (const attr of Array.from(el.attributes)) markup += ` ${attr.name}=${quoteAttribute(attr.name, attr.value)}`;
|
|
8579
|
+
markup += ">";
|
|
8580
|
+
if (el.namespaceURI === HTML_NAMESPACE && VOID_ELEMENTS.has(name)) return markup;
|
|
8581
|
+
const content = name === "template" ? el.content : el;
|
|
8582
|
+
return `${markup}${passageMarkup(content)}</${name}>`;
|
|
8583
|
+
}
|
|
8584
|
+
/**
|
|
8585
|
+
* An attribute value quoted so that the passage tokenizer reads it back:
|
|
8586
|
+
* in double quotes, else single ones, else with the double quotes in its
|
|
8587
|
+
* text written as `"`, which rendering decodes. Quotes inside markup in
|
|
8588
|
+
* the value (`{print "a"}`) are code, and don't end it.
|
|
8589
|
+
*/
|
|
8590
|
+
function quoteAttribute(name, value) {
|
|
8591
|
+
const code = isCodeAttribute(name);
|
|
8592
|
+
const probe = code ? "on" : "a";
|
|
8593
|
+
const readsBack = (quoted) => {
|
|
8594
|
+
const tokens = tokenize(`<i ${probe}=${quoted}>`);
|
|
8595
|
+
const tag = tokens[0];
|
|
8596
|
+
return tokens.length === 1 && tag?.type === "html" && tag.attributes[probe] === quoted.slice(1, -1);
|
|
8597
|
+
};
|
|
8598
|
+
return [`"${value}"`, `'${value}'`].find(readsBack) ?? `"${escapeTextQuotes(value, code)}"`;
|
|
8599
|
+
}
|
|
7338
8600
|
/**
|
|
7339
|
-
*
|
|
7340
|
-
*
|
|
8601
|
+
* `value` with the double quotes in its text written as `"`, but not
|
|
8602
|
+
* those in markup (`{print "a"}`) or, in a code attribute, in sigil
|
|
8603
|
+
* references (see splitSigilTemplate), which are code.
|
|
7341
8604
|
*/
|
|
7342
|
-
function
|
|
7343
|
-
|
|
8605
|
+
function escapeTextQuotes(value, code) {
|
|
8606
|
+
const escape = (text) => text.replace(/"/g, """);
|
|
8607
|
+
if (code) return splitSigilTemplate(value).map((part) => "text" in part ? escape(part.text) : "expr" in part ? `{${part.expr}}` : part.verbatim).join("");
|
|
8608
|
+
return tokenize(value, { text: true }).map((token) => {
|
|
8609
|
+
const source = value.slice(token.start, token.end);
|
|
8610
|
+
return token.type === "text" ? escape(source) : source;
|
|
8611
|
+
}).join("");
|
|
7344
8612
|
}
|
|
7345
8613
|
/**
|
|
7346
8614
|
* Parse <tw-storydata> and all <tw-passagedata> elements from the DOM.
|
|
7347
8615
|
*
|
|
7348
|
-
*
|
|
7349
|
-
* tags inside passage markup (e.g. <div> inside {button}) are
|
|
7350
|
-
*
|
|
8616
|
+
* Passage content is read with passageMarkup instead of textContent, so
|
|
8617
|
+
* that HTML tags inside passage markup (e.g. <div> inside {button}) are
|
|
8618
|
+
* preserved whether or not the compiler HTML-encoded them.
|
|
7351
8619
|
*/
|
|
7352
8620
|
function parseStoryData() {
|
|
7353
8621
|
const storyEl = document.querySelector("tw-storydata");
|
|
@@ -7365,7 +8633,7 @@ function parseStoryData() {
|
|
|
7365
8633
|
const pid = parseInt(el.getAttribute("pid") || "0", 10);
|
|
7366
8634
|
const passageName = el.getAttribute("name") || "";
|
|
7367
8635
|
const tags = (el.getAttribute("tags") || "").split(/\s+/).filter((t) => t.length > 0);
|
|
7368
|
-
const content =
|
|
8636
|
+
const content = passageMarkup(el);
|
|
7369
8637
|
const metadata = {};
|
|
7370
8638
|
const skipAttrs = new Set([
|
|
7371
8639
|
"pid",
|
|
@@ -8675,12 +9943,13 @@ function assignmentArgs(rawArgs, args) {
|
|
|
8675
9943
|
* and later recomputations read the same source.
|
|
8676
9944
|
*/
|
|
8677
9945
|
function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRef, localsUpdate) {
|
|
9946
|
+
const location = currentSourceLocation();
|
|
8678
9947
|
let newValue;
|
|
8679
9948
|
try {
|
|
8680
9949
|
const { variables, temporary, transient } = readState();
|
|
8681
9950
|
newValue = evaluate(expr, variables, temporary, getLocals(), transient);
|
|
8682
9951
|
} catch (err) {
|
|
8683
|
-
logMacroError(`computed ${rawArgs}`, err);
|
|
9952
|
+
logMacroError(`computed ${rawArgs}`, err, location);
|
|
8684
9953
|
return;
|
|
8685
9954
|
}
|
|
8686
9955
|
if (!deepEqual(prevRef.current, newValue)) {
|
|
@@ -8688,7 +9957,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
|
|
|
8688
9957
|
if (isLocal) try {
|
|
8689
9958
|
localsUpdate(name, newValue);
|
|
8690
9959
|
} catch (err) {
|
|
8691
|
-
logMacroError(`computed ${rawArgs}`, err);
|
|
9960
|
+
logMacroError(`computed ${rawArgs}`, err, location);
|
|
8692
9961
|
}
|
|
8693
9962
|
else {
|
|
8694
9963
|
const state = useStoryStore.getState();
|
|
@@ -9012,10 +10281,11 @@ defineMacro({
|
|
|
9012
10281
|
render({ children = [] }, ctx) {
|
|
9013
10282
|
const code = ctx.collectText(children);
|
|
9014
10283
|
ctx.hooks.useLayoutEffect(() => {
|
|
10284
|
+
const location = ctx.sourceLocation();
|
|
9015
10285
|
try {
|
|
9016
10286
|
ctx.mutate(code);
|
|
9017
10287
|
} catch (err) {
|
|
9018
|
-
logMacroError("do", err);
|
|
10288
|
+
logMacroError("do", err, location);
|
|
9019
10289
|
}
|
|
9020
10290
|
}, []);
|
|
9021
10291
|
return null;
|
|
@@ -9623,7 +10893,7 @@ function SaveManagerContent() {
|
|
|
9623
10893
|
};
|
|
9624
10894
|
const handleLoad = async (save) => {
|
|
9625
10895
|
try {
|
|
9626
|
-
loadFromPayload(
|
|
10896
|
+
loadFromPayload(decodeSavePayload(save.payload), void 0, save.meta.playthroughId);
|
|
9627
10897
|
showStatus("Game loaded");
|
|
9628
10898
|
if (closeDialog) closeTimer.current = window.setTimeout(closeDialog, 500);
|
|
9629
10899
|
await refresh();
|
|
@@ -9963,7 +11233,12 @@ defineMacro({
|
|
|
9963
11233
|
}],
|
|
9964
11234
|
render(_props, ctx) {
|
|
9965
11235
|
ctx.hooks.useLayoutEffect(() => {
|
|
9966
|
-
|
|
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
|
+
}
|
|
9967
11242
|
}, []);
|
|
9968
11243
|
return null;
|
|
9969
11244
|
}
|
|
@@ -10577,7 +11852,7 @@ defineMacro({
|
|
|
10577
11852
|
});
|
|
10578
11853
|
//#endregion
|
|
10579
11854
|
//#region src/styles.css?inline
|
|
10580
|
-
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";
|
|
10581
11856
|
//#endregion
|
|
10582
11857
|
//#region src/index.tsx
|
|
10583
11858
|
function renderErrors(root, errors) {
|