@rohal12/spindle 0.52.7 → 0.52.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +986 -854
- package/dist/pkg/macro-registry.json +18 -0
- package/package.json +1 -1
- package/src/action-registry.ts +9 -0
- package/src/class-registry.ts +1 -5
- package/src/components/Passage.tsx +35 -39
- package/src/components/PassageDialog.tsx +2 -2
- package/src/components/StoryInterface.tsx +2 -2
- package/src/components/macros/Checkbox.tsx +3 -8
- package/src/components/macros/Computed.tsx +1 -1
- package/src/components/macros/Cycle.tsx +3 -6
- package/src/components/macros/ExprDisplay.tsx +12 -29
- package/src/components/macros/If.tsx +35 -41
- package/src/components/macros/Include.tsx +2 -1
- package/src/components/macros/Listbox.tsx +5 -7
- package/src/components/macros/Print.tsx +3 -5
- package/src/components/macros/Radiobutton.tsx +3 -5
- package/src/components/macros/Repeat.tsx +2 -1
- package/src/components/macros/Switch.tsx +6 -13
- package/src/components/macros/Timed.tsx +14 -18
- package/src/components/macros/Type.tsx +2 -1
- package/src/components/macros/VarDisplay.tsx +12 -32
- package/src/components/macros/Widget.tsx +49 -20
- package/src/components/macros/branches.ts +18 -0
- package/src/components/macros/display.ts +19 -0
- package/src/components/macros/input-macro.ts +33 -11
- package/src/components/macros/macro-args.ts +7 -0
- package/src/components/macros/option-utils.ts +10 -7
- package/src/define-macro.ts +4 -6
- package/src/expression.ts +58 -40
- package/src/hooks/use-action.ts +7 -15
- package/src/index.tsx +5 -15
- package/src/interpolation.ts +34 -41
- package/src/markup/ast.ts +4 -3
- package/src/markup/render.tsx +121 -65
- package/src/parser.ts +119 -13
- package/src/registry.ts +13 -12
- package/src/saves/save-manager.ts +18 -13
- package/src/saves/storage.ts +19 -20
- package/src/story-api.ts +30 -73
- package/src/structural.ts +1 -18
- package/src/types-drift-check.ts +37 -50
- package/src/utils/macro-names.ts +40 -0
- package/src/utils/namespace.ts +17 -0
- package/src/widgets/widget-registry.ts +5 -4
package/dist/pkg/headless.js
CHANGED
|
@@ -52,6 +52,19 @@ var hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
|
52
52
|
/** `ns[key]` if `ns` holds it as an own property, else undefined. */
|
|
53
53
|
var ownValue = (ns, key) => ns && hasOwn(ns, key) ? ns[key] : void 0;
|
|
54
54
|
/**
|
|
55
|
+
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
56
|
+
* property (as JSON.parse does) instead of replacing the prototype.
|
|
57
|
+
*/
|
|
58
|
+
function setOwn(target, key, value) {
|
|
59
|
+
if (key === "__proto__") Object.defineProperty(target, key, {
|
|
60
|
+
value,
|
|
61
|
+
enumerable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
configurable: true
|
|
64
|
+
});
|
|
65
|
+
else target[key] = value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
55
68
|
* A record with no prototype holding the own entries of `sources`, or only
|
|
56
69
|
* those whose value passes `keep`.
|
|
57
70
|
*/
|
|
@@ -102,441 +115,96 @@ function createCounts(source, name) {
|
|
|
102
115
|
return counts;
|
|
103
116
|
}
|
|
104
117
|
//#endregion
|
|
105
|
-
//#region src/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* (class getters, `size` of a Map) are read.
|
|
112
|
-
*/
|
|
113
|
-
function getByPath(obj, segments) {
|
|
114
|
-
let current = obj;
|
|
115
|
-
for (const seg of segments) {
|
|
116
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
117
|
-
if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
|
|
118
|
-
current = current[seg];
|
|
119
|
-
}
|
|
120
|
-
return current;
|
|
121
|
-
}
|
|
122
|
-
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
123
|
-
var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
124
|
-
/**
|
|
125
|
-
* Built-ins whose content is not their properties: a property written into
|
|
126
|
-
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
127
|
-
*/
|
|
128
|
-
var builtinName = (value) => value instanceof Map ? "a Map" : value instanceof Set ? "a Set" : value instanceof Date ? "a Date" : value instanceof RegExp ? "a RegExp" : void 0;
|
|
129
|
-
/**
|
|
130
|
-
* Shallow copy that keeps the prototype, so a registered class instance
|
|
131
|
-
* stays an instance of its class (with the same own keys deepClone copies).
|
|
132
|
-
*/
|
|
133
|
-
function shallowCopy(value) {
|
|
134
|
-
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
135
|
-
for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
|
|
136
|
-
value: value[key],
|
|
137
|
-
enumerable: true,
|
|
138
|
-
writable: true,
|
|
139
|
-
configurable: true
|
|
140
|
-
});
|
|
141
|
-
return copy;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Set a value at dot-path `segments` below `root`.
|
|
145
|
-
*
|
|
146
|
-
* When `root` is an Immer draft, objects Immer does not draft (registered
|
|
147
|
-
* class instances and everything below them) are copied on the way down and
|
|
148
|
-
* the copy is assigned to its parent, so the write produces new references
|
|
149
|
-
* up to the root. Writing into such an object directly would mutate the
|
|
150
|
-
* instance shared with the previous state and with history, and subscribers
|
|
151
|
-
* would see no change. Outside a draft (a private working copy) the path is
|
|
152
|
-
* written in place.
|
|
153
|
-
*
|
|
154
|
-
* The path goes through own properties of plain objects, class instances
|
|
155
|
-
* and arrays (by index); anything else throws a TypeError rather than
|
|
156
|
-
* writing where no clone or save would see it: an inherited property such
|
|
157
|
-
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
158
|
-
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
159
|
-
*/
|
|
160
|
-
function setByPath(root, segments, value, options = {}) {
|
|
161
|
-
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
162
|
-
parent[segments[segments.length - 1]] = value;
|
|
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);
|
|
163
124
|
}
|
|
164
125
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* only inherited).
|
|
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.
|
|
169
129
|
*/
|
|
170
|
-
function
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
let holder = root;
|
|
174
|
-
for (const seg of segments.slice(0, -1)) {
|
|
175
|
-
if (holder == null || typeof holder !== "object" || !hasOwn(holder, seg)) return;
|
|
176
|
-
holder = holder[seg];
|
|
177
|
-
}
|
|
178
|
-
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
179
|
-
const parent = walkToParent(root, segments, false);
|
|
180
|
-
delete parent[last];
|
|
181
|
-
}
|
|
182
|
-
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
183
|
-
function checkSegments(segments) {
|
|
184
|
-
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
130
|
+
function registeredClassName(value) {
|
|
131
|
+
const proto = Object.getPrototypeOf(value);
|
|
132
|
+
return proto === null ? void 0 : ctorToName.get(proto.constructor);
|
|
185
133
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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";
|
|
190
153
|
}
|
|
191
154
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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.
|
|
196
162
|
*/
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
205
|
-
if (next == null || typeof next !== "object") {
|
|
206
|
-
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
207
|
-
next = {};
|
|
208
|
-
current[seg] = next;
|
|
209
|
-
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
210
|
-
next = shallowCopy(next);
|
|
211
|
-
current[seg] = next;
|
|
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 });
|
|
212
170
|
}
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
216
|
-
return current;
|
|
217
|
-
}
|
|
218
|
-
//#endregion
|
|
219
|
-
//#region src/structural.ts
|
|
220
|
-
/**
|
|
221
|
-
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
222
|
-
* property (as JSON.parse does) instead of replacing the prototype.
|
|
223
|
-
*/
|
|
224
|
-
function setOwn(target, key, value) {
|
|
225
|
-
if (key === "__proto__") Object.defineProperty(target, key, {
|
|
226
|
-
value,
|
|
227
|
-
enumerable: true,
|
|
228
|
-
writable: true,
|
|
229
|
-
configurable: true
|
|
230
|
-
});
|
|
231
|
-
else target[key] = value;
|
|
232
|
-
}
|
|
233
|
-
function isPlainObject(value) {
|
|
234
|
-
if (typeof value !== "object" || value === null) return false;
|
|
235
|
-
const proto = Object.getPrototypeOf(value);
|
|
236
|
-
return proto === Object.prototype || proto === null;
|
|
237
|
-
}
|
|
238
|
-
function deepClone(value, options = {}) {
|
|
239
|
-
const seen = /* @__PURE__ */ new Map();
|
|
240
|
-
function clone(val) {
|
|
171
|
+
if (typeof val === "bigint") return tagged("__BigInt__", { value: val.toString() });
|
|
241
172
|
if (val === null || typeof val !== "object") return val;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
seen.set(obj, arr);
|
|
249
|
-
for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
|
|
250
|
-
return arr;
|
|
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);
|
|
251
179
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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]);
|
|
257
186
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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;
|
|
263
199
|
}
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return
|
|
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;
|
|
270
206
|
}
|
|
271
|
-
return
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Structural equality over the value types deepClone() supports: primitives,
|
|
275
|
-
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
276
|
-
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
277
|
-
* are compared by length and index, so a hole equals an undefined element
|
|
278
|
-
* (deepClone() and save/load turn holes into undefined elements).
|
|
279
|
-
*/
|
|
280
|
-
function deepEqual(a, b) {
|
|
281
|
-
return equal(a, b, /* @__PURE__ */ new Map());
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
285
|
-
* cycle) assumes it equal. Every object may pair with several others, since
|
|
286
|
-
* cycles of different lengths can still unfold to the same value.
|
|
287
|
-
*/
|
|
288
|
-
function equal(a, b, assumed) {
|
|
289
|
-
if (Object.is(a, b)) return true;
|
|
290
|
-
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
291
|
-
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
292
|
-
const pairs = assumed.get(a);
|
|
293
|
-
if (pairs?.has(b)) return true;
|
|
294
|
-
if (pairs) pairs.add(b);
|
|
295
|
-
else assumed.set(a, new Set([b]));
|
|
296
|
-
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
297
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
298
|
-
if (a instanceof Map || a instanceof Set) {
|
|
299
|
-
const bc = b;
|
|
300
|
-
if (a.size !== bc.size) return false;
|
|
301
|
-
const ai = a.entries();
|
|
302
|
-
const bi = bc.entries();
|
|
303
|
-
for (let x = ai.next(), y = bi.next(); !x.done; x = ai.next(), y = bi.next()) if (!equal(x.value, y.value, assumed)) return false;
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
if (Array.isArray(a)) {
|
|
307
|
-
const bc = b;
|
|
308
|
-
if (a.length !== bc.length) return false;
|
|
309
|
-
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
const ao = a;
|
|
313
|
-
const bo = b;
|
|
314
|
-
const keys = Object.keys(ao);
|
|
315
|
-
if (keys.length !== Object.keys(bo).length) return false;
|
|
316
|
-
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Objects merged property by property: plain objects and (registered)
|
|
321
|
-
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
322
|
-
* replaced as a whole, since their elements have no stable identity to
|
|
323
|
-
* merge by (a shift moves every index).
|
|
324
|
-
*/
|
|
325
|
-
function isMergeable(value) {
|
|
326
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
330
|
-
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
331
|
-
*/
|
|
332
|
-
function mergesWith(a, b, arrays) {
|
|
333
|
-
if (arrays && (Array.isArray(a) || Array.isArray(b))) return Array.isArray(a) && Array.isArray(b);
|
|
334
|
-
return isMergeable(a) && isMergeable(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* The keys at which `after` differs from `before`, two values that merge
|
|
338
|
-
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
339
|
-
* updates are immutable, so a value that kept its identity is no change,
|
|
340
|
-
* and one rebuilt with equal content (mutation code commits whole values)
|
|
341
|
-
* is none either. Values that merge are reported as 'nested', to be
|
|
342
|
-
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
343
|
-
* merge too: elements at the indices both hold are compared, and the
|
|
344
|
-
* elements one has beyond the other's length are added or deleted.
|
|
345
|
-
*/
|
|
346
|
-
function keyChanges(before, after, arrays) {
|
|
347
|
-
const changes = [];
|
|
348
|
-
const compare = (key, b, a) => {
|
|
349
|
-
if (Object.is(b, a)) return;
|
|
350
|
-
if (mergesWith(b, a, arrays)) changes.push({
|
|
351
|
-
key,
|
|
352
|
-
kind: "nested",
|
|
353
|
-
before: b,
|
|
354
|
-
after: a
|
|
355
|
-
});
|
|
356
|
-
else if (!deepEqual(b, a)) changes.push({
|
|
357
|
-
key,
|
|
358
|
-
kind: "changed",
|
|
359
|
-
after: a
|
|
360
|
-
});
|
|
361
|
-
};
|
|
362
|
-
if (Array.isArray(after)) {
|
|
363
|
-
const b = before;
|
|
364
|
-
const a = after;
|
|
365
|
-
for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
|
|
366
|
-
for (let i = b.length; i < a.length; i++) changes.push({
|
|
367
|
-
key: String(i),
|
|
368
|
-
kind: "added",
|
|
369
|
-
after: a[i]
|
|
370
|
-
});
|
|
371
|
-
for (let i = a.length; i < b.length; i++) changes.push({
|
|
372
|
-
key: String(i),
|
|
373
|
-
kind: "deleted"
|
|
374
|
-
});
|
|
375
|
-
return changes;
|
|
376
|
-
}
|
|
377
|
-
for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
378
|
-
else changes.push({
|
|
379
|
-
key,
|
|
380
|
-
kind: "added",
|
|
381
|
-
after: after[key]
|
|
382
|
-
});
|
|
383
|
-
for (const key of Object.keys(before)) if (!hasOwn(after, key)) changes.push({
|
|
384
|
-
key,
|
|
385
|
-
kind: "deleted"
|
|
386
|
-
});
|
|
387
|
-
return changes;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* The property paths where `after` differs from `before`, two objects of
|
|
391
|
-
* one class: objects that merge (see isMergeable) are compared property by
|
|
392
|
-
* property, any other value as a whole.
|
|
393
|
-
*/
|
|
394
|
-
function diffPaths(before, after) {
|
|
395
|
-
const changes = [];
|
|
396
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
397
|
-
(function walk(b, a, path) {
|
|
398
|
-
if (ancestors.has(a)) return;
|
|
399
|
-
ancestors.add(a);
|
|
400
|
-
for (const change of keyChanges(b, a, false)) {
|
|
401
|
-
const at = [...path, change.key];
|
|
402
|
-
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
403
|
-
else changes.push(change.kind === "deleted" ? {
|
|
404
|
-
path: at,
|
|
405
|
-
deleted: true
|
|
406
|
-
} : {
|
|
407
|
-
path: at,
|
|
408
|
-
deleted: false,
|
|
409
|
-
value: change.after
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
ancestors.delete(a);
|
|
413
|
-
})(before, after, []);
|
|
414
|
-
return changes;
|
|
415
|
-
}
|
|
416
|
-
/** Whether `target` already holds what `change` would write. */
|
|
417
|
-
function isApplied(target, change) {
|
|
418
|
-
const parent = getByPath(target, change.path.slice(0, -1));
|
|
419
|
-
if (parent === null || typeof parent !== "object") return change.deleted;
|
|
420
|
-
const key = change.path[change.path.length - 1];
|
|
421
|
-
if (!hasOwn(parent, key)) return change.deleted;
|
|
422
|
-
return !change.deleted && deepEqual(parent[key], change.value);
|
|
423
|
-
}
|
|
424
|
-
function applyChange(target, change) {
|
|
425
|
-
if (change.deleted) deleteByPath(target, change.path);
|
|
426
|
-
else setByPath(target, change.path, change.value);
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
430
|
-
* index) into `target`, another version of the same object or array.
|
|
431
|
-
* Values that changed are written whole, as deep copies; for values that
|
|
432
|
-
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
433
|
-
* whether it did. Into an array, elements removed from the end are removed
|
|
434
|
-
* at the same indices and elements added are appended, and changes at
|
|
435
|
-
* indices `target` lacks are dropped: where `target` was resized, its
|
|
436
|
-
* indices do not line up with `before`'s.
|
|
437
|
-
*/
|
|
438
|
-
function mergeKeys(target, before, after, mergeNested) {
|
|
439
|
-
const list = Array.isArray(target) ? target : void 0;
|
|
440
|
-
for (const change of keyChanges(before, after, true)) {
|
|
441
|
-
const { key } = change;
|
|
442
|
-
if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
|
|
443
|
-
else delete target[key];
|
|
444
|
-
else if (list && change.kind === "added") list.push(deepClone(change.after));
|
|
445
|
-
else if (list && Number(key) >= list.length) continue;
|
|
446
|
-
else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region src/class-registry.ts
|
|
451
|
-
var registry$1 = /* @__PURE__ */ new Map();
|
|
452
|
-
var ctorToName = /* @__PURE__ */ new Map();
|
|
453
|
-
function registerClass(name, ctor) {
|
|
454
|
-
registry$1.set(name, ctor);
|
|
455
|
-
ctorToName.set(ctor, name);
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* The registered name of the class `value` is an instance of, or undefined.
|
|
459
|
-
* Looks the constructor up on the prototype, so an own property named
|
|
460
|
-
* "constructor" (plain data) cannot hide or fake the class.
|
|
461
|
-
*/
|
|
462
|
-
function registeredClassName(value) {
|
|
463
|
-
const proto = Object.getPrototypeOf(value);
|
|
464
|
-
return proto === null ? void 0 : ctorToName.get(proto.constructor);
|
|
465
|
-
}
|
|
466
|
-
var CLASS_TAG = "__spindle_class__";
|
|
467
|
-
var DATA_TAG = "__spindle_data__";
|
|
468
|
-
/** A tagged value, as serialize() writes and deserialize() reads it. */
|
|
469
|
-
var tagged = (name, data) => ({
|
|
470
|
-
[CLASS_TAG]: name,
|
|
471
|
-
[DATA_TAG]: data
|
|
472
|
-
});
|
|
473
|
-
/** Numbers JSON text cannot hold (it writes null for them, and 0 for -0). */
|
|
474
|
-
var SPECIAL_NUMBERS = {
|
|
475
|
-
NaN: NaN,
|
|
476
|
-
Infinity: Infinity,
|
|
477
|
-
"-Infinity": -Infinity,
|
|
478
|
-
"-0": -0
|
|
479
|
-
};
|
|
480
|
-
function specialNumberName(value) {
|
|
481
|
-
if (Number.isNaN(value)) return "NaN";
|
|
482
|
-
if (value === Infinity) return "Infinity";
|
|
483
|
-
if (value === -Infinity) return "-Infinity";
|
|
484
|
-
if (Object.is(value, -0)) return "-0";
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Turn a story value into data that survives JSON text (saves, exports,
|
|
488
|
-
* the session) and that deserialize() restores. Date, RegExp, Map, Set and
|
|
489
|
-
* registered class instances become tagged objects, and so do the values
|
|
490
|
-
* JSON text would drop or change: undefined (also array holes), NaN,
|
|
491
|
-
* ±Infinity, -0, bigint, invalid dates, and plain objects with a key named
|
|
492
|
-
* like the class tag. Throws on circular references and on a property
|
|
493
|
-
* named "__proto__", which story state cannot hold.
|
|
494
|
-
*/
|
|
495
|
-
function serialize(value) {
|
|
496
|
-
const seen = /* @__PURE__ */ new Set();
|
|
497
|
-
function ser(val) {
|
|
498
|
-
if (val === void 0) return tagged("__Undefined__", {});
|
|
499
|
-
if (typeof val === "number") {
|
|
500
|
-
const name = specialNumberName(val);
|
|
501
|
-
return name === void 0 ? val : tagged("__Number__", { value: name });
|
|
502
|
-
}
|
|
503
|
-
if (typeof val === "bigint") return tagged("__BigInt__", { value: val.toString() });
|
|
504
|
-
if (val === null || typeof val !== "object") return val;
|
|
505
|
-
if (seen.has(val)) throw new Error("spindle: Cannot serialize circular references");
|
|
506
|
-
seen.add(val);
|
|
507
|
-
try {
|
|
508
|
-
return serObject(val);
|
|
509
|
-
} finally {
|
|
510
|
-
seen.delete(val);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
function serKeys(obj) {
|
|
514
|
-
const data = {};
|
|
515
|
-
for (const key of Object.keys(obj)) {
|
|
516
|
-
if (key === "__proto__") throw new Error("spindle: Cannot save a property named \"__proto__\"");
|
|
517
|
-
data[key] = ser(obj[key]);
|
|
518
|
-
}
|
|
519
|
-
return data;
|
|
520
|
-
}
|
|
521
|
-
function serObject(val) {
|
|
522
|
-
if (val instanceof Date) return tagged("__Date__", { iso: !Number.isNaN(val.getTime()) ? val.toISOString() : null });
|
|
523
|
-
if (val instanceof RegExp) return tagged("__RegExp__", {
|
|
524
|
-
source: val.source,
|
|
525
|
-
flags: val.flags
|
|
526
|
-
});
|
|
527
|
-
if (Array.isArray(val)) {
|
|
528
|
-
const result = [];
|
|
529
|
-
for (let i = 0; i < val.length; i++) result.push(ser(val[i]));
|
|
530
|
-
return result;
|
|
531
|
-
}
|
|
532
|
-
if (val instanceof Map) return tagged("__Map__", { entries: [...val].map(([k, v]) => [ser(k), ser(v)]) });
|
|
533
|
-
if (val instanceof Set) return tagged("__Set__", { entries: [...val].map((v) => ser(v)) });
|
|
534
|
-
const name = registeredClassName(val);
|
|
535
|
-
if (name !== void 0) return tagged(name, serKeys(val));
|
|
536
|
-
const data = serKeys(val);
|
|
537
|
-
return hasOwn(val, CLASS_TAG) ? tagged("__Object__", data) : data;
|
|
538
|
-
}
|
|
539
|
-
return ser(value);
|
|
207
|
+
return ser(value);
|
|
540
208
|
}
|
|
541
209
|
function deserialize(value) {
|
|
542
210
|
function deserKeys(target, data) {
|
|
@@ -2624,9 +2292,46 @@ function tokenize(input, options = {}) {
|
|
|
2624
2292
|
return tokens;
|
|
2625
2293
|
}
|
|
2626
2294
|
//#endregion
|
|
2295
|
+
//#region src/utils/macro-names.ts
|
|
2296
|
+
/**
|
|
2297
|
+
* Macro and widget names are case-insensitive: `{StatusBar}` invokes the
|
|
2298
|
+
* widget defined as `statusbar`. A NameMap keys its entries by the lowercased
|
|
2299
|
+
* name.
|
|
2300
|
+
*/
|
|
2301
|
+
var NameMap = class extends Map {
|
|
2302
|
+
get(name) {
|
|
2303
|
+
return super.get(name.toLowerCase());
|
|
2304
|
+
}
|
|
2305
|
+
set(name, value) {
|
|
2306
|
+
return super.set(name.toLowerCase(), value);
|
|
2307
|
+
}
|
|
2308
|
+
has(name) {
|
|
2309
|
+
return super.has(name.toLowerCase());
|
|
2310
|
+
}
|
|
2311
|
+
delete(name) {
|
|
2312
|
+
return super.delete(name.toLowerCase());
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
/** A set of macro or widget names (see NameMap). */
|
|
2316
|
+
var NameSet = class {
|
|
2317
|
+
constructor(names = []) {
|
|
2318
|
+
this.names = new NameMap();
|
|
2319
|
+
for (const name of names) this.add(name);
|
|
2320
|
+
}
|
|
2321
|
+
add(name) {
|
|
2322
|
+
this.names.set(name, true);
|
|
2323
|
+
}
|
|
2324
|
+
has(name) {
|
|
2325
|
+
return this.names.has(name);
|
|
2326
|
+
}
|
|
2327
|
+
delete(name) {
|
|
2328
|
+
this.names.delete(name);
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
//#endregion
|
|
2627
2332
|
//#region src/markup/ast.ts
|
|
2628
2333
|
/** Macros that require a closing tag and can contain children */
|
|
2629
|
-
var BLOCK_MACROS = new
|
|
2334
|
+
var BLOCK_MACROS = new NameSet([
|
|
2630
2335
|
"if",
|
|
2631
2336
|
"for",
|
|
2632
2337
|
"do",
|
|
@@ -2644,7 +2349,7 @@ var BLOCK_MACROS = new Set([
|
|
|
2644
2349
|
]);
|
|
2645
2350
|
/** Register a custom macro as a block macro so the AST builder nests children. */
|
|
2646
2351
|
function registerBlockMacro(name) {
|
|
2647
|
-
BLOCK_MACROS.add(name
|
|
2352
|
+
BLOCK_MACROS.add(name);
|
|
2648
2353
|
}
|
|
2649
2354
|
/** Map from branch macro name → required parent macro name */
|
|
2650
2355
|
var BRANCH_PARENT = {
|
|
@@ -3038,15 +2743,15 @@ function countQueries(counts) {
|
|
|
3038
2743
|
all: (...names) => names.every((n) => count(n) > 0)
|
|
3039
2744
|
};
|
|
3040
2745
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
const visits = countQueries(
|
|
3048
|
-
const renders = countQueries(
|
|
3049
|
-
|
|
2746
|
+
/**
|
|
2747
|
+
* The history functions of passage code and the story API: the current and
|
|
2748
|
+
* previous passage, and visited()/rendered() with their has-forms, counting
|
|
2749
|
+
* in the counters `visitCounts()` and `renderCounts()` return.
|
|
2750
|
+
*/
|
|
2751
|
+
function historyQueries(visitCounts, renderCounts) {
|
|
2752
|
+
const visits = countQueries(visitCounts);
|
|
2753
|
+
const renders = countQueries(renderCounts);
|
|
2754
|
+
return {
|
|
3050
2755
|
currentPassage,
|
|
3051
2756
|
previousPassage,
|
|
3052
2757
|
visited: visits.count,
|
|
@@ -3056,7 +2761,17 @@ function buildExpressionFns() {
|
|
|
3056
2761
|
rendered: renders.count,
|
|
3057
2762
|
hasRendered: renders.has,
|
|
3058
2763
|
hasRenderedAny: renders.any,
|
|
3059
|
-
hasRenderedAll: renders.all
|
|
2764
|
+
hasRenderedAll: renders.all
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
var cachedFns = null;
|
|
2768
|
+
var cachedVisitCounts = null;
|
|
2769
|
+
var cachedRenderCounts = null;
|
|
2770
|
+
function buildExpressionFns() {
|
|
2771
|
+
const { visitCounts, renderCounts } = useStoryStore.getState();
|
|
2772
|
+
if (cachedFns && cachedVisitCounts === visitCounts && cachedRenderCounts === renderCounts) return cachedFns;
|
|
2773
|
+
cachedFns = {
|
|
2774
|
+
...historyQueries(() => visitCounts, () => renderCounts),
|
|
3060
2775
|
random,
|
|
3061
2776
|
randomInt
|
|
3062
2777
|
};
|
|
@@ -3077,57 +2792,61 @@ function namespaceArg(ns) {
|
|
|
3077
2792
|
Object.setPrototypeOf(ns, null);
|
|
3078
2793
|
return ns;
|
|
3079
2794
|
}
|
|
2795
|
+
/** Run compiled code in `namespaces`. */
|
|
2796
|
+
function run(fn, ...[variables, temporary, locals = EMPTY_NAMESPACE, transient = EMPTY_NAMESPACE]) {
|
|
2797
|
+
return fn(namespaceArg(variables), namespaceArg(temporary), namespaceArg(locals), buildExpressionFns(), namespaceArg(transient));
|
|
2798
|
+
}
|
|
3080
2799
|
/**
|
|
3081
2800
|
* Evaluate an expression and return its value.
|
|
3082
2801
|
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
3083
2802
|
*/
|
|
3084
|
-
function evaluate(expr,
|
|
2803
|
+
function evaluate(expr, ...namespaces) {
|
|
3085
2804
|
const body = `return (${transform(expr)}\n);`;
|
|
3086
|
-
return getOrCompile(body, body)
|
|
2805
|
+
return run(getOrCompile(body, body), ...namespaces);
|
|
3087
2806
|
}
|
|
3088
2807
|
/**
|
|
3089
2808
|
* Execute statements (no return value).
|
|
3090
2809
|
* e.g. execute("$health = 100; $name = 'Hero'", variables, temporary)
|
|
3091
2810
|
*/
|
|
3092
|
-
function execute(code,
|
|
2811
|
+
function execute(code, ...namespaces) {
|
|
3093
2812
|
const transformed = transform(code, "statements");
|
|
3094
|
-
getOrCompile("exec:" + transformed, transformed)
|
|
2813
|
+
run(getOrCompile("exec:" + transformed, transformed), ...namespaces);
|
|
3095
2814
|
}
|
|
3096
2815
|
//#endregion
|
|
3097
2816
|
//#region src/registry.ts
|
|
3098
|
-
var registry =
|
|
2817
|
+
var registry = new NameMap();
|
|
3099
2818
|
function registerMacro(name, component) {
|
|
3100
|
-
registry.set(name
|
|
2819
|
+
registry.set(name, component);
|
|
3101
2820
|
}
|
|
3102
2821
|
function getMacro(name) {
|
|
3103
|
-
return registry.get(name
|
|
2822
|
+
return registry.get(name);
|
|
3104
2823
|
}
|
|
3105
|
-
var textRegistry =
|
|
2824
|
+
var textRegistry = new NameMap();
|
|
3106
2825
|
/** Register (or, with undefined, remove) the text form of a macro. */
|
|
3107
2826
|
function registerMacroText(name, text) {
|
|
3108
|
-
if (text) textRegistry.set(name
|
|
3109
|
-
else textRegistry.delete(name
|
|
2827
|
+
if (text) textRegistry.set(name, text);
|
|
2828
|
+
else textRegistry.delete(name);
|
|
3110
2829
|
}
|
|
3111
2830
|
function getMacroText(name) {
|
|
3112
|
-
return textRegistry.get(name
|
|
2831
|
+
return textRegistry.get(name);
|
|
3113
2832
|
}
|
|
3114
|
-
var subMacros =
|
|
2833
|
+
var subMacros = new NameSet();
|
|
3115
2834
|
function registerSubMacro(name) {
|
|
3116
|
-
subMacros.add(name
|
|
2835
|
+
subMacros.add(name);
|
|
3117
2836
|
}
|
|
3118
2837
|
function isSubMacro(name) {
|
|
3119
|
-
return subMacros.has(name
|
|
2838
|
+
return subMacros.has(name);
|
|
3120
2839
|
}
|
|
3121
|
-
var metadataRegistry =
|
|
2840
|
+
var metadataRegistry = new NameMap();
|
|
3122
2841
|
function registerMacroMetadata(name, metadata) {
|
|
3123
|
-
metadataRegistry.set(name
|
|
2842
|
+
metadataRegistry.set(name, metadata);
|
|
3124
2843
|
}
|
|
3125
2844
|
function getMacroRegistry() {
|
|
3126
2845
|
return Array.from(metadataRegistry.values());
|
|
3127
2846
|
}
|
|
3128
2847
|
//#endregion
|
|
3129
2848
|
//#region src/widgets/widget-registry.ts
|
|
3130
|
-
var widgets =
|
|
2849
|
+
var widgets = new NameMap();
|
|
3131
2850
|
/**
|
|
3132
2851
|
* Register a widget. Its `@` parameters become locals of its body, so one
|
|
3133
2852
|
* that no namespace can hold (`@__proto__`) throws a TypeError and the
|
|
@@ -3136,14 +2855,14 @@ var widgets = /* @__PURE__ */ new Map();
|
|
|
3136
2855
|
function registerWidget(name, bodyAST, params, isBlock = false) {
|
|
3137
2856
|
const filteredParams = params.filter((p) => p !== "@children");
|
|
3138
2857
|
for (const param of filteredParams) if (param.startsWith("@")) checkVariableName(param.slice(1), param);
|
|
3139
|
-
widgets.set(name
|
|
2858
|
+
widgets.set(name, {
|
|
3140
2859
|
body: bodyAST,
|
|
3141
2860
|
params: filteredParams,
|
|
3142
2861
|
isBlock
|
|
3143
2862
|
});
|
|
3144
2863
|
}
|
|
3145
2864
|
function getWidget(name) {
|
|
3146
|
-
return widgets.get(name
|
|
2865
|
+
return widgets.get(name);
|
|
3147
2866
|
}
|
|
3148
2867
|
//#endregion
|
|
3149
2868
|
//#region src/components/macros/arg-utils.ts
|
|
@@ -3310,6 +3029,23 @@ function splitArgs(raw) {
|
|
|
3310
3029
|
return args;
|
|
3311
3030
|
}
|
|
3312
3031
|
//#endregion
|
|
3032
|
+
//#region src/components/macros/display.ts
|
|
3033
|
+
/** The text a value is shown as: nothing for null and undefined. */
|
|
3034
|
+
function display(value) {
|
|
3035
|
+
return value == null ? "" : String(value);
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* `content` in a `<span>` carrying `className` and `id`, or in a fragment if
|
|
3039
|
+
* it has neither.
|
|
3040
|
+
*/
|
|
3041
|
+
function wrapContent(className, id, content) {
|
|
3042
|
+
if (className || id) return h("span", {
|
|
3043
|
+
id,
|
|
3044
|
+
class: className
|
|
3045
|
+
}, content);
|
|
3046
|
+
return h(Fragment, null, content);
|
|
3047
|
+
}
|
|
3048
|
+
//#endregion
|
|
3313
3049
|
//#region src/interpolation.ts
|
|
3314
3050
|
/**
|
|
3315
3051
|
* Text-only markup: HTML attribute values, image alt text and link titles,
|
|
@@ -3378,9 +3114,6 @@ function mapTextNodes(nodes, fn) {
|
|
|
3378
3114
|
}
|
|
3379
3115
|
/** Deep enough for any real nesting; stops a widget that includes itself. */
|
|
3380
3116
|
var MAX_DEPTH = 100;
|
|
3381
|
-
function display(value) {
|
|
3382
|
-
return value == null ? "" : String(value);
|
|
3383
|
-
}
|
|
3384
3117
|
/**
|
|
3385
3118
|
* The value of a variable reference (`name` without its sigil, with an
|
|
3386
3119
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -3398,6 +3131,10 @@ function resolveVariable(scope, kind, name) {
|
|
|
3398
3131
|
}
|
|
3399
3132
|
return value;
|
|
3400
3133
|
}
|
|
3134
|
+
/** Evaluate an expression in `scope`. */
|
|
3135
|
+
function evaluateIn(expr, scope) {
|
|
3136
|
+
return evaluate(expr, scope.variables, scope.temporary, scope.locals, scope.transient);
|
|
3137
|
+
}
|
|
3401
3138
|
var TextEvaluator = class {
|
|
3402
3139
|
constructor() {
|
|
3403
3140
|
this.errors = [];
|
|
@@ -3407,9 +3144,6 @@ var TextEvaluator = class {
|
|
|
3407
3144
|
for (const node of nodes) text += this.node(node, scope, depth);
|
|
3408
3145
|
return text;
|
|
3409
3146
|
}
|
|
3410
|
-
evaluate(expr, scope) {
|
|
3411
|
-
return evaluate(expr, scope.variables, scope.temporary, scope.locals, scope.transient);
|
|
3412
|
-
}
|
|
3413
3147
|
node(node, scope, depth) {
|
|
3414
3148
|
switch (node.type) {
|
|
3415
3149
|
case "text": return node.value;
|
|
@@ -3421,7 +3155,7 @@ var TextEvaluator = class {
|
|
|
3421
3155
|
}
|
|
3422
3156
|
return this.variable(node.scope, node.name, scope);
|
|
3423
3157
|
case "expression": try {
|
|
3424
|
-
return display(
|
|
3158
|
+
return display(evaluateIn(node.expression, scope));
|
|
3425
3159
|
} catch (error) {
|
|
3426
3160
|
this.errors.push({
|
|
3427
3161
|
macro: "expression",
|
|
@@ -3471,7 +3205,7 @@ var TextEvaluator = class {
|
|
|
3471
3205
|
let value;
|
|
3472
3206
|
const expr = argExprs[i];
|
|
3473
3207
|
if (expr !== void 0) try {
|
|
3474
|
-
value =
|
|
3208
|
+
value = evaluateIn(expr, scope);
|
|
3475
3209
|
} catch {
|
|
3476
3210
|
value = void 0;
|
|
3477
3211
|
}
|
|
@@ -3493,7 +3227,7 @@ var TextEvaluator = class {
|
|
|
3493
3227
|
return "";
|
|
3494
3228
|
}
|
|
3495
3229
|
const ctx = {
|
|
3496
|
-
evaluate: (expr) =>
|
|
3230
|
+
evaluate: (expr) => evaluateIn(expr, scope),
|
|
3497
3231
|
renderText: (nodes, locals) => this.nested(node.name, nodes, scope, depth, locals ? { locals: withLocals(scope.locals, locals) } : {})
|
|
3498
3232
|
};
|
|
3499
3233
|
try {
|
|
@@ -3570,7 +3304,7 @@ function interpolateCode(template, scope, decode) {
|
|
|
3570
3304
|
});
|
|
3571
3305
|
}
|
|
3572
3306
|
} else try {
|
|
3573
|
-
text += display(
|
|
3307
|
+
text += display(evaluateIn(part.expr, scope));
|
|
3574
3308
|
} catch (error) {
|
|
3575
3309
|
errors.push({
|
|
3576
3310
|
macro: "expression",
|
|
@@ -3636,29 +3370,26 @@ function useInterpolate() {
|
|
|
3636
3370
|
}
|
|
3637
3371
|
//#endregion
|
|
3638
3372
|
//#region src/components/macros/VarDisplay.tsx
|
|
3639
|
-
var SIGILS = {
|
|
3640
|
-
variable: "$",
|
|
3641
|
-
temporary: "_",
|
|
3642
|
-
local: "@",
|
|
3643
|
-
transient: "%"
|
|
3644
|
-
};
|
|
3645
3373
|
var STORE_NAMESPACES = {
|
|
3646
3374
|
variable: "variables",
|
|
3647
3375
|
temporary: "temporary",
|
|
3648
3376
|
transient: "transient"
|
|
3649
3377
|
};
|
|
3650
|
-
|
|
3378
|
+
/** A `{$name}` (`{_name}`, `{@name}`, `{%name}`) variable display. */
|
|
3379
|
+
function VarDisplay({ node }) {
|
|
3380
|
+
const { name, scope } = node;
|
|
3651
3381
|
const resolve = useInterpolate();
|
|
3652
|
-
className = resolve(className);
|
|
3653
|
-
id = resolve(id);
|
|
3382
|
+
const className = resolve(node.className);
|
|
3383
|
+
const id = resolve(node.id);
|
|
3654
3384
|
const localsValues = useContext(LocalsValuesContext);
|
|
3655
3385
|
const parts = name.split(".");
|
|
3656
3386
|
const root = parts[0];
|
|
3657
3387
|
const storeValue = useStoryStore((s) => scope === "local" ? void 0 : ownValue(s[STORE_NAMESPACES[scope]], root));
|
|
3658
|
-
const
|
|
3388
|
+
const sigil = SCOPE_SIGILS[scope];
|
|
3389
|
+
const nameError = variableNameError(root, sigil + root);
|
|
3659
3390
|
if (nameError) return /* @__PURE__ */ jsx("span", {
|
|
3660
3391
|
class: "error",
|
|
3661
|
-
children: `{${
|
|
3392
|
+
children: `{${sigil}${name} error: ${nameError}}`
|
|
3662
3393
|
});
|
|
3663
3394
|
let value;
|
|
3664
3395
|
if (scope === "local") value = ownValue(localsValues, root);
|
|
@@ -3671,34 +3402,24 @@ function VarDisplay({ name, scope, className, id }) {
|
|
|
3671
3402
|
const part = parts[i];
|
|
3672
3403
|
value = value[part];
|
|
3673
3404
|
}
|
|
3674
|
-
|
|
3675
|
-
if (className || id) return /* @__PURE__ */ jsx("span", {
|
|
3676
|
-
id,
|
|
3677
|
-
class: className,
|
|
3678
|
-
children: display
|
|
3679
|
-
});
|
|
3680
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3405
|
+
return wrapContent(className, id, display(value));
|
|
3681
3406
|
}
|
|
3682
3407
|
//#endregion
|
|
3683
3408
|
//#region src/components/macros/ExprDisplay.tsx
|
|
3684
|
-
|
|
3409
|
+
/** An expression display: `{$hp + 1}`. */
|
|
3410
|
+
function ExprDisplay({ node }) {
|
|
3411
|
+
const { expression } = node;
|
|
3685
3412
|
const resolve = useInterpolate();
|
|
3686
|
-
className = resolve(className);
|
|
3687
|
-
id = resolve(id);
|
|
3413
|
+
const className = resolve(node.className);
|
|
3414
|
+
const id = resolve(node.id);
|
|
3688
3415
|
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
3689
|
-
let
|
|
3416
|
+
let text;
|
|
3690
3417
|
try {
|
|
3691
|
-
|
|
3692
|
-
display = value == null ? "" : String(value);
|
|
3418
|
+
text = display(evaluate(expression, variables, temporary, localsValues, transient));
|
|
3693
3419
|
} catch {
|
|
3694
|
-
|
|
3420
|
+
text = `{error: ${expression}}`;
|
|
3695
3421
|
}
|
|
3696
|
-
|
|
3697
|
-
id,
|
|
3698
|
-
class: className,
|
|
3699
|
-
children: display
|
|
3700
|
-
});
|
|
3701
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3422
|
+
return wrapContent(className, id, text);
|
|
3702
3423
|
}
|
|
3703
3424
|
//#endregion
|
|
3704
3425
|
//#region src/hooks/use-render-options.ts
|
|
@@ -4046,6 +3767,7 @@ var RawTextContext = createContext(false);
|
|
|
4046
3767
|
* no live form properties, so they are set as written (see splitAttributes).
|
|
4047
3768
|
*/
|
|
4048
3769
|
var SvgContext = createContext(false);
|
|
3770
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
4049
3771
|
/** Elements whose content is literal text, not markdown. */
|
|
4050
3772
|
var PREFORMATTED_ELEMENTS = new Set(["pre", "textarea"]);
|
|
4051
3773
|
var WidgetChildrenContext = createContext(null);
|
|
@@ -4090,20 +3812,42 @@ function expandPlaceholderText(text, ph) {
|
|
|
4090
3812
|
return splitPlaceholderText(text, ph).map((part) => typeof part === "number" ? ph.components[part] : part);
|
|
4091
3813
|
}
|
|
4092
3814
|
/**
|
|
4093
|
-
* An element whose attributes contain placeholders (a
|
|
4094
|
-
* text or a link title). Each such attribute is
|
|
4095
|
-
* the store and locals, so it follows its
|
|
3815
|
+
* An element of markdown output whose attributes contain placeholders (a
|
|
3816
|
+
* variable in image alt text or a link title). Each such attribute is
|
|
3817
|
+
* evaluated as text against the store and locals, so it follows its
|
|
3818
|
+
* variables as the component would.
|
|
4096
3819
|
*/
|
|
4097
|
-
function PlaceholderAttributes({ tag,
|
|
3820
|
+
function PlaceholderAttributes({ tag, attributes, placeholders, svg, children }) {
|
|
4098
3821
|
const scope = useTextScope();
|
|
4099
|
-
const resolved = { ...props };
|
|
4100
3822
|
const errors = [];
|
|
4101
|
-
|
|
3823
|
+
const { props, direct } = splitAttributes(attributes.map(([name, value, written]) => {
|
|
3824
|
+
const nodes = placeholders[name];
|
|
3825
|
+
if (!nodes) return [
|
|
3826
|
+
name,
|
|
3827
|
+
value,
|
|
3828
|
+
written
|
|
3829
|
+
];
|
|
4102
3830
|
const result = renderText(nodes, scope);
|
|
4103
|
-
resolved[name] = result.text;
|
|
4104
3831
|
for (const error of result.errors) errors.push([name, error]);
|
|
4105
|
-
|
|
4106
|
-
|
|
3832
|
+
return [
|
|
3833
|
+
name,
|
|
3834
|
+
result.text,
|
|
3835
|
+
written
|
|
3836
|
+
];
|
|
3837
|
+
}), svg);
|
|
3838
|
+
const ref = useDirectAttributes(direct);
|
|
3839
|
+
return withAttributeErrors(errors, h(tag, ref ? {
|
|
3840
|
+
...props,
|
|
3841
|
+
ref
|
|
3842
|
+
} : props, ...children));
|
|
3843
|
+
}
|
|
3844
|
+
/** An element of markdown output with attributes to set directly. */
|
|
3845
|
+
function DirectAttributes({ tag, props, direct, children }) {
|
|
3846
|
+
const ref = useDirectAttributes(direct);
|
|
3847
|
+
return h(tag, {
|
|
3848
|
+
...props,
|
|
3849
|
+
ref
|
|
3850
|
+
}, ...children);
|
|
4107
3851
|
}
|
|
4108
3852
|
/**
|
|
4109
3853
|
* Show the errors met in an element's attributes in front of it, the way a
|
|
@@ -4125,16 +3869,29 @@ function placeholderIndex(node, ph) {
|
|
|
4125
3869
|
return parseInt(value.slice(prefix.length), 10);
|
|
4126
3870
|
}
|
|
4127
3871
|
/**
|
|
3872
|
+
* Parse HTML into nodes without side effects. The content of a `<template>`
|
|
3873
|
+
* belongs to a document with no browsing context, so parsing it fetches no
|
|
3874
|
+
* images and media, runs no inline event handlers (`<img onerror>`) and
|
|
3875
|
+
* constructs no custom elements. A detached `<div>` belongs to the page's
|
|
3876
|
+
* document, which does all of these while parsing.
|
|
3877
|
+
*/
|
|
3878
|
+
function parseHtmlInert(html) {
|
|
3879
|
+
const template = document.createElement("template");
|
|
3880
|
+
template.innerHTML = html;
|
|
3881
|
+
return template.content;
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
4128
3884
|
* Convert an HTML string (from micromark) to Preact VNodes,
|
|
4129
3885
|
* replacing placeholder elements with pre-rendered components.
|
|
4130
3886
|
* With `unwrapParagraphs` (nobr or inline content), top-level <p> wrappers are
|
|
4131
3887
|
* replaced by their children.
|
|
4132
3888
|
*/
|
|
4133
3889
|
function htmlToPreact(html, ph, unwrapParagraphs = false) {
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
3890
|
+
const content = parseHtmlInert(html.trim());
|
|
3891
|
+
if (unwrapParagraphs) {
|
|
3892
|
+
for (const child of Array.from(content.childNodes)) if (child.nodeType === Node.ELEMENT_NODE && child.nodeName === "P") child.replaceWith(...Array.from(child.childNodes));
|
|
3893
|
+
}
|
|
3894
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: Array.from(content.childNodes).map((child, i) => convertDomNode(child, i, ph)) });
|
|
4138
3895
|
}
|
|
4139
3896
|
function convertDomNode(node, key, ph) {
|
|
4140
3897
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -4148,25 +3905,36 @@ function convertDomNode(node, key, ph) {
|
|
|
4148
3905
|
const tag = el.localName;
|
|
4149
3906
|
const idx = placeholderIndex(el, ph);
|
|
4150
3907
|
if (idx !== -1) return ph.components[idx];
|
|
4151
|
-
const
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
3908
|
+
const svg = el.namespaceURI === SVG_NAMESPACE;
|
|
3909
|
+
const attributes = [];
|
|
3910
|
+
let placeholders;
|
|
3911
|
+
for (const { name, value } of Array.from(el.attributes)) {
|
|
3912
|
+
attributes.push([
|
|
3913
|
+
name,
|
|
3914
|
+
value,
|
|
3915
|
+
value
|
|
3916
|
+
]);
|
|
3917
|
+
const parts = splitPlaceholderText(value, ph);
|
|
3918
|
+
if (parts.every((part) => typeof part === "string")) continue;
|
|
3919
|
+
placeholders ??= {};
|
|
3920
|
+
placeholders[name] = parts.map((part) => typeof part === "string" ? {
|
|
4161
3921
|
type: "text",
|
|
4162
3922
|
value: part
|
|
4163
3923
|
} : ph.nodes[part]);
|
|
4164
3924
|
}
|
|
4165
3925
|
const children = Array.from(el.childNodes).map((child, i) => convertDomNode(child, i, ph));
|
|
4166
|
-
if (
|
|
3926
|
+
if (placeholders) return /* @__PURE__ */ jsx(PlaceholderAttributes, {
|
|
3927
|
+
tag,
|
|
3928
|
+
attributes,
|
|
3929
|
+
placeholders,
|
|
3930
|
+
svg,
|
|
3931
|
+
children
|
|
3932
|
+
}, key);
|
|
3933
|
+
const { props, direct } = splitAttributes(attributes, svg);
|
|
3934
|
+
if (direct.length > 0) return /* @__PURE__ */ jsx(DirectAttributes, {
|
|
4167
3935
|
tag,
|
|
4168
3936
|
props,
|
|
4169
|
-
|
|
3937
|
+
direct,
|
|
4170
3938
|
children
|
|
4171
3939
|
}, key);
|
|
4172
3940
|
return h(tag, {
|
|
@@ -4281,9 +4049,7 @@ function setRawAttribute(el, name, value) {
|
|
|
4281
4049
|
el.setAttribute(name, value);
|
|
4282
4050
|
return;
|
|
4283
4051
|
}
|
|
4284
|
-
const
|
|
4285
|
-
template.innerHTML = `<i ${name}=""></i>`;
|
|
4286
|
-
const parsed = template.content.firstChild.attributes[0];
|
|
4052
|
+
const parsed = parseHtmlInert(`<i ${name}=""></i>`).firstChild.attributes[0];
|
|
4287
4053
|
if (!parsed) return;
|
|
4288
4054
|
const attr = parsed.cloneNode();
|
|
4289
4055
|
attr.value = value;
|
|
@@ -4321,6 +4087,23 @@ function splitAttributes(attributes, svg) {
|
|
|
4321
4087
|
direct
|
|
4322
4088
|
};
|
|
4323
4089
|
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Set the attributes `splitAttributes` leaves to set directly on the element
|
|
4092
|
+
* given the returned ref (none if there are none), and remove them again
|
|
4093
|
+
* when they change.
|
|
4094
|
+
*/
|
|
4095
|
+
function useDirectAttributes(direct) {
|
|
4096
|
+
const elementRef = useRef(null);
|
|
4097
|
+
useLayoutEffect(() => {
|
|
4098
|
+
const el = elementRef.current;
|
|
4099
|
+
if (!el || direct.length === 0) return;
|
|
4100
|
+
for (const [name, value] of direct) setRawAttribute(el, name, value);
|
|
4101
|
+
return () => {
|
|
4102
|
+
for (const [name] of direct) el.removeAttribute(name);
|
|
4103
|
+
};
|
|
4104
|
+
}, [JSON.stringify(direct)]);
|
|
4105
|
+
return direct.length > 0 ? elementRef : void 0;
|
|
4106
|
+
}
|
|
4324
4107
|
var decodedAttributeText = /* @__PURE__ */ new Map();
|
|
4325
4108
|
/**
|
|
4326
4109
|
* Decode character references (`&`, `{`) in attribute text the way
|
|
@@ -4330,9 +4113,7 @@ function decodeAttributeText(text) {
|
|
|
4330
4113
|
if (!text.includes("&")) return text;
|
|
4331
4114
|
let decoded = decodedAttributeText.get(text);
|
|
4332
4115
|
if (decoded === void 0) {
|
|
4333
|
-
|
|
4334
|
-
template.innerHTML = `<i title="${text.replace(/"/g, """)}"></i>`;
|
|
4335
|
-
decoded = template.content.firstChild.getAttribute("title") ?? text;
|
|
4116
|
+
decoded = parseHtmlInert(`<i title="${text.replace(/"/g, """)}"></i>`).firstChild.getAttribute("title") ?? text;
|
|
4336
4117
|
decodedAttributeText.set(text, decoded);
|
|
4337
4118
|
}
|
|
4338
4119
|
return decoded;
|
|
@@ -4397,16 +4178,8 @@ function HtmlNodeRenderer({ node }) {
|
|
|
4397
4178
|
resolveAttributeValue(k, v, scope, errors),
|
|
4398
4179
|
v
|
|
4399
4180
|
]), inSvg || isSvgRoot);
|
|
4400
|
-
const
|
|
4401
|
-
|
|
4402
|
-
const el = elementRef.current;
|
|
4403
|
-
if (!el || direct.length === 0) return;
|
|
4404
|
-
for (const [name, value] of direct) setRawAttribute(el, name, value);
|
|
4405
|
-
return () => {
|
|
4406
|
-
for (const [name] of direct) el.removeAttribute(name);
|
|
4407
|
-
};
|
|
4408
|
-
}, [JSON.stringify(direct)]);
|
|
4409
|
-
if (direct.length > 0) props.ref = elementRef;
|
|
4181
|
+
const ref = useDirectAttributes(direct);
|
|
4182
|
+
if (ref) props.ref = ref;
|
|
4410
4183
|
const isInline = INLINE_ELEMENTS.has(tag);
|
|
4411
4184
|
let children = void 0;
|
|
4412
4185
|
if (node.children.length > 0) if (inRaw || isRawRoot) children = renderInlineNodes(node.children);
|
|
@@ -4433,17 +4206,9 @@ function HtmlNodeRenderer({ node }) {
|
|
|
4433
4206
|
}
|
|
4434
4207
|
function ChildrenSlot() {
|
|
4435
4208
|
const childrenAST = useContext(WidgetChildrenContext);
|
|
4436
|
-
const
|
|
4437
|
-
const inline = useContext(InlineContext);
|
|
4438
|
-
const raw = useContext(RawTextContext);
|
|
4439
|
-
const locals = useContext(LocalsValuesContext);
|
|
4209
|
+
const renderOptions = useRenderOptions();
|
|
4440
4210
|
if (!childrenAST || childrenAST.length === 0) return null;
|
|
4441
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: renderNodes(childrenAST,
|
|
4442
|
-
nobr,
|
|
4443
|
-
locals,
|
|
4444
|
-
inline,
|
|
4445
|
-
raw
|
|
4446
|
-
}) });
|
|
4211
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: renderNodes(childrenAST, renderOptions) });
|
|
4447
4212
|
}
|
|
4448
4213
|
/**
|
|
4449
4214
|
* Stable per-node keys. Keying rendered children by AST node identity (not
|
|
@@ -4493,17 +4258,8 @@ function renderSingleNode(node) {
|
|
|
4493
4258
|
switch (node.type) {
|
|
4494
4259
|
case "variable":
|
|
4495
4260
|
if (node.scope === "local" && node.name === "children") return /* @__PURE__ */ jsx(ChildrenSlot, {}, key);
|
|
4496
|
-
return /* @__PURE__ */ jsx(VarDisplay, {
|
|
4497
|
-
|
|
4498
|
-
scope: node.scope,
|
|
4499
|
-
className: node.className,
|
|
4500
|
-
id: node.id
|
|
4501
|
-
}, key);
|
|
4502
|
-
case "expression": return /* @__PURE__ */ jsx(ExprDisplay, {
|
|
4503
|
-
expression: node.expression,
|
|
4504
|
-
className: node.className,
|
|
4505
|
-
id: node.id
|
|
4506
|
-
}, key);
|
|
4261
|
+
return /* @__PURE__ */ jsx(VarDisplay, { node }, key);
|
|
4262
|
+
case "expression": return /* @__PURE__ */ jsx(ExprDisplay, { node }, key);
|
|
4507
4263
|
case "macro": return renderMacro(node, key);
|
|
4508
4264
|
case "html": return /* @__PURE__ */ jsx(HtmlNodeRenderer, { node }, key);
|
|
4509
4265
|
default: return node;
|
|
@@ -4784,6 +4540,24 @@ function createLocalStorageBackend() {
|
|
|
4784
4540
|
}
|
|
4785
4541
|
var IDB_DB_NAME = "spindle";
|
|
4786
4542
|
var IDB_DB_VERSION = 1;
|
|
4543
|
+
/** Each object store's key path, and the key path of each of its indexes. */
|
|
4544
|
+
var IDB_STORES = {
|
|
4545
|
+
saves: {
|
|
4546
|
+
keyPath: "meta.id",
|
|
4547
|
+
indexes: {
|
|
4548
|
+
ifid: "meta.ifid",
|
|
4549
|
+
playthroughId: "meta.playthroughId"
|
|
4550
|
+
}
|
|
4551
|
+
},
|
|
4552
|
+
playthroughs: {
|
|
4553
|
+
keyPath: "id",
|
|
4554
|
+
indexes: { ifid: "ifid" }
|
|
4555
|
+
},
|
|
4556
|
+
meta: {
|
|
4557
|
+
keyPath: "key",
|
|
4558
|
+
indexes: {}
|
|
4559
|
+
}
|
|
4560
|
+
};
|
|
4787
4561
|
function createIDBBackend() {
|
|
4788
4562
|
let dbPromise = null;
|
|
4789
4563
|
function openDB() {
|
|
@@ -4792,13 +4566,11 @@ function createIDBBackend() {
|
|
|
4792
4566
|
const req = indexedDB.open(IDB_DB_NAME, IDB_DB_VERSION);
|
|
4793
4567
|
req.onupgradeneeded = () => {
|
|
4794
4568
|
const db = req.result;
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4569
|
+
for (const [name, { keyPath, indexes }] of Object.entries(IDB_STORES)) {
|
|
4570
|
+
if (db.objectStoreNames.contains(name)) continue;
|
|
4571
|
+
const store = db.createObjectStore(name, { keyPath });
|
|
4572
|
+
for (const [index, path] of Object.entries(indexes)) store.createIndex(index, path, { unique: false });
|
|
4799
4573
|
}
|
|
4800
|
-
if (!db.objectStoreNames.contains("playthroughs")) db.createObjectStore("playthroughs", { keyPath: "id" }).createIndex("ifid", "ifid", { unique: false });
|
|
4801
|
-
if (!db.objectStoreNames.contains("meta")) db.createObjectStore("meta", { keyPath: "key" });
|
|
4802
4574
|
};
|
|
4803
4575
|
req.onsuccess = () => resolve(req.result);
|
|
4804
4576
|
req.onerror = () => reject(req.error);
|
|
@@ -4849,57 +4621,389 @@ function createIDBBackend() {
|
|
|
4849
4621
|
req.onsuccess = () => resolve();
|
|
4850
4622
|
req.onerror = () => reject(req.error);
|
|
4851
4623
|
});
|
|
4852
|
-
});
|
|
4853
|
-
}
|
|
4854
|
-
var _backend = null;
|
|
4855
|
-
async function detectBackend() {
|
|
4856
|
-
if (typeof indexedDB !== "undefined") try {
|
|
4857
|
-
await new Promise((resolve, reject) => {
|
|
4858
|
-
const req = indexedDB.open("__spindle_probe__", 1);
|
|
4859
|
-
req.onsuccess = () => {
|
|
4860
|
-
req.result.close();
|
|
4861
|
-
indexedDB.deleteDatabase("__spindle_probe__");
|
|
4862
|
-
resolve();
|
|
4863
|
-
};
|
|
4864
|
-
req.onerror = () => reject(req.error);
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
var _backend = null;
|
|
4627
|
+
async function detectBackend() {
|
|
4628
|
+
if (typeof indexedDB !== "undefined") try {
|
|
4629
|
+
await new Promise((resolve, reject) => {
|
|
4630
|
+
const req = indexedDB.open("__spindle_probe__", 1);
|
|
4631
|
+
req.onsuccess = () => {
|
|
4632
|
+
req.result.close();
|
|
4633
|
+
indexedDB.deleteDatabase("__spindle_probe__");
|
|
4634
|
+
resolve();
|
|
4635
|
+
};
|
|
4636
|
+
req.onerror = () => reject(req.error);
|
|
4637
|
+
});
|
|
4638
|
+
return createIDBBackend();
|
|
4639
|
+
} catch {}
|
|
4640
|
+
if (typeof localStorage !== "undefined") try {
|
|
4641
|
+
const probeKey = "__spindle_probe__";
|
|
4642
|
+
localStorage.setItem(probeKey, "1");
|
|
4643
|
+
const val = localStorage.getItem(probeKey);
|
|
4644
|
+
localStorage.removeItem(probeKey);
|
|
4645
|
+
if (val === "1") return createLocalStorageBackend();
|
|
4646
|
+
} catch {}
|
|
4647
|
+
return createMemoryBackend();
|
|
4648
|
+
}
|
|
4649
|
+
/**
|
|
4650
|
+
* Detection in flight. Concurrent first callers share it so they all get the
|
|
4651
|
+
* same backend (the memory fallback makes a new, empty store per detection).
|
|
4652
|
+
*/
|
|
4653
|
+
var _backendPromise = null;
|
|
4654
|
+
function getBackend() {
|
|
4655
|
+
if (_backend) return Promise.resolve(_backend);
|
|
4656
|
+
if (!_backendPromise) {
|
|
4657
|
+
const pending = detectBackend().then((backend) => {
|
|
4658
|
+
if (_backendPromise === pending) _backend = backend;
|
|
4659
|
+
return backend;
|
|
4660
|
+
});
|
|
4661
|
+
_backendPromise = pending;
|
|
4662
|
+
}
|
|
4663
|
+
return _backendPromise;
|
|
4664
|
+
}
|
|
4665
|
+
function resetBackend() {
|
|
4666
|
+
_backend = null;
|
|
4667
|
+
_backendPromise = null;
|
|
4668
|
+
}
|
|
4669
|
+
/**
|
|
4670
|
+
* Synchronous accessor for the current backend type.
|
|
4671
|
+
* Returns 'memory' if the backend hasn't been detected yet.
|
|
4672
|
+
*/
|
|
4673
|
+
function getBackendType() {
|
|
4674
|
+
return _backend?.type ?? "memory";
|
|
4675
|
+
}
|
|
4676
|
+
//#endregion
|
|
4677
|
+
//#region src/utils/object-path.ts
|
|
4678
|
+
/**
|
|
4679
|
+
* Traverse dot-path segments on an object and return the nested value.
|
|
4680
|
+
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
4681
|
+
* are not story state: unless an object holds one as its own property, it
|
|
4682
|
+
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
4683
|
+
* (class getters, `size` of a Map) are read.
|
|
4684
|
+
*/
|
|
4685
|
+
function getByPath(obj, segments) {
|
|
4686
|
+
let current = obj;
|
|
4687
|
+
for (const seg of segments) {
|
|
4688
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
4689
|
+
if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
|
|
4690
|
+
current = current[seg];
|
|
4691
|
+
}
|
|
4692
|
+
return current;
|
|
4693
|
+
}
|
|
4694
|
+
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
4695
|
+
var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
4696
|
+
/**
|
|
4697
|
+
* Built-ins whose content is not their properties: a property written into
|
|
4698
|
+
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
4699
|
+
*/
|
|
4700
|
+
var builtinName = (value) => value instanceof Map ? "a Map" : value instanceof Set ? "a Set" : value instanceof Date ? "a Date" : value instanceof RegExp ? "a RegExp" : void 0;
|
|
4701
|
+
/**
|
|
4702
|
+
* Shallow copy that keeps the prototype, so a registered class instance
|
|
4703
|
+
* stays an instance of its class (with the same own keys deepClone copies).
|
|
4704
|
+
*/
|
|
4705
|
+
function shallowCopy(value) {
|
|
4706
|
+
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
4707
|
+
for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
|
|
4708
|
+
value: value[key],
|
|
4709
|
+
enumerable: true,
|
|
4710
|
+
writable: true,
|
|
4711
|
+
configurable: true
|
|
4712
|
+
});
|
|
4713
|
+
return copy;
|
|
4714
|
+
}
|
|
4715
|
+
/**
|
|
4716
|
+
* Set a value at dot-path `segments` below `root`.
|
|
4717
|
+
*
|
|
4718
|
+
* When `root` is an Immer draft, objects Immer does not draft (registered
|
|
4719
|
+
* class instances and everything below them) are copied on the way down and
|
|
4720
|
+
* the copy is assigned to its parent, so the write produces new references
|
|
4721
|
+
* up to the root. Writing into such an object directly would mutate the
|
|
4722
|
+
* instance shared with the previous state and with history, and subscribers
|
|
4723
|
+
* would see no change. Outside a draft (a private working copy) the path is
|
|
4724
|
+
* written in place.
|
|
4725
|
+
*
|
|
4726
|
+
* The path goes through own properties of plain objects, class instances
|
|
4727
|
+
* and arrays (by index); anything else throws a TypeError rather than
|
|
4728
|
+
* writing where no clone or save would see it: an inherited property such
|
|
4729
|
+
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
4730
|
+
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
4731
|
+
*/
|
|
4732
|
+
function setByPath(root, segments, value, options = {}) {
|
|
4733
|
+
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
4734
|
+
parent[segments[segments.length - 1]] = value;
|
|
4735
|
+
}
|
|
4736
|
+
/**
|
|
4737
|
+
* Delete the property at dot-path `segments` below `root`, copying
|
|
4738
|
+
* undrafted objects on the way down like setByPath(). Does nothing when an
|
|
4739
|
+
* intermediate is missing or not an object, or the property is absent (or
|
|
4740
|
+
* only inherited).
|
|
4741
|
+
*/
|
|
4742
|
+
function deleteByPath(root, segments) {
|
|
4743
|
+
const last = segments[segments.length - 1];
|
|
4744
|
+
checkSegments(segments);
|
|
4745
|
+
let holder = root;
|
|
4746
|
+
for (const seg of segments.slice(0, -1)) {
|
|
4747
|
+
if (holder == null || typeof holder !== "object" || !hasOwn(holder, seg)) return;
|
|
4748
|
+
holder = holder[seg];
|
|
4749
|
+
}
|
|
4750
|
+
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
4751
|
+
const parent = walkToParent(root, segments, false);
|
|
4752
|
+
delete parent[last];
|
|
4753
|
+
}
|
|
4754
|
+
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
4755
|
+
function checkSegments(segments) {
|
|
4756
|
+
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
4757
|
+
}
|
|
4758
|
+
/** Throw unless `holder` can take `key` as story state (see setByPath). */
|
|
4759
|
+
function checkHolder(holder, key, segments, depth) {
|
|
4760
|
+
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
4761
|
+
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
4762
|
+
}
|
|
4763
|
+
/**
|
|
4764
|
+
* Walk to the object holding the last of `segments`, copying undrafted
|
|
4765
|
+
* objects when `root` is an Immer draft (see setByPath). A missing or
|
|
4766
|
+
* non-object intermediate is replaced by a plain object when
|
|
4767
|
+
* `createMissing` is set, and throws a TypeError otherwise.
|
|
4768
|
+
*/
|
|
4769
|
+
function walkToParent(root, segments, createMissing) {
|
|
4770
|
+
checkSegments(segments);
|
|
4771
|
+
const copyOnWrite = isDraft(root);
|
|
4772
|
+
let current = root;
|
|
4773
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
4774
|
+
const seg = segments[i];
|
|
4775
|
+
checkHolder(current, seg, segments, i);
|
|
4776
|
+
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
4777
|
+
if (next == null || typeof next !== "object") {
|
|
4778
|
+
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
4779
|
+
next = {};
|
|
4780
|
+
current[seg] = next;
|
|
4781
|
+
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
4782
|
+
next = shallowCopy(next);
|
|
4783
|
+
current[seg] = next;
|
|
4784
|
+
}
|
|
4785
|
+
current = next;
|
|
4786
|
+
}
|
|
4787
|
+
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
4788
|
+
return current;
|
|
4789
|
+
}
|
|
4790
|
+
//#endregion
|
|
4791
|
+
//#region src/structural.ts
|
|
4792
|
+
function isPlainObject(value) {
|
|
4793
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4794
|
+
const proto = Object.getPrototypeOf(value);
|
|
4795
|
+
return proto === Object.prototype || proto === null;
|
|
4796
|
+
}
|
|
4797
|
+
function deepClone(value, options = {}) {
|
|
4798
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4799
|
+
function clone(val) {
|
|
4800
|
+
if (val === null || typeof val !== "object") return val;
|
|
4801
|
+
const obj = val;
|
|
4802
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
4803
|
+
if (val instanceof Date) return new Date(val.getTime());
|
|
4804
|
+
if (val instanceof RegExp) return new RegExp(val.source, val.flags);
|
|
4805
|
+
if (Array.isArray(val)) {
|
|
4806
|
+
const arr = [];
|
|
4807
|
+
seen.set(obj, arr);
|
|
4808
|
+
for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
|
|
4809
|
+
return arr;
|
|
4810
|
+
}
|
|
4811
|
+
if (val instanceof Map) {
|
|
4812
|
+
const copy = /* @__PURE__ */ new Map();
|
|
4813
|
+
seen.set(obj, copy);
|
|
4814
|
+
for (const [k, v] of val) copy.set(clone(k), clone(v));
|
|
4815
|
+
return copy;
|
|
4816
|
+
}
|
|
4817
|
+
if (val instanceof Set) {
|
|
4818
|
+
const copy = /* @__PURE__ */ new Set();
|
|
4819
|
+
seen.set(obj, copy);
|
|
4820
|
+
for (const v of val) copy.add(clone(v));
|
|
4821
|
+
return copy;
|
|
4822
|
+
}
|
|
4823
|
+
const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
|
|
4824
|
+
if (!keepsProto && options.keepUnregistered) return val;
|
|
4825
|
+
const copy = keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {};
|
|
4826
|
+
seen.set(obj, copy);
|
|
4827
|
+
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
4828
|
+
return copy;
|
|
4829
|
+
}
|
|
4830
|
+
return clone(value);
|
|
4831
|
+
}
|
|
4832
|
+
/**
|
|
4833
|
+
* Structural equality over the value types deepClone() supports: primitives,
|
|
4834
|
+
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
4835
|
+
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
4836
|
+
* are compared by length and index, so a hole equals an undefined element
|
|
4837
|
+
* (deepClone() and save/load turn holes into undefined elements).
|
|
4838
|
+
*/
|
|
4839
|
+
function deepEqual(a, b) {
|
|
4840
|
+
return equal(a, b, /* @__PURE__ */ new Map());
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
4844
|
+
* cycle) assumes it equal. Every object may pair with several others, since
|
|
4845
|
+
* cycles of different lengths can still unfold to the same value.
|
|
4846
|
+
*/
|
|
4847
|
+
function equal(a, b, assumed) {
|
|
4848
|
+
if (Object.is(a, b)) return true;
|
|
4849
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
4850
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
4851
|
+
const pairs = assumed.get(a);
|
|
4852
|
+
if (pairs?.has(b)) return true;
|
|
4853
|
+
if (pairs) pairs.add(b);
|
|
4854
|
+
else assumed.set(a, new Set([b]));
|
|
4855
|
+
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
4856
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
4857
|
+
if (a instanceof Map || a instanceof Set) {
|
|
4858
|
+
const bc = b;
|
|
4859
|
+
if (a.size !== bc.size) return false;
|
|
4860
|
+
const ai = a.entries();
|
|
4861
|
+
const bi = bc.entries();
|
|
4862
|
+
for (let x = ai.next(), y = bi.next(); !x.done; x = ai.next(), y = bi.next()) if (!equal(x.value, y.value, assumed)) return false;
|
|
4863
|
+
return true;
|
|
4864
|
+
}
|
|
4865
|
+
if (Array.isArray(a)) {
|
|
4866
|
+
const bc = b;
|
|
4867
|
+
if (a.length !== bc.length) return false;
|
|
4868
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
4869
|
+
return true;
|
|
4870
|
+
}
|
|
4871
|
+
const ao = a;
|
|
4872
|
+
const bo = b;
|
|
4873
|
+
const keys = Object.keys(ao);
|
|
4874
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
4875
|
+
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
4876
|
+
return true;
|
|
4877
|
+
}
|
|
4878
|
+
/**
|
|
4879
|
+
* Objects merged property by property: plain objects and (registered)
|
|
4880
|
+
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
4881
|
+
* replaced as a whole, since their elements have no stable identity to
|
|
4882
|
+
* merge by (a shift moves every index).
|
|
4883
|
+
*/
|
|
4884
|
+
function isMergeable(value) {
|
|
4885
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
|
|
4886
|
+
}
|
|
4887
|
+
/**
|
|
4888
|
+
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
4889
|
+
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
4890
|
+
*/
|
|
4891
|
+
function mergesWith(a, b, arrays) {
|
|
4892
|
+
if (arrays && (Array.isArray(a) || Array.isArray(b))) return Array.isArray(a) && Array.isArray(b);
|
|
4893
|
+
return isMergeable(a) && isMergeable(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
4894
|
+
}
|
|
4895
|
+
/**
|
|
4896
|
+
* The keys at which `after` differs from `before`, two values that merge
|
|
4897
|
+
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
4898
|
+
* updates are immutable, so a value that kept its identity is no change,
|
|
4899
|
+
* and one rebuilt with equal content (mutation code commits whole values)
|
|
4900
|
+
* is none either. Values that merge are reported as 'nested', to be
|
|
4901
|
+
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
4902
|
+
* merge too: elements at the indices both hold are compared, and the
|
|
4903
|
+
* elements one has beyond the other's length are added or deleted.
|
|
4904
|
+
*/
|
|
4905
|
+
function keyChanges(before, after, arrays) {
|
|
4906
|
+
const changes = [];
|
|
4907
|
+
const compare = (key, b, a) => {
|
|
4908
|
+
if (Object.is(b, a)) return;
|
|
4909
|
+
if (mergesWith(b, a, arrays)) changes.push({
|
|
4910
|
+
key,
|
|
4911
|
+
kind: "nested",
|
|
4912
|
+
before: b,
|
|
4913
|
+
after: a
|
|
4914
|
+
});
|
|
4915
|
+
else if (!deepEqual(b, a)) changes.push({
|
|
4916
|
+
key,
|
|
4917
|
+
kind: "changed",
|
|
4918
|
+
after: a
|
|
4919
|
+
});
|
|
4920
|
+
};
|
|
4921
|
+
if (Array.isArray(after)) {
|
|
4922
|
+
const b = before;
|
|
4923
|
+
const a = after;
|
|
4924
|
+
for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
|
|
4925
|
+
for (let i = b.length; i < a.length; i++) changes.push({
|
|
4926
|
+
key: String(i),
|
|
4927
|
+
kind: "added",
|
|
4928
|
+
after: a[i]
|
|
4929
|
+
});
|
|
4930
|
+
for (let i = a.length; i < b.length; i++) changes.push({
|
|
4931
|
+
key: String(i),
|
|
4932
|
+
kind: "deleted"
|
|
4865
4933
|
});
|
|
4866
|
-
return
|
|
4867
|
-
}
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4934
|
+
return changes;
|
|
4935
|
+
}
|
|
4936
|
+
for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
4937
|
+
else changes.push({
|
|
4938
|
+
key,
|
|
4939
|
+
kind: "added",
|
|
4940
|
+
after: after[key]
|
|
4941
|
+
});
|
|
4942
|
+
for (const key of Object.keys(before)) if (!hasOwn(after, key)) changes.push({
|
|
4943
|
+
key,
|
|
4944
|
+
kind: "deleted"
|
|
4945
|
+
});
|
|
4946
|
+
return changes;
|
|
4876
4947
|
}
|
|
4877
4948
|
/**
|
|
4878
|
-
*
|
|
4879
|
-
*
|
|
4949
|
+
* The property paths where `after` differs from `before`, two objects of
|
|
4950
|
+
* one class: objects that merge (see isMergeable) are compared property by
|
|
4951
|
+
* property, any other value as a whole.
|
|
4880
4952
|
*/
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4953
|
+
function diffPaths(before, after) {
|
|
4954
|
+
const changes = [];
|
|
4955
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
4956
|
+
(function walk(b, a, path) {
|
|
4957
|
+
if (ancestors.has(a)) return;
|
|
4958
|
+
ancestors.add(a);
|
|
4959
|
+
for (const change of keyChanges(b, a, false)) {
|
|
4960
|
+
const at = [...path, change.key];
|
|
4961
|
+
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
4962
|
+
else changes.push(change.kind === "deleted" ? {
|
|
4963
|
+
path: at,
|
|
4964
|
+
deleted: true
|
|
4965
|
+
} : {
|
|
4966
|
+
path: at,
|
|
4967
|
+
deleted: false,
|
|
4968
|
+
value: change.after
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
ancestors.delete(a);
|
|
4972
|
+
})(before, after, []);
|
|
4973
|
+
return changes;
|
|
4892
4974
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4975
|
+
/** Whether `target` already holds what `change` would write. */
|
|
4976
|
+
function isApplied(target, change) {
|
|
4977
|
+
const parent = getByPath(target, change.path.slice(0, -1));
|
|
4978
|
+
if (parent === null || typeof parent !== "object") return change.deleted;
|
|
4979
|
+
const key = change.path[change.path.length - 1];
|
|
4980
|
+
if (!hasOwn(parent, key)) return change.deleted;
|
|
4981
|
+
return !change.deleted && deepEqual(parent[key], change.value);
|
|
4982
|
+
}
|
|
4983
|
+
function applyChange(target, change) {
|
|
4984
|
+
if (change.deleted) deleteByPath(target, change.path);
|
|
4985
|
+
else setByPath(target, change.path, change.value);
|
|
4896
4986
|
}
|
|
4897
4987
|
/**
|
|
4898
|
-
*
|
|
4899
|
-
*
|
|
4988
|
+
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
4989
|
+
* index) into `target`, another version of the same object or array.
|
|
4990
|
+
* Values that changed are written whole, as deep copies; for values that
|
|
4991
|
+
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
4992
|
+
* whether it did. Into an array, elements removed from the end are removed
|
|
4993
|
+
* at the same indices and elements added are appended, and changes at
|
|
4994
|
+
* indices `target` lacks are dropped: where `target` was resized, its
|
|
4995
|
+
* indices do not line up with `before`'s.
|
|
4900
4996
|
*/
|
|
4901
|
-
function
|
|
4902
|
-
|
|
4997
|
+
function mergeKeys(target, before, after, mergeNested) {
|
|
4998
|
+
const list = Array.isArray(target) ? target : void 0;
|
|
4999
|
+
for (const change of keyChanges(before, after, true)) {
|
|
5000
|
+
const { key } = change;
|
|
5001
|
+
if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
|
|
5002
|
+
else delete target[key];
|
|
5003
|
+
else if (list && change.kind === "added") list.push(deepClone(change.after));
|
|
5004
|
+
else if (list && Number(key) >= list.length) continue;
|
|
5005
|
+
else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
|
|
5006
|
+
}
|
|
4903
5007
|
}
|
|
4904
5008
|
//#endregion
|
|
4905
5009
|
//#region src/event-emitter.ts
|
|
@@ -5002,6 +5106,10 @@ function inOrder(op) {
|
|
|
5002
5106
|
function queued(op) {
|
|
5003
5107
|
return (...args) => inOrder(() => op(...args));
|
|
5004
5108
|
}
|
|
5109
|
+
/** An operation passing the save `saveId`, if it is stored, to `read`. */
|
|
5110
|
+
var readingSave = (read) => queued(async (saveId) => read(await (await getBackend()).getSave(saveId)));
|
|
5111
|
+
/** An operation passing the save a slot holds, if any, to `read`. */
|
|
5112
|
+
var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(ifid, slot)));
|
|
5005
5113
|
/**
|
|
5006
5114
|
* Store a new playthrough and make it the story's current one. Pass `id` when
|
|
5007
5115
|
* the caller has already switched to the playthrough (restart() does, so
|
|
@@ -5202,7 +5310,7 @@ function deserializePayload(payload) {
|
|
|
5202
5310
|
}
|
|
5203
5311
|
/** The live payload of a stored save, if there is one. */
|
|
5204
5312
|
var livePayload = (record) => record && deserializePayload(record.payload);
|
|
5205
|
-
|
|
5313
|
+
readingSave(livePayload);
|
|
5206
5314
|
/**
|
|
5207
5315
|
* Delete a save record. If the default slot or a named slot holds it, that
|
|
5208
5316
|
* slot is cleared as well (pointer and slot index), as `deleteSlotSave` would.
|
|
@@ -5358,7 +5466,7 @@ queued(hasQuickSaveNow);
|
|
|
5358
5466
|
async function hasQuickSaveNow(ifid, slot) {
|
|
5359
5467
|
return await slotRecord(ifid, slot) !== void 0;
|
|
5360
5468
|
}
|
|
5361
|
-
|
|
5469
|
+
readingSlot(livePayload);
|
|
5362
5470
|
/**
|
|
5363
5471
|
* Read the save held in a slot (the default slot when `slot` is omitted) for
|
|
5364
5472
|
* the running game to load, and make its playthrough the story's current one
|
|
@@ -5454,7 +5562,7 @@ function toExport(record) {
|
|
|
5454
5562
|
save: record
|
|
5455
5563
|
};
|
|
5456
5564
|
}
|
|
5457
|
-
var exportSave =
|
|
5565
|
+
var exportSave = readingSave(toExport);
|
|
5458
5566
|
/**
|
|
5459
5567
|
* Validate an export against the running story and build the record to store:
|
|
5460
5568
|
* a fresh save ID, and the playthrough record created if it doesn't exist yet.
|
|
@@ -5482,7 +5590,7 @@ function importSave(data, ifid) {
|
|
|
5482
5590
|
* Export the save held in a slot (default autosave slot when `slot` is omitted).
|
|
5483
5591
|
* Returns undefined if the slot is empty.
|
|
5484
5592
|
*/
|
|
5485
|
-
var exportSlotSave =
|
|
5593
|
+
var exportSlotSave = readingSlot(toExport);
|
|
5486
5594
|
/**
|
|
5487
5595
|
* Import an exported save into a slot (default autosave slot when `slot` is
|
|
5488
5596
|
* omitted), replacing any save already in that slot. Applies the same
|
|
@@ -7046,7 +7154,8 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
7046
7154
|
//#region src/components/StoryInterface.tsx
|
|
7047
7155
|
var DEFAULT_MARKUP = "<header class=\"story-menubar\">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}";
|
|
7048
7156
|
function StoryInterface() {
|
|
7049
|
-
const
|
|
7157
|
+
const { storyData } = useStoryFields("storyData");
|
|
7158
|
+
const overridePassage = storyData?.passages.get("StoryInterface");
|
|
7050
7159
|
const markup = overridePassage !== void 0 ? overridePassage.content : DEFAULT_MARKUP;
|
|
7051
7160
|
const nobr = overridePassage?.tags.includes("nobr") ?? false;
|
|
7052
7161
|
const rendered = useMemo(() => {
|
|
@@ -7146,7 +7255,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
7146
7255
|
const onCloseRef = useRef(onClose);
|
|
7147
7256
|
onCloseRef.current = onClose;
|
|
7148
7257
|
const stableOnClose = useCallback(() => onCloseRef.current(), []);
|
|
7149
|
-
const storyData =
|
|
7258
|
+
const { storyData } = useStoryFields("storyData");
|
|
7150
7259
|
const passage = passageName ? storyData?.passages.get(passageName) : void 0;
|
|
7151
7260
|
const markup = passage?.content ?? fallbackMarkup;
|
|
7152
7261
|
const nobr = passage?.tags.includes("nobr") ?? false;
|
|
@@ -7278,19 +7387,92 @@ function App() {
|
|
|
7278
7387
|
}
|
|
7279
7388
|
//#endregion
|
|
7280
7389
|
//#region src/parser.ts
|
|
7390
|
+
var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
7391
|
+
/** HTML elements written without content or end tag, as innerHTML does. */
|
|
7392
|
+
var VOID_ELEMENTS = new Set([
|
|
7393
|
+
"area",
|
|
7394
|
+
"base",
|
|
7395
|
+
"basefont",
|
|
7396
|
+
"bgsound",
|
|
7397
|
+
"br",
|
|
7398
|
+
"col",
|
|
7399
|
+
"embed",
|
|
7400
|
+
"frame",
|
|
7401
|
+
"hr",
|
|
7402
|
+
"img",
|
|
7403
|
+
"input",
|
|
7404
|
+
"keygen",
|
|
7405
|
+
"link",
|
|
7406
|
+
"meta",
|
|
7407
|
+
"param",
|
|
7408
|
+
"source",
|
|
7409
|
+
"track",
|
|
7410
|
+
"wbr"
|
|
7411
|
+
]);
|
|
7412
|
+
/**
|
|
7413
|
+
* The passage markup in the nodes of passage data.
|
|
7414
|
+
*
|
|
7415
|
+
* A compiler that HTML-escapes passage text (Twine, Tweego) leaves a text
|
|
7416
|
+
* node whose text is the markup; a no-break space in it is U+00A0, which
|
|
7417
|
+
* innerHTML writes as ` `. One that doesn't escape it leaves the
|
|
7418
|
+
* browser's parse of it, whose elements are written back as markup here.
|
|
7419
|
+
* innerHTML did that too, but its escaping can't be undone: a `"` in an
|
|
7420
|
+
* attribute value is written `"`, and decoding that ends the value
|
|
7421
|
+
* early, while text it leaves unescaped (in `<style>`, comments) would be
|
|
7422
|
+
* decoded wrongly.
|
|
7423
|
+
*/
|
|
7424
|
+
function passageMarkup(parent) {
|
|
7425
|
+
let markup = "";
|
|
7426
|
+
for (const node of Array.from(parent.childNodes)) if (node.nodeType === Node.TEXT_NODE) markup += node.data;
|
|
7427
|
+
else if (node.nodeType === Node.COMMENT_NODE) markup += `<!--${node.data}-->`;
|
|
7428
|
+
else if (node.nodeType === Node.ELEMENT_NODE) markup += elementMarkup(node);
|
|
7429
|
+
return markup;
|
|
7430
|
+
}
|
|
7431
|
+
/** An element of passage data as markup (see passageMarkup). */
|
|
7432
|
+
function elementMarkup(el) {
|
|
7433
|
+
const name = el.localName;
|
|
7434
|
+
let markup = `<${name}`;
|
|
7435
|
+
for (const attr of Array.from(el.attributes)) markup += ` ${attr.name}=${quoteAttribute(attr.name, attr.value)}`;
|
|
7436
|
+
markup += ">";
|
|
7437
|
+
if (el.namespaceURI === HTML_NAMESPACE && VOID_ELEMENTS.has(name)) return markup;
|
|
7438
|
+
const content = name === "template" ? el.content : el;
|
|
7439
|
+
return `${markup}${passageMarkup(content)}</${name}>`;
|
|
7440
|
+
}
|
|
7441
|
+
/**
|
|
7442
|
+
* An attribute value quoted so that the passage tokenizer reads it back:
|
|
7443
|
+
* in double quotes, else single ones, else with the double quotes in its
|
|
7444
|
+
* text written as `"`, which rendering decodes. Quotes inside markup in
|
|
7445
|
+
* the value (`{print "a"}`) are code, and don't end it.
|
|
7446
|
+
*/
|
|
7447
|
+
function quoteAttribute(name, value) {
|
|
7448
|
+
const code = isCodeAttribute(name);
|
|
7449
|
+
const probe = code ? "on" : "a";
|
|
7450
|
+
const readsBack = (quoted) => {
|
|
7451
|
+
const tokens = tokenize(`<i ${probe}=${quoted}>`);
|
|
7452
|
+
const tag = tokens[0];
|
|
7453
|
+
return tokens.length === 1 && tag?.type === "html" && tag.attributes[probe] === quoted.slice(1, -1);
|
|
7454
|
+
};
|
|
7455
|
+
return [`"${value}"`, `'${value}'`].find(readsBack) ?? `"${escapeTextQuotes(value, code)}"`;
|
|
7456
|
+
}
|
|
7281
7457
|
/**
|
|
7282
|
-
*
|
|
7283
|
-
*
|
|
7458
|
+
* `value` with the double quotes in its text written as `"`, but not
|
|
7459
|
+
* those in markup (`{print "a"}`) or, in a code attribute, in sigil
|
|
7460
|
+
* references (see splitSigilTemplate), which are code.
|
|
7284
7461
|
*/
|
|
7285
|
-
function
|
|
7286
|
-
|
|
7462
|
+
function escapeTextQuotes(value, code) {
|
|
7463
|
+
const escape = (text) => text.replace(/"/g, """);
|
|
7464
|
+
if (code) return splitSigilTemplate(value).map((part) => "text" in part ? escape(part.text) : "expr" in part ? `{${part.expr}}` : part.verbatim).join("");
|
|
7465
|
+
return tokenize(value, { text: true }).map((token) => {
|
|
7466
|
+
const source = value.slice(token.start, token.end);
|
|
7467
|
+
return token.type === "text" ? escape(source) : source;
|
|
7468
|
+
}).join("");
|
|
7287
7469
|
}
|
|
7288
7470
|
/**
|
|
7289
7471
|
* Parse <tw-storydata> and all <tw-passagedata> elements from the DOM.
|
|
7290
7472
|
*
|
|
7291
|
-
*
|
|
7292
|
-
* tags inside passage markup (e.g. <div> inside {button}) are
|
|
7293
|
-
*
|
|
7473
|
+
* Passage content is read with passageMarkup instead of textContent, so
|
|
7474
|
+
* that HTML tags inside passage markup (e.g. <div> inside {button}) are
|
|
7475
|
+
* preserved whether or not the compiler HTML-encoded them.
|
|
7294
7476
|
*/
|
|
7295
7477
|
function parseStoryData() {
|
|
7296
7478
|
const storyEl = document.querySelector("tw-storydata");
|
|
@@ -7308,7 +7490,7 @@ function parseStoryData() {
|
|
|
7308
7490
|
const pid = parseInt(el.getAttribute("pid") || "0", 10);
|
|
7309
7491
|
const passageName = el.getAttribute("name") || "";
|
|
7310
7492
|
const tags = (el.getAttribute("tags") || "").split(/\s+/).filter((t) => t.length > 0);
|
|
7311
|
-
const content =
|
|
7493
|
+
const content = passageMarkup(el);
|
|
7312
7494
|
const metadata = {};
|
|
7313
7495
|
const skipAttrs = new Set([
|
|
7314
7496
|
"pid",
|
|
@@ -7428,6 +7610,14 @@ var settings = {
|
|
|
7428
7610
|
};
|
|
7429
7611
|
//#endregion
|
|
7430
7612
|
//#region src/action-registry.ts
|
|
7613
|
+
/** The fields of a StoryAction that are left out when unset. */
|
|
7614
|
+
var OPTIONAL_ACTION_FIELDS = [
|
|
7615
|
+
"target",
|
|
7616
|
+
"variable",
|
|
7617
|
+
"options",
|
|
7618
|
+
"value",
|
|
7619
|
+
"disabled"
|
|
7620
|
+
];
|
|
7431
7621
|
/**
|
|
7432
7622
|
* The registrations holding each ID, oldest first; the newest is the one
|
|
7433
7623
|
* listed. Generated IDs don't collide, but author IDs can, and one control's
|
|
@@ -7510,11 +7700,7 @@ function useAction(opts) {
|
|
|
7510
7700
|
label: opts.label,
|
|
7511
7701
|
perform: (...args) => performRef.current(...args)
|
|
7512
7702
|
};
|
|
7513
|
-
if (opts
|
|
7514
|
-
if (opts.variable !== void 0) action.variable = opts.variable;
|
|
7515
|
-
if (opts.options !== void 0) action.options = opts.options;
|
|
7516
|
-
if (opts.value !== void 0) action.value = opts.value;
|
|
7517
|
-
if (opts.disabled !== void 0) action.disabled = opts.disabled;
|
|
7703
|
+
for (const field of OPTIONAL_ACTION_FIELDS) if (opts[field] !== void 0) Object.assign(action, { [field]: opts[field] });
|
|
7518
7704
|
return action;
|
|
7519
7705
|
};
|
|
7520
7706
|
const registration = useRef(null);
|
|
@@ -7729,6 +7915,12 @@ function readGroup(params, src, args) {
|
|
|
7729
7915
|
const rest = positional[restIndex];
|
|
7730
7916
|
args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
|
|
7731
7917
|
}
|
|
7918
|
+
/** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
|
|
7919
|
+
var DELAY_PARAMETER = {
|
|
7920
|
+
name: "delay",
|
|
7921
|
+
type: "delay",
|
|
7922
|
+
required: true
|
|
7923
|
+
};
|
|
7732
7924
|
/** Read `rawArgs` into the declared `parameters` (see above). */
|
|
7733
7925
|
function parseMacroArgs(rawArgs, parameters) {
|
|
7734
7926
|
const args = {};
|
|
@@ -7782,15 +7974,19 @@ function parseVarArgs(rawArgs) {
|
|
|
7782
7974
|
placeholder
|
|
7783
7975
|
};
|
|
7784
7976
|
}
|
|
7977
|
+
/** The parameters of an `{option}` sub-macro: its value. */
|
|
7978
|
+
var OPTION_PARAMETERS = [{
|
|
7979
|
+
name: "value",
|
|
7980
|
+
type: "string"
|
|
7981
|
+
}];
|
|
7785
7982
|
/**
|
|
7786
|
-
* Walk AST children to find {option} macro nodes, returning their
|
|
7983
|
+
* Walk AST children to find {option} macro nodes, returning their values.
|
|
7787
7984
|
*/
|
|
7788
7985
|
function extractOptions(children) {
|
|
7789
7986
|
const options = [];
|
|
7790
7987
|
for (const node of children) if (node.type === "macro" && node.name === "option") {
|
|
7791
|
-
const
|
|
7792
|
-
|
|
7793
|
-
options.push(stripped);
|
|
7988
|
+
const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
|
|
7989
|
+
options.push(value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, "$2"));
|
|
7794
7990
|
}
|
|
7795
7991
|
return options;
|
|
7796
7992
|
}
|
|
@@ -7844,13 +8040,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
7844
8040
|
mutate: (code) => executeMutation(code, getValues(), update),
|
|
7845
8041
|
update,
|
|
7846
8042
|
getValues,
|
|
7847
|
-
wrap: (content) =>
|
|
7848
|
-
if (className || id) return h("span", {
|
|
7849
|
-
id,
|
|
7850
|
-
class: className
|
|
7851
|
-
}, content);
|
|
7852
|
-
return h(Fragment, null, content);
|
|
7853
|
-
}
|
|
8043
|
+
wrap: (content) => wrapContent(className, id, content)
|
|
7854
8044
|
};
|
|
7855
8045
|
if (config.merged) {
|
|
7856
8046
|
ctx.merged = useMergedLocals();
|
|
@@ -7981,9 +8171,11 @@ function setOne(draft, name, value) {
|
|
|
7981
8171
|
if (key.includes(".")) setByPath(namespace, key.split("."), value);
|
|
7982
8172
|
else namespace[key] = value;
|
|
7983
8173
|
}
|
|
8174
|
+
/** A method calling the store action `name`, as the store holds it then. */
|
|
8175
|
+
function storeAction(name) {
|
|
8176
|
+
return ((...args) => useStoryStore.getState()[name](...args));
|
|
8177
|
+
}
|
|
7984
8178
|
function createStoryAPI() {
|
|
7985
|
-
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
7986
|
-
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
7987
8179
|
return {
|
|
7988
8180
|
get(name) {
|
|
7989
8181
|
const { isTransient, key } = parseName(name);
|
|
@@ -8003,52 +8195,19 @@ function createStoryAPI() {
|
|
|
8003
8195
|
for (const [k, v] of entries) setOne(draft, k, v);
|
|
8004
8196
|
});
|
|
8005
8197
|
},
|
|
8006
|
-
goto(
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
return useStoryStore.getState().save(slot, custom);
|
|
8020
|
-
},
|
|
8021
|
-
load(slot) {
|
|
8022
|
-
return useStoryStore.getState().load(slot);
|
|
8023
|
-
},
|
|
8024
|
-
hasSave(slot) {
|
|
8025
|
-
return useStoryStore.getState().hasSave(slot);
|
|
8026
|
-
},
|
|
8027
|
-
getSaveInfo(slot) {
|
|
8028
|
-
return useStoryStore.getState().getSaveInfo(slot);
|
|
8029
|
-
},
|
|
8030
|
-
listSaves() {
|
|
8031
|
-
return useStoryStore.getState().listSaves();
|
|
8032
|
-
},
|
|
8033
|
-
deleteSave(slot) {
|
|
8034
|
-
return useStoryStore.getState().deleteSave(slot);
|
|
8035
|
-
},
|
|
8036
|
-
exportSave(slot) {
|
|
8037
|
-
return useStoryStore.getState().exportSave(slot);
|
|
8038
|
-
},
|
|
8039
|
-
importSave(data, slot) {
|
|
8040
|
-
return useStoryStore.getState().importSave(data, slot);
|
|
8041
|
-
},
|
|
8042
|
-
visited: visits.count,
|
|
8043
|
-
hasVisited: visits.has,
|
|
8044
|
-
hasVisitedAny: visits.any,
|
|
8045
|
-
hasVisitedAll: visits.all,
|
|
8046
|
-
rendered: renders.count,
|
|
8047
|
-
hasRendered: renders.has,
|
|
8048
|
-
hasRenderedAny: renders.any,
|
|
8049
|
-
hasRenderedAll: renders.all,
|
|
8050
|
-
currentPassage,
|
|
8051
|
-
previousPassage,
|
|
8198
|
+
goto: storeAction("navigate"),
|
|
8199
|
+
back: storeAction("goBack"),
|
|
8200
|
+
forward: storeAction("goForward"),
|
|
8201
|
+
restart: storeAction("restart"),
|
|
8202
|
+
save: storeAction("save"),
|
|
8203
|
+
load: storeAction("load"),
|
|
8204
|
+
hasSave: storeAction("hasSave"),
|
|
8205
|
+
getSaveInfo: storeAction("getSaveInfo"),
|
|
8206
|
+
listSaves: storeAction("listSaves"),
|
|
8207
|
+
deleteSave: storeAction("deleteSave"),
|
|
8208
|
+
exportSave: storeAction("exportSave"),
|
|
8209
|
+
importSave: storeAction("importSave"),
|
|
8210
|
+
...historyQueries(() => useStoryStore.getState().visitCounts, () => useStoryStore.getState().renderCounts),
|
|
8052
8211
|
get title() {
|
|
8053
8212
|
return useStoryStore.getState().storyData?.name || "";
|
|
8054
8213
|
},
|
|
@@ -8094,15 +8253,9 @@ function createStoryAPI() {
|
|
|
8094
8253
|
estimateSupported: false
|
|
8095
8254
|
};
|
|
8096
8255
|
},
|
|
8097
|
-
clearGameData()
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
clearAllData() {
|
|
8101
|
-
return useStoryStore.getState().clearAllData();
|
|
8102
|
-
},
|
|
8103
|
-
deletePlaythrough(playthroughId) {
|
|
8104
|
-
return useStoryStore.getState().deletePlaythrough(playthroughId);
|
|
8105
|
-
},
|
|
8256
|
+
clearGameData: storeAction("clearGameData"),
|
|
8257
|
+
clearAllData: storeAction("clearAllData"),
|
|
8258
|
+
deletePlaythrough: storeAction("deletePlaythrough"),
|
|
8106
8259
|
get backend() {
|
|
8107
8260
|
return getBackendType();
|
|
8108
8261
|
}
|
|
@@ -8510,26 +8663,51 @@ function astContainsChildren(nodes) {
|
|
|
8510
8663
|
}
|
|
8511
8664
|
//#endregion
|
|
8512
8665
|
//#region src/components/macros/Widget.tsx
|
|
8513
|
-
/**
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8666
|
+
/** A {widget} definition's name, then its `@` parameters. */
|
|
8667
|
+
var WIDGET_PARAMETERS = [{
|
|
8668
|
+
name: "name",
|
|
8669
|
+
type: "text",
|
|
8670
|
+
required: true
|
|
8671
|
+
}, {
|
|
8672
|
+
name: "parameters",
|
|
8673
|
+
type: "expression"
|
|
8674
|
+
}];
|
|
8675
|
+
/**
|
|
8676
|
+
* The widget a {widget} definition's arguments declare: its name and its
|
|
8677
|
+
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
8678
|
+
* Other words, such as `$name`, are not parameters.
|
|
8679
|
+
*/
|
|
8680
|
+
function widgetDef({ name = "", parameters = "" }) {
|
|
8520
8681
|
return {
|
|
8521
|
-
name
|
|
8522
|
-
params:
|
|
8682
|
+
name,
|
|
8683
|
+
params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
|
|
8523
8684
|
};
|
|
8524
8685
|
}
|
|
8686
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
8687
|
+
function parseWidgetDef(rawArgs) {
|
|
8688
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
8689
|
+
}
|
|
8690
|
+
/**
|
|
8691
|
+
* Register the widget a definition declares, with its body. Widgets whose
|
|
8692
|
+
* body renders {@children} take a closing tag, so they are registered as
|
|
8693
|
+
* block macros too: passages parsed later nest their content. A parameter
|
|
8694
|
+
* that no namespace can hold throws (see registerWidget), registering
|
|
8695
|
+
* nothing.
|
|
8696
|
+
*/
|
|
8697
|
+
function registerWidgetDef({ name, params }, body) {
|
|
8698
|
+
const isBlock = astContainsChildren(body);
|
|
8699
|
+
registerWidget(name, body, params, isBlock);
|
|
8700
|
+
if (isBlock) registerBlockMacro(name);
|
|
8701
|
+
}
|
|
8525
8702
|
defineMacro({
|
|
8526
8703
|
name: "widget",
|
|
8527
8704
|
block: true,
|
|
8528
|
-
|
|
8705
|
+
parameters: WIDGET_PARAMETERS,
|
|
8706
|
+
render({ children = [] }, ctx) {
|
|
8529
8707
|
let parsed;
|
|
8530
8708
|
let error;
|
|
8531
8709
|
try {
|
|
8532
|
-
const def =
|
|
8710
|
+
const def = widgetDef(ctx.args);
|
|
8533
8711
|
for (const param of def.params) checkVariableName(param.slice(1), param);
|
|
8534
8712
|
parsed = def;
|
|
8535
8713
|
} catch (err) {
|
|
@@ -8542,10 +8720,7 @@ defineMacro({
|
|
|
8542
8720
|
const childrenKey = JSON.stringify(children);
|
|
8543
8721
|
const paramsKey = params.join(",");
|
|
8544
8722
|
ctx.hooks.useLayoutEffect(() => {
|
|
8545
|
-
if (
|
|
8546
|
-
const isBlock = astContainsChildren(children);
|
|
8547
|
-
registerWidget(name, children, params, isBlock);
|
|
8548
|
-
if (isBlock) registerBlockMacro(name);
|
|
8723
|
+
if (parsed) registerWidgetDef(parsed, children);
|
|
8549
8724
|
}, [
|
|
8550
8725
|
name,
|
|
8551
8726
|
childrenKey,
|
|
@@ -8584,9 +8759,7 @@ defineMacro({
|
|
|
8584
8759
|
merged: true,
|
|
8585
8760
|
render({ rawArgs }, ctx) {
|
|
8586
8761
|
try {
|
|
8587
|
-
|
|
8588
|
-
const display = result == null ? "" : String(result);
|
|
8589
|
-
return ctx.wrap(display);
|
|
8762
|
+
return ctx.wrap(display(ctx.evaluate(rawArgs)));
|
|
8590
8763
|
} catch (err) {
|
|
8591
8764
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
8592
8765
|
macro: "print",
|
|
@@ -8595,8 +8768,7 @@ defineMacro({
|
|
|
8595
8768
|
}
|
|
8596
8769
|
},
|
|
8597
8770
|
text({ rawArgs }, ctx) {
|
|
8598
|
-
|
|
8599
|
-
return result == null ? "" : String(result);
|
|
8771
|
+
return display(ctx.evaluate(rawArgs));
|
|
8600
8772
|
}
|
|
8601
8773
|
});
|
|
8602
8774
|
//#endregion
|
|
@@ -8766,40 +8938,50 @@ defineMacro({
|
|
|
8766
8938
|
}
|
|
8767
8939
|
});
|
|
8768
8940
|
//#endregion
|
|
8941
|
+
//#region src/components/macros/branches.ts
|
|
8942
|
+
/**
|
|
8943
|
+
* The branch a conditional block ({if}, {switch}) shows: the first of
|
|
8944
|
+
* `branches` whose argument `matches`, else the last bare one ({else},
|
|
8945
|
+
* {default}), else null. Throws what `matches` throws.
|
|
8946
|
+
*/
|
|
8947
|
+
function selectBranch(branches, matches) {
|
|
8948
|
+
let fallback = null;
|
|
8949
|
+
for (const branch of branches) if (branch.rawArgs === "") fallback = branch;
|
|
8950
|
+
else if (matches(branch.rawArgs)) return branch;
|
|
8951
|
+
return fallback;
|
|
8952
|
+
}
|
|
8953
|
+
//#endregion
|
|
8769
8954
|
//#region src/components/macros/If.tsx
|
|
8955
|
+
/**
|
|
8956
|
+
* The {if}/{elseif}/{else} branch whose condition holds, else the {else}
|
|
8957
|
+
* branch, else null: the chain ends at its first bare branch. Throws what a
|
|
8958
|
+
* condition throws.
|
|
8959
|
+
*/
|
|
8960
|
+
function selectIfBranch(branches, evaluate) {
|
|
8961
|
+
const end = branches.findIndex((branch) => branch.rawArgs === "");
|
|
8962
|
+
return selectBranch(end < 0 ? branches : branches.slice(0, end + 1), (expr) => Boolean(evaluate(expr)));
|
|
8963
|
+
}
|
|
8770
8964
|
defineMacro({
|
|
8771
8965
|
name: "if",
|
|
8772
8966
|
block: true,
|
|
8773
8967
|
interpolate: true,
|
|
8774
8968
|
merged: true,
|
|
8775
8969
|
render({ branches = [] }, ctx) {
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
children
|
|
8970
|
+
let branch;
|
|
8971
|
+
try {
|
|
8972
|
+
branch = selectIfBranch(branches, ctx.evaluate);
|
|
8973
|
+
} catch (err) {
|
|
8974
|
+
return /* @__PURE__ */ jsx(MacroError, {
|
|
8975
|
+
macro: "if",
|
|
8976
|
+
error: err
|
|
8784
8977
|
});
|
|
8785
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
8786
|
-
}
|
|
8787
|
-
for (const branch of branches) {
|
|
8788
|
-
if (branch.rawArgs === "") return renderBranch(branch);
|
|
8789
|
-
try {
|
|
8790
|
-
if (ctx.evaluate(branch.rawArgs)) return renderBranch(branch);
|
|
8791
|
-
} catch (err) {
|
|
8792
|
-
return /* @__PURE__ */ jsx(MacroError, {
|
|
8793
|
-
macro: "if",
|
|
8794
|
-
error: err
|
|
8795
|
-
});
|
|
8796
|
-
}
|
|
8797
8978
|
}
|
|
8798
|
-
return null;
|
|
8979
|
+
if (!branch) return null;
|
|
8980
|
+
return wrapContent(ctx.resolve(branch.className), ctx.resolve(branch.id), ctx.renderNodes(branch.children));
|
|
8799
8981
|
},
|
|
8800
8982
|
text({ branches = [] }, ctx) {
|
|
8801
|
-
|
|
8802
|
-
return "";
|
|
8983
|
+
const branch = selectIfBranch(branches, ctx.evaluate);
|
|
8984
|
+
return branch ? ctx.renderText(branch.children) : "";
|
|
8803
8985
|
}
|
|
8804
8986
|
});
|
|
8805
8987
|
//#endregion
|
|
@@ -8995,11 +9177,7 @@ defineMacro({
|
|
|
8995
9177
|
name: "repeat",
|
|
8996
9178
|
block: true,
|
|
8997
9179
|
interpolate: true,
|
|
8998
|
-
parameters: [
|
|
8999
|
-
name: "delay",
|
|
9000
|
-
type: "delay",
|
|
9001
|
-
required: true
|
|
9002
|
-
}],
|
|
9180
|
+
parameters: [DELAY_PARAMETER],
|
|
9003
9181
|
render({ children = [] }, ctx) {
|
|
9004
9182
|
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
9005
9183
|
const delay = ctx.args.delay ?? 0;
|
|
@@ -9166,14 +9344,8 @@ defineMacro({
|
|
|
9166
9344
|
* The first {case} branch whose value equals `value`, else the {default}
|
|
9167
9345
|
* branch, else null. Throws what a case expression throws.
|
|
9168
9346
|
*/
|
|
9169
|
-
function
|
|
9170
|
-
|
|
9171
|
-
for (let i = 1; i < branches.length; i++) {
|
|
9172
|
-
const branch = branches[i];
|
|
9173
|
-
if (branch.rawArgs === "") defaultBranch = branch;
|
|
9174
|
-
else if (value === evaluate(branch.rawArgs)) return branch;
|
|
9175
|
-
}
|
|
9176
|
-
return defaultBranch;
|
|
9347
|
+
function selectCase(value, branches, evaluate) {
|
|
9348
|
+
return selectBranch(branches.slice(1), (expr) => value === evaluate(expr));
|
|
9177
9349
|
}
|
|
9178
9350
|
defineMacro({
|
|
9179
9351
|
name: "switch",
|
|
@@ -9196,7 +9368,7 @@ defineMacro({
|
|
|
9196
9368
|
}
|
|
9197
9369
|
let branch;
|
|
9198
9370
|
try {
|
|
9199
|
-
branch =
|
|
9371
|
+
branch = selectCase(switchValue, branches, ctx.evaluate);
|
|
9200
9372
|
} catch (err) {
|
|
9201
9373
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
9202
9374
|
macro: "case",
|
|
@@ -9206,21 +9378,24 @@ defineMacro({
|
|
|
9206
9378
|
return branch && /* @__PURE__ */ jsx(Fragment$1, { children: ctx.renderNodes(branch.children) });
|
|
9207
9379
|
},
|
|
9208
9380
|
text({ rawArgs, branches = [] }, ctx) {
|
|
9209
|
-
const branch =
|
|
9381
|
+
const branch = selectCase(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
9210
9382
|
return branch ? ctx.renderText(branch.children) : "";
|
|
9211
9383
|
}
|
|
9212
9384
|
});
|
|
9213
9385
|
//#endregion
|
|
9214
9386
|
//#region src/components/macros/Timed.tsx
|
|
9387
|
+
/** The parameters of {timed} and of each {next}: the delay before it shows. */
|
|
9388
|
+
var TIMED_PARAMETERS = [DELAY_PARAMETER];
|
|
9215
9389
|
defineMacro({
|
|
9216
9390
|
name: "timed",
|
|
9217
9391
|
subMacros: ["next"],
|
|
9218
9392
|
interpolate: true,
|
|
9393
|
+
parameters: TIMED_PARAMETERS,
|
|
9219
9394
|
render({ branches = [] }, ctx) {
|
|
9220
9395
|
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
9221
9396
|
const sections = useMemo(() => {
|
|
9222
9397
|
return branches.map((branch) => ({
|
|
9223
|
-
delay: branch.rawArgs
|
|
9398
|
+
delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
|
|
9224
9399
|
nodes: branch.children,
|
|
9225
9400
|
className: branch.className,
|
|
9226
9401
|
id: branch.id
|
|
@@ -9238,15 +9413,7 @@ defineMacro({
|
|
|
9238
9413
|
}, [visibleIndex, sections]);
|
|
9239
9414
|
if (visibleIndex < 0) return ctx.wrap(null);
|
|
9240
9415
|
const section = sections[visibleIndex];
|
|
9241
|
-
|
|
9242
|
-
const sectionClass = ctx.resolve(section.className);
|
|
9243
|
-
const sectionId = ctx.resolve(section.id);
|
|
9244
|
-
const inner = sectionClass || sectionId ? /* @__PURE__ */ jsx("span", {
|
|
9245
|
-
id: sectionId,
|
|
9246
|
-
class: sectionClass,
|
|
9247
|
-
children: content
|
|
9248
|
-
}) : content;
|
|
9249
|
-
return ctx.wrap(inner);
|
|
9416
|
+
return ctx.wrap(wrapContent(ctx.resolve(section.className), ctx.resolve(section.id), ctx.renderNodes(section.nodes)));
|
|
9250
9417
|
}
|
|
9251
9418
|
});
|
|
9252
9419
|
//#endregion
|
|
@@ -9267,11 +9434,7 @@ defineMacro({
|
|
|
9267
9434
|
name: "type",
|
|
9268
9435
|
block: true,
|
|
9269
9436
|
interpolate: true,
|
|
9270
|
-
parameters: [
|
|
9271
|
-
name: "delay",
|
|
9272
|
-
type: "delay",
|
|
9273
|
-
required: true
|
|
9274
|
-
}],
|
|
9437
|
+
parameters: [DELAY_PARAMETER],
|
|
9275
9438
|
render({ children = [] }, ctx) {
|
|
9276
9439
|
const { useState, useEffect, useRef } = ctx.hooks;
|
|
9277
9440
|
const speed = ctx.args.delay ?? 0;
|
|
@@ -9886,7 +10049,7 @@ defineMacro({
|
|
|
9886
10049
|
required: true
|
|
9887
10050
|
}],
|
|
9888
10051
|
render(_props, ctx) {
|
|
9889
|
-
const storyData =
|
|
10052
|
+
const { storyData } = useStoryFields("storyData");
|
|
9890
10053
|
const { inline } = ctx.args;
|
|
9891
10054
|
const passageName = evaluatePassageName(ctx.args.passage, ctx.evaluate);
|
|
9892
10055
|
const passage = storyData?.passages.get(passageName);
|
|
@@ -9953,6 +10116,26 @@ defineMacro({
|
|
|
9953
10116
|
});
|
|
9954
10117
|
//#endregion
|
|
9955
10118
|
//#region src/components/macros/input-macro.ts
|
|
10119
|
+
/** The story variable a `storeVar` macro binds: its first parameter. */
|
|
10120
|
+
var VARIABLE_PARAMETER = {
|
|
10121
|
+
name: "variable",
|
|
10122
|
+
type: "variable",
|
|
10123
|
+
required: true
|
|
10124
|
+
};
|
|
10125
|
+
/**
|
|
10126
|
+
* Register the automation action of a macro bound to a story variable: keyed
|
|
10127
|
+
* by the variable (or `key`), with the variable's value unless `action`
|
|
10128
|
+
* gives another.
|
|
10129
|
+
*/
|
|
10130
|
+
function useVariableAction(ctx, action) {
|
|
10131
|
+
return ctx.useAction({
|
|
10132
|
+
key: `$${ctx.varName}`,
|
|
10133
|
+
authorId: ctx.id,
|
|
10134
|
+
variable: ctx.varName,
|
|
10135
|
+
value: ctx.value,
|
|
10136
|
+
...action
|
|
10137
|
+
});
|
|
10138
|
+
}
|
|
9956
10139
|
/**
|
|
9957
10140
|
* Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
|
|
9958
10141
|
* `type` (or, without one, a `<textarea>`) bound to the story variable.
|
|
@@ -9964,30 +10147,22 @@ function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? St
|
|
|
9964
10147
|
defineMacro({
|
|
9965
10148
|
name,
|
|
9966
10149
|
storeVar: true,
|
|
9967
|
-
parameters: [{
|
|
9968
|
-
name: "variable",
|
|
9969
|
-
type: "variable",
|
|
9970
|
-
required: true
|
|
9971
|
-
}, {
|
|
10150
|
+
parameters: [VARIABLE_PARAMETER, {
|
|
9972
10151
|
name: "placeholder",
|
|
9973
10152
|
type: "string"
|
|
9974
10153
|
}],
|
|
9975
10154
|
render(_props, ctx) {
|
|
9976
10155
|
const placeholder = ctx.args.placeholder ?? "";
|
|
9977
|
-
ctx
|
|
10156
|
+
useVariableAction(ctx, {
|
|
9978
10157
|
type: name,
|
|
9979
|
-
key: `$${ctx.varName}`,
|
|
9980
|
-
authorId: ctx.id,
|
|
9981
10158
|
label: placeholder || ctx.varName,
|
|
9982
|
-
variable: ctx.varName,
|
|
9983
|
-
value: ctx.value,
|
|
9984
10159
|
perform: (v) => ctx.setValue(toValue(v))
|
|
9985
10160
|
});
|
|
9986
10161
|
return h(type ? "input" : "textarea", {
|
|
9987
10162
|
type,
|
|
9988
10163
|
id: ctx.id,
|
|
9989
10164
|
class: ctx.cls,
|
|
9990
|
-
value:
|
|
10165
|
+
value: display(ctx.value),
|
|
9991
10166
|
placeholder,
|
|
9992
10167
|
onInput: (e) => ctx.setValue(toValue(e.target.value))
|
|
9993
10168
|
});
|
|
@@ -10008,22 +10183,15 @@ defineInputMacro("textarea");
|
|
|
10008
10183
|
defineMacro({
|
|
10009
10184
|
name: "checkbox",
|
|
10010
10185
|
storeVar: true,
|
|
10011
|
-
parameters: [{
|
|
10012
|
-
name: "variable",
|
|
10013
|
-
type: "variable",
|
|
10014
|
-
required: true
|
|
10015
|
-
}, {
|
|
10186
|
+
parameters: [VARIABLE_PARAMETER, {
|
|
10016
10187
|
name: "label",
|
|
10017
10188
|
type: "text"
|
|
10018
10189
|
}],
|
|
10019
10190
|
render(_props, ctx) {
|
|
10020
10191
|
const label = ctx.args.label ?? "";
|
|
10021
|
-
ctx
|
|
10192
|
+
useVariableAction(ctx, {
|
|
10022
10193
|
type: "checkbox",
|
|
10023
|
-
key: `$${ctx.varName}`,
|
|
10024
|
-
authorId: ctx.id,
|
|
10025
10194
|
label: label || ctx.varName || "",
|
|
10026
|
-
variable: ctx.varName,
|
|
10027
10195
|
value: !!ctx.value,
|
|
10028
10196
|
perform: (v) => ctx.setValue(v !== void 0 ? !!v : !ctx.value)
|
|
10029
10197
|
});
|
|
@@ -10044,11 +10212,7 @@ defineMacro({
|
|
|
10044
10212
|
name: "radiobutton",
|
|
10045
10213
|
storeVar: true,
|
|
10046
10214
|
parameters: [
|
|
10047
|
-
|
|
10048
|
-
name: "variable",
|
|
10049
|
-
type: "variable",
|
|
10050
|
-
required: true
|
|
10051
|
-
},
|
|
10215
|
+
VARIABLE_PARAMETER,
|
|
10052
10216
|
{
|
|
10053
10217
|
name: "value",
|
|
10054
10218
|
type: "text",
|
|
@@ -10062,13 +10226,10 @@ defineMacro({
|
|
|
10062
10226
|
render(_props, ctx) {
|
|
10063
10227
|
const radioValue = ctx.args.value ?? "";
|
|
10064
10228
|
const label = ctx.args.label ?? "";
|
|
10065
|
-
ctx
|
|
10229
|
+
useVariableAction(ctx, {
|
|
10066
10230
|
type: "radiobutton",
|
|
10067
10231
|
key: `$${ctx.varName}:${radioValue}`,
|
|
10068
|
-
authorId: ctx.id,
|
|
10069
10232
|
label: label || radioValue,
|
|
10070
|
-
variable: ctx.varName,
|
|
10071
|
-
value: ctx.value,
|
|
10072
10233
|
perform: () => ctx.setValue(radioValue)
|
|
10073
10234
|
});
|
|
10074
10235
|
return /* @__PURE__ */ jsxs("label", {
|
|
@@ -10089,21 +10250,13 @@ defineMacro({
|
|
|
10089
10250
|
name: "listbox",
|
|
10090
10251
|
subMacros: ["option"],
|
|
10091
10252
|
storeVar: true,
|
|
10092
|
-
parameters: [
|
|
10093
|
-
name: "variable",
|
|
10094
|
-
type: "variable",
|
|
10095
|
-
required: true
|
|
10096
|
-
}],
|
|
10253
|
+
parameters: [VARIABLE_PARAMETER],
|
|
10097
10254
|
render({ children = [] }, ctx) {
|
|
10098
10255
|
const options = ctx.extractOptions(children);
|
|
10099
|
-
ctx
|
|
10256
|
+
useVariableAction(ctx, {
|
|
10100
10257
|
type: "listbox",
|
|
10101
|
-
key: `$${ctx.varName}`,
|
|
10102
|
-
authorId: ctx.id,
|
|
10103
10258
|
label: ctx.varName,
|
|
10104
|
-
variable: ctx.varName,
|
|
10105
10259
|
options,
|
|
10106
|
-
value: ctx.value,
|
|
10107
10260
|
perform: (v) => {
|
|
10108
10261
|
if (v !== void 0) ctx.setValue(String(v));
|
|
10109
10262
|
}
|
|
@@ -10111,7 +10264,7 @@ defineMacro({
|
|
|
10111
10264
|
return /* @__PURE__ */ jsx("select", {
|
|
10112
10265
|
id: ctx.id,
|
|
10113
10266
|
class: ctx.cls,
|
|
10114
|
-
value:
|
|
10267
|
+
value: display(ctx.value),
|
|
10115
10268
|
onChange: (e) => ctx.setValue(e.target.value),
|
|
10116
10269
|
children: options.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
10117
10270
|
value: opt,
|
|
@@ -10126,11 +10279,7 @@ defineMacro({
|
|
|
10126
10279
|
name: "cycle",
|
|
10127
10280
|
block: true,
|
|
10128
10281
|
storeVar: true,
|
|
10129
|
-
parameters: [
|
|
10130
|
-
name: "variable",
|
|
10131
|
-
type: "variable",
|
|
10132
|
-
required: true
|
|
10133
|
-
}],
|
|
10282
|
+
parameters: [VARIABLE_PARAMETER],
|
|
10134
10283
|
render({ children = [] }, ctx) {
|
|
10135
10284
|
const options = ctx.extractOptions(children);
|
|
10136
10285
|
const handleClick = () => {
|
|
@@ -10139,14 +10288,10 @@ defineMacro({
|
|
|
10139
10288
|
const nextIndex = (options.indexOf(String(current)) + 1) % options.length;
|
|
10140
10289
|
ctx.setValue(options[nextIndex]);
|
|
10141
10290
|
};
|
|
10142
|
-
ctx
|
|
10291
|
+
useVariableAction(ctx, {
|
|
10143
10292
|
type: "cycle",
|
|
10144
|
-
key: `$${ctx.varName}`,
|
|
10145
|
-
authorId: ctx.id,
|
|
10146
10293
|
label: ctx.value == null ? options[0] || "" : String(ctx.value),
|
|
10147
|
-
variable: ctx.varName,
|
|
10148
10294
|
options,
|
|
10149
|
-
value: ctx.value,
|
|
10150
10295
|
perform: (v) => {
|
|
10151
10296
|
if (v !== void 0) ctx.setValue(v);
|
|
10152
10297
|
else handleClick();
|
|
@@ -10192,6 +10337,20 @@ function parsePassage(passage) {
|
|
|
10192
10337
|
function renderPassageContent(passage) {
|
|
10193
10338
|
return renderNodes(parsePassage(passage), passage.tags.includes("nobr") ? { nobr: true } : void 0);
|
|
10194
10339
|
}
|
|
10340
|
+
/**
|
|
10341
|
+
* The content of a special passage shown with every passage (PassageHeader,
|
|
10342
|
+
* PassageFooter, PassageDone), or null. One that fails to render is logged
|
|
10343
|
+
* and left out, so it can't take the passage down with it.
|
|
10344
|
+
*/
|
|
10345
|
+
function renderSpecialPassage(passage) {
|
|
10346
|
+
if (!passage) return null;
|
|
10347
|
+
try {
|
|
10348
|
+
return renderPassageContent(passage);
|
|
10349
|
+
} catch (err) {
|
|
10350
|
+
console.error(`spindle: Error in ${passage.name}:`, err);
|
|
10351
|
+
return null;
|
|
10352
|
+
}
|
|
10353
|
+
}
|
|
10195
10354
|
var CODE_PASSAGES = new Set([
|
|
10196
10355
|
"PassageReady",
|
|
10197
10356
|
"PassageHeader",
|
|
@@ -10199,7 +10358,7 @@ var CODE_PASSAGES = new Set([
|
|
|
10199
10358
|
"PassageDone"
|
|
10200
10359
|
]);
|
|
10201
10360
|
function Passage({ passage, dataTransition, navigationId }) {
|
|
10202
|
-
const storyData =
|
|
10361
|
+
const { storyData } = useStoryFields("storyData");
|
|
10203
10362
|
const isCodePassage = CODE_PASSAGES.has(passage.name);
|
|
10204
10363
|
const [doneReady, setDoneReady] = useState(false);
|
|
10205
10364
|
const content = useMemo(() => {
|
|
@@ -10219,40 +10378,17 @@ function Passage({ passage, dataTransition, navigationId }) {
|
|
|
10219
10378
|
});
|
|
10220
10379
|
}
|
|
10221
10380
|
}, [passage.content, passage.name]);
|
|
10222
|
-
const
|
|
10223
|
-
const
|
|
10224
|
-
const
|
|
10225
|
-
const
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
return renderPassageContent(headerPassage);
|
|
10229
|
-
} catch (err) {
|
|
10230
|
-
console.error("spindle: Error in PassageHeader:", err);
|
|
10231
|
-
return null;
|
|
10232
|
-
}
|
|
10233
|
-
}, [headerPassage?.content]);
|
|
10234
|
-
const footerContent = useMemo(() => {
|
|
10235
|
-
if (!footerPassage) return null;
|
|
10236
|
-
try {
|
|
10237
|
-
return renderPassageContent(footerPassage);
|
|
10238
|
-
} catch (err) {
|
|
10239
|
-
console.error("spindle: Error in PassageFooter:", err);
|
|
10240
|
-
return null;
|
|
10241
|
-
}
|
|
10242
|
-
}, [footerPassage?.content]);
|
|
10381
|
+
const special = (name) => isCodePassage ? void 0 : storyData?.passages.get(name);
|
|
10382
|
+
const headerPassage = special("PassageHeader");
|
|
10383
|
+
const footerPassage = special("PassageFooter");
|
|
10384
|
+
const donePassage = special("PassageDone");
|
|
10385
|
+
const headerContent = useMemo(() => renderSpecialPassage(headerPassage), [headerPassage?.content]);
|
|
10386
|
+
const footerContent = useMemo(() => renderSpecialPassage(footerPassage), [footerPassage?.content]);
|
|
10243
10387
|
useEffect(() => {
|
|
10244
10388
|
if (donePassage) setDoneReady(true);
|
|
10245
10389
|
return () => setDoneReady(false);
|
|
10246
10390
|
}, [passage.name]);
|
|
10247
|
-
const doneContent = useMemo(() =>
|
|
10248
|
-
if (!doneReady || !donePassage) return null;
|
|
10249
|
-
try {
|
|
10250
|
-
return renderPassageContent(donePassage);
|
|
10251
|
-
} catch (err) {
|
|
10252
|
-
console.error("spindle: Error in PassageDone:", err);
|
|
10253
|
-
return null;
|
|
10254
|
-
}
|
|
10255
|
-
}, [doneReady, donePassage?.content]);
|
|
10391
|
+
const doneContent = useMemo(() => doneReady ? renderSpecialPassage(donePassage) : null, [doneReady, donePassage?.content]);
|
|
10256
10392
|
const elRef = useRef(null);
|
|
10257
10393
|
useLayoutEffect(() => {
|
|
10258
10394
|
if (navigationId !== void 0) markPassageRendered(navigationId);
|
|
@@ -10653,16 +10789,12 @@ function boot() {
|
|
|
10653
10789
|
for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
|
|
10654
10790
|
const widgetAST = buildAST(tokenize(passage.content));
|
|
10655
10791
|
for (const node of widgetAST) if (node.type === "macro" && node.name === "widget" && node.rawArgs) {
|
|
10656
|
-
const
|
|
10657
|
-
const children = node.children;
|
|
10658
|
-
const isBlock = astContainsChildren(children);
|
|
10792
|
+
const def = parseWidgetDef(node.rawArgs);
|
|
10659
10793
|
try {
|
|
10660
|
-
|
|
10794
|
+
registerWidgetDef(def, node.children);
|
|
10661
10795
|
} catch (err) {
|
|
10662
|
-
console.error(`spindle: widget "${
|
|
10663
|
-
continue;
|
|
10796
|
+
console.error(`spindle: widget "${def.name}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`);
|
|
10664
10797
|
}
|
|
10665
|
-
if (isBlock) registerBlockMacro(widgetName);
|
|
10666
10798
|
}
|
|
10667
10799
|
}
|
|
10668
10800
|
let prevNavigationId = useStoryStore.getState().navigationId;
|