@rohal12/spindle 0.52.6 → 0.52.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +764 -735
- package/dist/pkg/macro-registry.json +18 -0
- package/dist/pkg/story-variables.js +54 -32
- 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/js-lexer.ts +80 -45
- package/src/markup/ast.ts +4 -3
- package/src/markup/render.tsx +4 -12
- 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,351 +115,6 @@ function createCounts(source, name) {
|
|
|
102
115
|
return counts;
|
|
103
116
|
}
|
|
104
117
|
//#endregion
|
|
105
|
-
//#region src/utils/object-path.ts
|
|
106
|
-
/**
|
|
107
|
-
* Traverse dot-path segments on an object and return the nested value.
|
|
108
|
-
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
109
|
-
* are not story state: unless an object holds one as its own property, it
|
|
110
|
-
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
111
|
-
* (class getters, `size` of a Map) are read.
|
|
112
|
-
*/
|
|
113
|
-
function getByPath(obj, segments) {
|
|
114
|
-
let current = obj;
|
|
115
|
-
for (const seg of segments) {
|
|
116
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
117
|
-
if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
|
|
118
|
-
current = current[seg];
|
|
119
|
-
}
|
|
120
|
-
return current;
|
|
121
|
-
}
|
|
122
|
-
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
123
|
-
var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
124
|
-
/**
|
|
125
|
-
* Built-ins whose content is not their properties: a property written into
|
|
126
|
-
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
127
|
-
*/
|
|
128
|
-
var builtinName = (value) => value instanceof Map ? "a Map" : value instanceof Set ? "a Set" : value instanceof Date ? "a Date" : value instanceof RegExp ? "a RegExp" : void 0;
|
|
129
|
-
/**
|
|
130
|
-
* Shallow copy that keeps the prototype, so a registered class instance
|
|
131
|
-
* stays an instance of its class (with the same own keys deepClone copies).
|
|
132
|
-
*/
|
|
133
|
-
function shallowCopy(value) {
|
|
134
|
-
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
135
|
-
for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
|
|
136
|
-
value: value[key],
|
|
137
|
-
enumerable: true,
|
|
138
|
-
writable: true,
|
|
139
|
-
configurable: true
|
|
140
|
-
});
|
|
141
|
-
return copy;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Set a value at dot-path `segments` below `root`.
|
|
145
|
-
*
|
|
146
|
-
* When `root` is an Immer draft, objects Immer does not draft (registered
|
|
147
|
-
* class instances and everything below them) are copied on the way down and
|
|
148
|
-
* the copy is assigned to its parent, so the write produces new references
|
|
149
|
-
* up to the root. Writing into such an object directly would mutate the
|
|
150
|
-
* instance shared with the previous state and with history, and subscribers
|
|
151
|
-
* would see no change. Outside a draft (a private working copy) the path is
|
|
152
|
-
* written in place.
|
|
153
|
-
*
|
|
154
|
-
* The path goes through own properties of plain objects, class instances
|
|
155
|
-
* and arrays (by index); anything else throws a TypeError rather than
|
|
156
|
-
* writing where no clone or save would see it: an inherited property such
|
|
157
|
-
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
158
|
-
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
159
|
-
*/
|
|
160
|
-
function setByPath(root, segments, value, options = {}) {
|
|
161
|
-
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
162
|
-
parent[segments[segments.length - 1]] = value;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Delete the property at dot-path `segments` below `root`, copying
|
|
166
|
-
* undrafted objects on the way down like setByPath(). Does nothing when an
|
|
167
|
-
* intermediate is missing or not an object, or the property is absent (or
|
|
168
|
-
* only inherited).
|
|
169
|
-
*/
|
|
170
|
-
function deleteByPath(root, segments) {
|
|
171
|
-
const last = segments[segments.length - 1];
|
|
172
|
-
checkSegments(segments);
|
|
173
|
-
let holder = root;
|
|
174
|
-
for (const seg of segments.slice(0, -1)) {
|
|
175
|
-
if (holder == null || typeof holder !== "object" || !hasOwn(holder, seg)) return;
|
|
176
|
-
holder = holder[seg];
|
|
177
|
-
}
|
|
178
|
-
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
179
|
-
const parent = walkToParent(root, segments, false);
|
|
180
|
-
delete parent[last];
|
|
181
|
-
}
|
|
182
|
-
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
183
|
-
function checkSegments(segments) {
|
|
184
|
-
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
185
|
-
}
|
|
186
|
-
/** Throw unless `holder` can take `key` as story state (see setByPath). */
|
|
187
|
-
function checkHolder(holder, key, segments, depth) {
|
|
188
|
-
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
189
|
-
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Walk to the object holding the last of `segments`, copying undrafted
|
|
193
|
-
* objects when `root` is an Immer draft (see setByPath). A missing or
|
|
194
|
-
* non-object intermediate is replaced by a plain object when
|
|
195
|
-
* `createMissing` is set, and throws a TypeError otherwise.
|
|
196
|
-
*/
|
|
197
|
-
function walkToParent(root, segments, createMissing) {
|
|
198
|
-
checkSegments(segments);
|
|
199
|
-
const copyOnWrite = isDraft(root);
|
|
200
|
-
let current = root;
|
|
201
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
202
|
-
const seg = segments[i];
|
|
203
|
-
checkHolder(current, seg, segments, i);
|
|
204
|
-
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
205
|
-
if (next == null || typeof next !== "object") {
|
|
206
|
-
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
207
|
-
next = {};
|
|
208
|
-
current[seg] = next;
|
|
209
|
-
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
210
|
-
next = shallowCopy(next);
|
|
211
|
-
current[seg] = next;
|
|
212
|
-
}
|
|
213
|
-
current = next;
|
|
214
|
-
}
|
|
215
|
-
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
216
|
-
return current;
|
|
217
|
-
}
|
|
218
|
-
//#endregion
|
|
219
|
-
//#region src/structural.ts
|
|
220
|
-
/**
|
|
221
|
-
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
222
|
-
* property (as JSON.parse does) instead of replacing the prototype.
|
|
223
|
-
*/
|
|
224
|
-
function setOwn(target, key, value) {
|
|
225
|
-
if (key === "__proto__") Object.defineProperty(target, key, {
|
|
226
|
-
value,
|
|
227
|
-
enumerable: true,
|
|
228
|
-
writable: true,
|
|
229
|
-
configurable: true
|
|
230
|
-
});
|
|
231
|
-
else target[key] = value;
|
|
232
|
-
}
|
|
233
|
-
function isPlainObject(value) {
|
|
234
|
-
if (typeof value !== "object" || value === null) return false;
|
|
235
|
-
const proto = Object.getPrototypeOf(value);
|
|
236
|
-
return proto === Object.prototype || proto === null;
|
|
237
|
-
}
|
|
238
|
-
function deepClone(value, options = {}) {
|
|
239
|
-
const seen = /* @__PURE__ */ new Map();
|
|
240
|
-
function clone(val) {
|
|
241
|
-
if (val === null || typeof val !== "object") return val;
|
|
242
|
-
const obj = val;
|
|
243
|
-
if (seen.has(obj)) return seen.get(obj);
|
|
244
|
-
if (val instanceof Date) return new Date(val.getTime());
|
|
245
|
-
if (val instanceof RegExp) return new RegExp(val.source, val.flags);
|
|
246
|
-
if (Array.isArray(val)) {
|
|
247
|
-
const arr = [];
|
|
248
|
-
seen.set(obj, arr);
|
|
249
|
-
for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
|
|
250
|
-
return arr;
|
|
251
|
-
}
|
|
252
|
-
if (val instanceof Map) {
|
|
253
|
-
const copy = /* @__PURE__ */ new Map();
|
|
254
|
-
seen.set(obj, copy);
|
|
255
|
-
for (const [k, v] of val) copy.set(clone(k), clone(v));
|
|
256
|
-
return copy;
|
|
257
|
-
}
|
|
258
|
-
if (val instanceof Set) {
|
|
259
|
-
const copy = /* @__PURE__ */ new Set();
|
|
260
|
-
seen.set(obj, copy);
|
|
261
|
-
for (const v of val) copy.add(clone(v));
|
|
262
|
-
return copy;
|
|
263
|
-
}
|
|
264
|
-
const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
|
|
265
|
-
if (!keepsProto && options.keepUnregistered) return val;
|
|
266
|
-
const copy = keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {};
|
|
267
|
-
seen.set(obj, copy);
|
|
268
|
-
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
269
|
-
return copy;
|
|
270
|
-
}
|
|
271
|
-
return clone(value);
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Structural equality over the value types deepClone() supports: primitives,
|
|
275
|
-
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
276
|
-
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
277
|
-
* are compared by length and index, so a hole equals an undefined element
|
|
278
|
-
* (deepClone() and save/load turn holes into undefined elements).
|
|
279
|
-
*/
|
|
280
|
-
function deepEqual(a, b) {
|
|
281
|
-
return equal(a, b, /* @__PURE__ */ new Map());
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
285
|
-
* cycle) assumes it equal. Every object may pair with several others, since
|
|
286
|
-
* cycles of different lengths can still unfold to the same value.
|
|
287
|
-
*/
|
|
288
|
-
function equal(a, b, assumed) {
|
|
289
|
-
if (Object.is(a, b)) return true;
|
|
290
|
-
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
291
|
-
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
292
|
-
const pairs = assumed.get(a);
|
|
293
|
-
if (pairs?.has(b)) return true;
|
|
294
|
-
if (pairs) pairs.add(b);
|
|
295
|
-
else assumed.set(a, new Set([b]));
|
|
296
|
-
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
297
|
-
if (a instanceof RegExp) return String(a) === String(b);
|
|
298
|
-
if (a instanceof Map || a instanceof Set) {
|
|
299
|
-
const bc = b;
|
|
300
|
-
if (a.size !== bc.size) return false;
|
|
301
|
-
const ai = a.entries();
|
|
302
|
-
const bi = bc.entries();
|
|
303
|
-
for (let x = ai.next(), y = bi.next(); !x.done; x = ai.next(), y = bi.next()) if (!equal(x.value, y.value, assumed)) return false;
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
if (Array.isArray(a)) {
|
|
307
|
-
const bc = b;
|
|
308
|
-
if (a.length !== bc.length) return false;
|
|
309
|
-
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
const ao = a;
|
|
313
|
-
const bo = b;
|
|
314
|
-
const keys = Object.keys(ao);
|
|
315
|
-
if (keys.length !== Object.keys(bo).length) return false;
|
|
316
|
-
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Objects merged property by property: plain objects and (registered)
|
|
321
|
-
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
322
|
-
* replaced as a whole, since their elements have no stable identity to
|
|
323
|
-
* merge by (a shift moves every index).
|
|
324
|
-
*/
|
|
325
|
-
function isMergeable(value) {
|
|
326
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
330
|
-
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
331
|
-
*/
|
|
332
|
-
function mergesWith(a, b, arrays) {
|
|
333
|
-
if (arrays && (Array.isArray(a) || Array.isArray(b))) return Array.isArray(a) && Array.isArray(b);
|
|
334
|
-
return isMergeable(a) && isMergeable(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* The keys at which `after` differs from `before`, two values that merge
|
|
338
|
-
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
339
|
-
* updates are immutable, so a value that kept its identity is no change,
|
|
340
|
-
* and one rebuilt with equal content (mutation code commits whole values)
|
|
341
|
-
* is none either. Values that merge are reported as 'nested', to be
|
|
342
|
-
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
343
|
-
* merge too: elements at the indices both hold are compared, and the
|
|
344
|
-
* elements one has beyond the other's length are added or deleted.
|
|
345
|
-
*/
|
|
346
|
-
function keyChanges(before, after, arrays) {
|
|
347
|
-
const changes = [];
|
|
348
|
-
const compare = (key, b, a) => {
|
|
349
|
-
if (Object.is(b, a)) return;
|
|
350
|
-
if (mergesWith(b, a, arrays)) changes.push({
|
|
351
|
-
key,
|
|
352
|
-
kind: "nested",
|
|
353
|
-
before: b,
|
|
354
|
-
after: a
|
|
355
|
-
});
|
|
356
|
-
else if (!deepEqual(b, a)) changes.push({
|
|
357
|
-
key,
|
|
358
|
-
kind: "changed",
|
|
359
|
-
after: a
|
|
360
|
-
});
|
|
361
|
-
};
|
|
362
|
-
if (Array.isArray(after)) {
|
|
363
|
-
const b = before;
|
|
364
|
-
const a = after;
|
|
365
|
-
for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
|
|
366
|
-
for (let i = b.length; i < a.length; i++) changes.push({
|
|
367
|
-
key: String(i),
|
|
368
|
-
kind: "added",
|
|
369
|
-
after: a[i]
|
|
370
|
-
});
|
|
371
|
-
for (let i = a.length; i < b.length; i++) changes.push({
|
|
372
|
-
key: String(i),
|
|
373
|
-
kind: "deleted"
|
|
374
|
-
});
|
|
375
|
-
return changes;
|
|
376
|
-
}
|
|
377
|
-
for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
378
|
-
else changes.push({
|
|
379
|
-
key,
|
|
380
|
-
kind: "added",
|
|
381
|
-
after: after[key]
|
|
382
|
-
});
|
|
383
|
-
for (const key of Object.keys(before)) if (!hasOwn(after, key)) changes.push({
|
|
384
|
-
key,
|
|
385
|
-
kind: "deleted"
|
|
386
|
-
});
|
|
387
|
-
return changes;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* The property paths where `after` differs from `before`, two objects of
|
|
391
|
-
* one class: objects that merge (see isMergeable) are compared property by
|
|
392
|
-
* property, any other value as a whole.
|
|
393
|
-
*/
|
|
394
|
-
function diffPaths(before, after) {
|
|
395
|
-
const changes = [];
|
|
396
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
397
|
-
(function walk(b, a, path) {
|
|
398
|
-
if (ancestors.has(a)) return;
|
|
399
|
-
ancestors.add(a);
|
|
400
|
-
for (const change of keyChanges(b, a, false)) {
|
|
401
|
-
const at = [...path, change.key];
|
|
402
|
-
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
403
|
-
else changes.push(change.kind === "deleted" ? {
|
|
404
|
-
path: at,
|
|
405
|
-
deleted: true
|
|
406
|
-
} : {
|
|
407
|
-
path: at,
|
|
408
|
-
deleted: false,
|
|
409
|
-
value: change.after
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
ancestors.delete(a);
|
|
413
|
-
})(before, after, []);
|
|
414
|
-
return changes;
|
|
415
|
-
}
|
|
416
|
-
/** Whether `target` already holds what `change` would write. */
|
|
417
|
-
function isApplied(target, change) {
|
|
418
|
-
const parent = getByPath(target, change.path.slice(0, -1));
|
|
419
|
-
if (parent === null || typeof parent !== "object") return change.deleted;
|
|
420
|
-
const key = change.path[change.path.length - 1];
|
|
421
|
-
if (!hasOwn(parent, key)) return change.deleted;
|
|
422
|
-
return !change.deleted && deepEqual(parent[key], change.value);
|
|
423
|
-
}
|
|
424
|
-
function applyChange(target, change) {
|
|
425
|
-
if (change.deleted) deleteByPath(target, change.path);
|
|
426
|
-
else setByPath(target, change.path, change.value);
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
430
|
-
* index) into `target`, another version of the same object or array.
|
|
431
|
-
* Values that changed are written whole, as deep copies; for values that
|
|
432
|
-
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
433
|
-
* whether it did. Into an array, elements removed from the end are removed
|
|
434
|
-
* at the same indices and elements added are appended, and changes at
|
|
435
|
-
* indices `target` lacks are dropped: where `target` was resized, its
|
|
436
|
-
* indices do not line up with `before`'s.
|
|
437
|
-
*/
|
|
438
|
-
function mergeKeys(target, before, after, mergeNested) {
|
|
439
|
-
const list = Array.isArray(target) ? target : void 0;
|
|
440
|
-
for (const change of keyChanges(before, after, true)) {
|
|
441
|
-
const { key } = change;
|
|
442
|
-
if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
|
|
443
|
-
else delete target[key];
|
|
444
|
-
else if (list && change.kind === "added") list.push(deepClone(change.after));
|
|
445
|
-
else if (list && Number(key) >= list.length) continue;
|
|
446
|
-
else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
//#endregion
|
|
450
118
|
//#region src/class-registry.ts
|
|
451
119
|
var registry$1 = /* @__PURE__ */ new Map();
|
|
452
120
|
var ctorToName = /* @__PURE__ */ new Map();
|
|
@@ -946,6 +614,13 @@ var UNCLOSED = -1;
|
|
|
946
614
|
var MALFORMED = -2;
|
|
947
615
|
/** A frame result: the scan stops at index `s` inside it (`STOPPED - s`). */
|
|
948
616
|
var STOPPED = -3;
|
|
617
|
+
/**
|
|
618
|
+
* The result of a frame that closes at `end`. `bodyStarted`: the code inside
|
|
619
|
+
* started a function or class body. That body replaced the one to come, and
|
|
620
|
+
* is gone once the frame is closed, so after the frame no body is to come —
|
|
621
|
+
* a scan that skips the frame must know, as must the frames around it.
|
|
622
|
+
*/
|
|
623
|
+
var frameEnd = (end, bodyStarted) => end * 2 + +bodyStarted;
|
|
949
624
|
/** The closers whose search for their frame a frame result may depend on. */
|
|
950
625
|
var CLOSERS = [
|
|
951
626
|
")",
|
|
@@ -1178,14 +853,30 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1178
853
|
]] : void 0;
|
|
1179
854
|
/**
|
|
1180
855
|
* How many function or class bodies to come the scan has started, and with
|
|
1181
|
-
* `parens`, that count when each open frame opened. Code in a
|
|
1182
|
-
*
|
|
1183
|
-
* follow once the frame is closed: none is to come then.
|
|
856
|
+
* `braces` or `parens`, that count when each open frame opened. Code in a
|
|
857
|
+
* frame that starts one replaces the one to come, and the new one can't
|
|
858
|
+
* follow once the frame is closed: none is to come then. A scan skipping
|
|
859
|
+
* the frame learns this from its result (`frameEnd`, `resume`), and counts
|
|
860
|
+
* the body as started too, for the frames around it to record.
|
|
1184
861
|
*/
|
|
1185
862
|
let bodyStarts = 0;
|
|
1186
|
-
const startsAt = parens ? [0] : void 0;
|
|
863
|
+
const startsAt = braces || parens ? [0] : void 0;
|
|
1187
864
|
/** A `(…)` or `[…]` frame result found in `parens`, to skip to. */
|
|
1188
865
|
let parenTo;
|
|
866
|
+
/** The result of the frame at index `k`, closing at `i`. */
|
|
867
|
+
const closedAt = (k) => frameEnd(i, startsAt !== void 0 && bodyStarts !== startsAt[k]);
|
|
868
|
+
/**
|
|
869
|
+
* Go on after a frame an earlier scan lexed, whose result (`frameEnd`) is
|
|
870
|
+
* `result`: returns the index it closes at. A body started in it is gone
|
|
871
|
+
* after it, with the one it replaced.
|
|
872
|
+
*/
|
|
873
|
+
function resume(result) {
|
|
874
|
+
if (result % 2 === 1) {
|
|
875
|
+
pendingBody = void 0;
|
|
876
|
+
bodyStarts++;
|
|
877
|
+
}
|
|
878
|
+
return Math.floor(result / 2);
|
|
879
|
+
}
|
|
1189
880
|
/** The closer `c` looked for its frame and found index `k`. */
|
|
1190
881
|
function lookedFor(c, k) {
|
|
1191
882
|
if (!lowest) return;
|
|
@@ -1333,8 +1024,8 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1333
1024
|
if (lowest) {
|
|
1334
1025
|
for (let j = frames.length - 1; j >= k; j--) foldLowest(j);
|
|
1335
1026
|
lowest.length = k;
|
|
1336
|
-
startsAt.length = k;
|
|
1337
1027
|
}
|
|
1028
|
+
if (startsAt) startsAt.length = k;
|
|
1338
1029
|
frames.length = k;
|
|
1339
1030
|
if (stackIds) stackIds.length = k;
|
|
1340
1031
|
for (const list of Object.values(closers)) while (list.length && list[list.length - 1] >= k) list.pop();
|
|
@@ -1411,6 +1102,18 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1411
1102
|
stmtNext = f.kind === "block";
|
|
1412
1103
|
keyNext = f.kind === "object" || f.kind === "class";
|
|
1413
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Go past frame `f` if an earlier scan lexed it: to just after it, or to
|
|
1107
|
+
* the end of the source when it never closes. Whether it did, or
|
|
1108
|
+
* MALFORMED when that scan found the code malformed.
|
|
1109
|
+
*/
|
|
1110
|
+
function skipLexed(f) {
|
|
1111
|
+
const end = known(f);
|
|
1112
|
+
if (end === void 0) return false;
|
|
1113
|
+
if (end === MALFORMED) return MALFORMED;
|
|
1114
|
+
i = end === UNCLOSED ? src.length : resume(end) + 1;
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1414
1117
|
function openBrace() {
|
|
1415
1118
|
let f;
|
|
1416
1119
|
if (pendingBody?.depth === frames.length) {
|
|
@@ -1454,10 +1157,10 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1454
1157
|
return;
|
|
1455
1158
|
}
|
|
1456
1159
|
const closed = frames[k];
|
|
1160
|
+
const result = closedAt(k);
|
|
1457
1161
|
if (lowest && c !== "}") {
|
|
1458
1162
|
for (let j = frames.length - 1; j > k; j--) foldLowest(j);
|
|
1459
|
-
|
|
1460
|
-
recordParen(k, i * 2 + +bodyStarted);
|
|
1163
|
+
recordParen(k, result);
|
|
1461
1164
|
}
|
|
1462
1165
|
truncate(k);
|
|
1463
1166
|
if (c === "]" && !ctx.lookahead) ctx.brackets.set(closed.open, i);
|
|
@@ -1466,7 +1169,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1466
1169
|
stmtNext = closed.header;
|
|
1467
1170
|
} else if (c === "]") endPunct(c, false);
|
|
1468
1171
|
else {
|
|
1469
|
-
record(closed,
|
|
1172
|
+
record(closed, result);
|
|
1470
1173
|
afterBrace(closed);
|
|
1471
1174
|
}
|
|
1472
1175
|
}
|
|
@@ -1568,23 +1271,16 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1568
1271
|
} else if (ch === "`") {
|
|
1569
1272
|
literal(ch, i);
|
|
1570
1273
|
if (frames.length === 1) return i + 1;
|
|
1571
|
-
record(top(),
|
|
1274
|
+
record(top(), closedAt(frames.length - 1));
|
|
1572
1275
|
i++;
|
|
1573
1276
|
truncate(frames.length - 1);
|
|
1574
1277
|
endOperand();
|
|
1575
1278
|
} else if (ch === "$" && src.charAt(i + 1) === "{") {
|
|
1576
1279
|
const f = frame("expr", "}", i);
|
|
1577
1280
|
f.interpolation = true;
|
|
1578
|
-
const
|
|
1579
|
-
if (
|
|
1580
|
-
if (
|
|
1581
|
-
i = src.length;
|
|
1582
|
-
break;
|
|
1583
|
-
}
|
|
1584
|
-
if (end !== void 0) {
|
|
1585
|
-
i = end + 1;
|
|
1586
|
-
continue;
|
|
1587
|
-
}
|
|
1281
|
+
const skipped = skipLexed(f);
|
|
1282
|
+
if (skipped === MALFORMED) return malformed();
|
|
1283
|
+
if (skipped) continue;
|
|
1588
1284
|
literal("${", i);
|
|
1589
1285
|
i += 2;
|
|
1590
1286
|
nesting++;
|
|
@@ -1611,14 +1307,9 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1611
1307
|
if (ch === "`") {
|
|
1612
1308
|
endWord();
|
|
1613
1309
|
const f = frame("template", "`", i);
|
|
1614
|
-
const
|
|
1615
|
-
if (
|
|
1616
|
-
if (
|
|
1617
|
-
i = src.length;
|
|
1618
|
-
break;
|
|
1619
|
-
}
|
|
1620
|
-
if (end !== void 0) {
|
|
1621
|
-
i = end + 1;
|
|
1310
|
+
const skipped = skipLexed(f);
|
|
1311
|
+
if (skipped === MALFORMED) return malformed();
|
|
1312
|
+
if (skipped) {
|
|
1622
1313
|
endOperand();
|
|
1623
1314
|
continue;
|
|
1624
1315
|
}
|
|
@@ -1637,7 +1328,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1637
1328
|
lookedFor("}", k);
|
|
1638
1329
|
if (frames[k].interpolation) {
|
|
1639
1330
|
endWord();
|
|
1640
|
-
record(frames[k],
|
|
1331
|
+
record(frames[k], closedAt(k));
|
|
1641
1332
|
truncate(k);
|
|
1642
1333
|
nesting--;
|
|
1643
1334
|
literal(ch, i);
|
|
@@ -1719,7 +1410,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1719
1410
|
continue;
|
|
1720
1411
|
}
|
|
1721
1412
|
}
|
|
1722
|
-
if (ch === outer.closer && frames.length === 1) break;
|
|
1413
|
+
if (ch === outer.closer && (frames.length === 1 || ch === "]" && innermost("]") === 0 && innermost("}") === 0)) break;
|
|
1723
1414
|
trackCode(ch);
|
|
1724
1415
|
if (skipTo) {
|
|
1725
1416
|
const { frame: skipped, end } = skipTo;
|
|
@@ -1729,7 +1420,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1729
1420
|
i = src.length;
|
|
1730
1421
|
break;
|
|
1731
1422
|
}
|
|
1732
|
-
i = end;
|
|
1423
|
+
i = resume(end);
|
|
1733
1424
|
afterBrace(skipped);
|
|
1734
1425
|
i++;
|
|
1735
1426
|
continue;
|
|
@@ -1747,8 +1438,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1747
1438
|
strict.stopped = true;
|
|
1748
1439
|
return STOPPED - result;
|
|
1749
1440
|
}
|
|
1750
|
-
i =
|
|
1751
|
-
if (result % 2 === 1) pendingBody = void 0;
|
|
1441
|
+
i = resume(result);
|
|
1752
1442
|
if (skipped.closer === ")") {
|
|
1753
1443
|
endPunct(")", skipped.header);
|
|
1754
1444
|
stmtNext = skipped.header;
|
|
@@ -2602,9 +2292,46 @@ function tokenize(input, options = {}) {
|
|
|
2602
2292
|
return tokens;
|
|
2603
2293
|
}
|
|
2604
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
|
|
2605
2332
|
//#region src/markup/ast.ts
|
|
2606
2333
|
/** Macros that require a closing tag and can contain children */
|
|
2607
|
-
var BLOCK_MACROS = new
|
|
2334
|
+
var BLOCK_MACROS = new NameSet([
|
|
2608
2335
|
"if",
|
|
2609
2336
|
"for",
|
|
2610
2337
|
"do",
|
|
@@ -2622,7 +2349,7 @@ var BLOCK_MACROS = new Set([
|
|
|
2622
2349
|
]);
|
|
2623
2350
|
/** Register a custom macro as a block macro so the AST builder nests children. */
|
|
2624
2351
|
function registerBlockMacro(name) {
|
|
2625
|
-
BLOCK_MACROS.add(name
|
|
2352
|
+
BLOCK_MACROS.add(name);
|
|
2626
2353
|
}
|
|
2627
2354
|
/** Map from branch macro name → required parent macro name */
|
|
2628
2355
|
var BRANCH_PARENT = {
|
|
@@ -3016,15 +2743,15 @@ function countQueries(counts) {
|
|
|
3016
2743
|
all: (...names) => names.every((n) => count(n) > 0)
|
|
3017
2744
|
};
|
|
3018
2745
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
const visits = countQueries(
|
|
3026
|
-
const renders = countQueries(
|
|
3027
|
-
|
|
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 {
|
|
3028
2755
|
currentPassage,
|
|
3029
2756
|
previousPassage,
|
|
3030
2757
|
visited: visits.count,
|
|
@@ -3034,7 +2761,17 @@ function buildExpressionFns() {
|
|
|
3034
2761
|
rendered: renders.count,
|
|
3035
2762
|
hasRendered: renders.has,
|
|
3036
2763
|
hasRenderedAny: renders.any,
|
|
3037
|
-
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),
|
|
3038
2775
|
random,
|
|
3039
2776
|
randomInt
|
|
3040
2777
|
};
|
|
@@ -3055,57 +2792,61 @@ function namespaceArg(ns) {
|
|
|
3055
2792
|
Object.setPrototypeOf(ns, null);
|
|
3056
2793
|
return ns;
|
|
3057
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
|
+
}
|
|
3058
2799
|
/**
|
|
3059
2800
|
* Evaluate an expression and return its value.
|
|
3060
2801
|
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
3061
2802
|
*/
|
|
3062
|
-
function evaluate(expr,
|
|
2803
|
+
function evaluate(expr, ...namespaces) {
|
|
3063
2804
|
const body = `return (${transform(expr)}\n);`;
|
|
3064
|
-
return getOrCompile(body, body)
|
|
2805
|
+
return run(getOrCompile(body, body), ...namespaces);
|
|
3065
2806
|
}
|
|
3066
2807
|
/**
|
|
3067
2808
|
* Execute statements (no return value).
|
|
3068
2809
|
* e.g. execute("$health = 100; $name = 'Hero'", variables, temporary)
|
|
3069
2810
|
*/
|
|
3070
|
-
function execute(code,
|
|
2811
|
+
function execute(code, ...namespaces) {
|
|
3071
2812
|
const transformed = transform(code, "statements");
|
|
3072
|
-
getOrCompile("exec:" + transformed, transformed)
|
|
2813
|
+
run(getOrCompile("exec:" + transformed, transformed), ...namespaces);
|
|
3073
2814
|
}
|
|
3074
2815
|
//#endregion
|
|
3075
2816
|
//#region src/registry.ts
|
|
3076
|
-
var registry =
|
|
2817
|
+
var registry = new NameMap();
|
|
3077
2818
|
function registerMacro(name, component) {
|
|
3078
|
-
registry.set(name
|
|
2819
|
+
registry.set(name, component);
|
|
3079
2820
|
}
|
|
3080
2821
|
function getMacro(name) {
|
|
3081
|
-
return registry.get(name
|
|
2822
|
+
return registry.get(name);
|
|
3082
2823
|
}
|
|
3083
|
-
var textRegistry =
|
|
2824
|
+
var textRegistry = new NameMap();
|
|
3084
2825
|
/** Register (or, with undefined, remove) the text form of a macro. */
|
|
3085
2826
|
function registerMacroText(name, text) {
|
|
3086
|
-
if (text) textRegistry.set(name
|
|
3087
|
-
else textRegistry.delete(name
|
|
2827
|
+
if (text) textRegistry.set(name, text);
|
|
2828
|
+
else textRegistry.delete(name);
|
|
3088
2829
|
}
|
|
3089
2830
|
function getMacroText(name) {
|
|
3090
|
-
return textRegistry.get(name
|
|
2831
|
+
return textRegistry.get(name);
|
|
3091
2832
|
}
|
|
3092
|
-
var subMacros =
|
|
2833
|
+
var subMacros = new NameSet();
|
|
3093
2834
|
function registerSubMacro(name) {
|
|
3094
|
-
subMacros.add(name
|
|
2835
|
+
subMacros.add(name);
|
|
3095
2836
|
}
|
|
3096
2837
|
function isSubMacro(name) {
|
|
3097
|
-
return subMacros.has(name
|
|
2838
|
+
return subMacros.has(name);
|
|
3098
2839
|
}
|
|
3099
|
-
var metadataRegistry =
|
|
2840
|
+
var metadataRegistry = new NameMap();
|
|
3100
2841
|
function registerMacroMetadata(name, metadata) {
|
|
3101
|
-
metadataRegistry.set(name
|
|
2842
|
+
metadataRegistry.set(name, metadata);
|
|
3102
2843
|
}
|
|
3103
2844
|
function getMacroRegistry() {
|
|
3104
2845
|
return Array.from(metadataRegistry.values());
|
|
3105
2846
|
}
|
|
3106
2847
|
//#endregion
|
|
3107
2848
|
//#region src/widgets/widget-registry.ts
|
|
3108
|
-
var widgets =
|
|
2849
|
+
var widgets = new NameMap();
|
|
3109
2850
|
/**
|
|
3110
2851
|
* Register a widget. Its `@` parameters become locals of its body, so one
|
|
3111
2852
|
* that no namespace can hold (`@__proto__`) throws a TypeError and the
|
|
@@ -3114,14 +2855,14 @@ var widgets = /* @__PURE__ */ new Map();
|
|
|
3114
2855
|
function registerWidget(name, bodyAST, params, isBlock = false) {
|
|
3115
2856
|
const filteredParams = params.filter((p) => p !== "@children");
|
|
3116
2857
|
for (const param of filteredParams) if (param.startsWith("@")) checkVariableName(param.slice(1), param);
|
|
3117
|
-
widgets.set(name
|
|
2858
|
+
widgets.set(name, {
|
|
3118
2859
|
body: bodyAST,
|
|
3119
2860
|
params: filteredParams,
|
|
3120
2861
|
isBlock
|
|
3121
2862
|
});
|
|
3122
2863
|
}
|
|
3123
2864
|
function getWidget(name) {
|
|
3124
|
-
return widgets.get(name
|
|
2865
|
+
return widgets.get(name);
|
|
3125
2866
|
}
|
|
3126
2867
|
//#endregion
|
|
3127
2868
|
//#region src/components/macros/arg-utils.ts
|
|
@@ -3288,6 +3029,23 @@ function splitArgs(raw) {
|
|
|
3288
3029
|
return args;
|
|
3289
3030
|
}
|
|
3290
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
|
|
3291
3049
|
//#region src/interpolation.ts
|
|
3292
3050
|
/**
|
|
3293
3051
|
* Text-only markup: HTML attribute values, image alt text and link titles,
|
|
@@ -3356,9 +3114,6 @@ function mapTextNodes(nodes, fn) {
|
|
|
3356
3114
|
}
|
|
3357
3115
|
/** Deep enough for any real nesting; stops a widget that includes itself. */
|
|
3358
3116
|
var MAX_DEPTH = 100;
|
|
3359
|
-
function display(value) {
|
|
3360
|
-
return value == null ? "" : String(value);
|
|
3361
|
-
}
|
|
3362
3117
|
/**
|
|
3363
3118
|
* The value of a variable reference (`name` without its sigil, with an
|
|
3364
3119
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -3376,6 +3131,10 @@ function resolveVariable(scope, kind, name) {
|
|
|
3376
3131
|
}
|
|
3377
3132
|
return value;
|
|
3378
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
|
+
}
|
|
3379
3138
|
var TextEvaluator = class {
|
|
3380
3139
|
constructor() {
|
|
3381
3140
|
this.errors = [];
|
|
@@ -3385,9 +3144,6 @@ var TextEvaluator = class {
|
|
|
3385
3144
|
for (const node of nodes) text += this.node(node, scope, depth);
|
|
3386
3145
|
return text;
|
|
3387
3146
|
}
|
|
3388
|
-
evaluate(expr, scope) {
|
|
3389
|
-
return evaluate(expr, scope.variables, scope.temporary, scope.locals, scope.transient);
|
|
3390
|
-
}
|
|
3391
3147
|
node(node, scope, depth) {
|
|
3392
3148
|
switch (node.type) {
|
|
3393
3149
|
case "text": return node.value;
|
|
@@ -3399,7 +3155,7 @@ var TextEvaluator = class {
|
|
|
3399
3155
|
}
|
|
3400
3156
|
return this.variable(node.scope, node.name, scope);
|
|
3401
3157
|
case "expression": try {
|
|
3402
|
-
return display(
|
|
3158
|
+
return display(evaluateIn(node.expression, scope));
|
|
3403
3159
|
} catch (error) {
|
|
3404
3160
|
this.errors.push({
|
|
3405
3161
|
macro: "expression",
|
|
@@ -3449,7 +3205,7 @@ var TextEvaluator = class {
|
|
|
3449
3205
|
let value;
|
|
3450
3206
|
const expr = argExprs[i];
|
|
3451
3207
|
if (expr !== void 0) try {
|
|
3452
|
-
value =
|
|
3208
|
+
value = evaluateIn(expr, scope);
|
|
3453
3209
|
} catch {
|
|
3454
3210
|
value = void 0;
|
|
3455
3211
|
}
|
|
@@ -3471,7 +3227,7 @@ var TextEvaluator = class {
|
|
|
3471
3227
|
return "";
|
|
3472
3228
|
}
|
|
3473
3229
|
const ctx = {
|
|
3474
|
-
evaluate: (expr) =>
|
|
3230
|
+
evaluate: (expr) => evaluateIn(expr, scope),
|
|
3475
3231
|
renderText: (nodes, locals) => this.nested(node.name, nodes, scope, depth, locals ? { locals: withLocals(scope.locals, locals) } : {})
|
|
3476
3232
|
};
|
|
3477
3233
|
try {
|
|
@@ -3548,7 +3304,7 @@ function interpolateCode(template, scope, decode) {
|
|
|
3548
3304
|
});
|
|
3549
3305
|
}
|
|
3550
3306
|
} else try {
|
|
3551
|
-
text += display(
|
|
3307
|
+
text += display(evaluateIn(part.expr, scope));
|
|
3552
3308
|
} catch (error) {
|
|
3553
3309
|
errors.push({
|
|
3554
3310
|
macro: "expression",
|
|
@@ -3614,29 +3370,26 @@ function useInterpolate() {
|
|
|
3614
3370
|
}
|
|
3615
3371
|
//#endregion
|
|
3616
3372
|
//#region src/components/macros/VarDisplay.tsx
|
|
3617
|
-
var SIGILS = {
|
|
3618
|
-
variable: "$",
|
|
3619
|
-
temporary: "_",
|
|
3620
|
-
local: "@",
|
|
3621
|
-
transient: "%"
|
|
3622
|
-
};
|
|
3623
3373
|
var STORE_NAMESPACES = {
|
|
3624
3374
|
variable: "variables",
|
|
3625
3375
|
temporary: "temporary",
|
|
3626
3376
|
transient: "transient"
|
|
3627
3377
|
};
|
|
3628
|
-
|
|
3378
|
+
/** A `{$name}` (`{_name}`, `{@name}`, `{%name}`) variable display. */
|
|
3379
|
+
function VarDisplay({ node }) {
|
|
3380
|
+
const { name, scope } = node;
|
|
3629
3381
|
const resolve = useInterpolate();
|
|
3630
|
-
className = resolve(className);
|
|
3631
|
-
id = resolve(id);
|
|
3382
|
+
const className = resolve(node.className);
|
|
3383
|
+
const id = resolve(node.id);
|
|
3632
3384
|
const localsValues = useContext(LocalsValuesContext);
|
|
3633
3385
|
const parts = name.split(".");
|
|
3634
3386
|
const root = parts[0];
|
|
3635
3387
|
const storeValue = useStoryStore((s) => scope === "local" ? void 0 : ownValue(s[STORE_NAMESPACES[scope]], root));
|
|
3636
|
-
const
|
|
3388
|
+
const sigil = SCOPE_SIGILS[scope];
|
|
3389
|
+
const nameError = variableNameError(root, sigil + root);
|
|
3637
3390
|
if (nameError) return /* @__PURE__ */ jsx("span", {
|
|
3638
3391
|
class: "error",
|
|
3639
|
-
children: `{${
|
|
3392
|
+
children: `{${sigil}${name} error: ${nameError}}`
|
|
3640
3393
|
});
|
|
3641
3394
|
let value;
|
|
3642
3395
|
if (scope === "local") value = ownValue(localsValues, root);
|
|
@@ -3649,34 +3402,24 @@ function VarDisplay({ name, scope, className, id }) {
|
|
|
3649
3402
|
const part = parts[i];
|
|
3650
3403
|
value = value[part];
|
|
3651
3404
|
}
|
|
3652
|
-
|
|
3653
|
-
if (className || id) return /* @__PURE__ */ jsx("span", {
|
|
3654
|
-
id,
|
|
3655
|
-
class: className,
|
|
3656
|
-
children: display
|
|
3657
|
-
});
|
|
3658
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3405
|
+
return wrapContent(className, id, display(value));
|
|
3659
3406
|
}
|
|
3660
3407
|
//#endregion
|
|
3661
3408
|
//#region src/components/macros/ExprDisplay.tsx
|
|
3662
|
-
|
|
3409
|
+
/** An expression display: `{$hp + 1}`. */
|
|
3410
|
+
function ExprDisplay({ node }) {
|
|
3411
|
+
const { expression } = node;
|
|
3663
3412
|
const resolve = useInterpolate();
|
|
3664
|
-
className = resolve(className);
|
|
3665
|
-
id = resolve(id);
|
|
3413
|
+
const className = resolve(node.className);
|
|
3414
|
+
const id = resolve(node.id);
|
|
3666
3415
|
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
3667
|
-
let
|
|
3416
|
+
let text;
|
|
3668
3417
|
try {
|
|
3669
|
-
|
|
3670
|
-
display = value == null ? "" : String(value);
|
|
3418
|
+
text = display(evaluate(expression, variables, temporary, localsValues, transient));
|
|
3671
3419
|
} catch {
|
|
3672
|
-
|
|
3420
|
+
text = `{error: ${expression}}`;
|
|
3673
3421
|
}
|
|
3674
|
-
|
|
3675
|
-
id,
|
|
3676
|
-
class: className,
|
|
3677
|
-
children: display
|
|
3678
|
-
});
|
|
3679
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3422
|
+
return wrapContent(className, id, text);
|
|
3680
3423
|
}
|
|
3681
3424
|
//#endregion
|
|
3682
3425
|
//#region src/hooks/use-render-options.ts
|
|
@@ -4411,17 +4154,9 @@ function HtmlNodeRenderer({ node }) {
|
|
|
4411
4154
|
}
|
|
4412
4155
|
function ChildrenSlot() {
|
|
4413
4156
|
const childrenAST = useContext(WidgetChildrenContext);
|
|
4414
|
-
const
|
|
4415
|
-
const inline = useContext(InlineContext);
|
|
4416
|
-
const raw = useContext(RawTextContext);
|
|
4417
|
-
const locals = useContext(LocalsValuesContext);
|
|
4157
|
+
const renderOptions = useRenderOptions();
|
|
4418
4158
|
if (!childrenAST || childrenAST.length === 0) return null;
|
|
4419
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: renderNodes(childrenAST,
|
|
4420
|
-
nobr,
|
|
4421
|
-
locals,
|
|
4422
|
-
inline,
|
|
4423
|
-
raw
|
|
4424
|
-
}) });
|
|
4159
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: renderNodes(childrenAST, renderOptions) });
|
|
4425
4160
|
}
|
|
4426
4161
|
/**
|
|
4427
4162
|
* Stable per-node keys. Keying rendered children by AST node identity (not
|
|
@@ -4471,17 +4206,8 @@ function renderSingleNode(node) {
|
|
|
4471
4206
|
switch (node.type) {
|
|
4472
4207
|
case "variable":
|
|
4473
4208
|
if (node.scope === "local" && node.name === "children") return /* @__PURE__ */ jsx(ChildrenSlot, {}, key);
|
|
4474
|
-
return /* @__PURE__ */ jsx(VarDisplay, {
|
|
4475
|
-
|
|
4476
|
-
scope: node.scope,
|
|
4477
|
-
className: node.className,
|
|
4478
|
-
id: node.id
|
|
4479
|
-
}, key);
|
|
4480
|
-
case "expression": return /* @__PURE__ */ jsx(ExprDisplay, {
|
|
4481
|
-
expression: node.expression,
|
|
4482
|
-
className: node.className,
|
|
4483
|
-
id: node.id
|
|
4484
|
-
}, key);
|
|
4209
|
+
return /* @__PURE__ */ jsx(VarDisplay, { node }, key);
|
|
4210
|
+
case "expression": return /* @__PURE__ */ jsx(ExprDisplay, { node }, key);
|
|
4485
4211
|
case "macro": return renderMacro(node, key);
|
|
4486
4212
|
case "html": return /* @__PURE__ */ jsx(HtmlNodeRenderer, { node }, key);
|
|
4487
4213
|
default: return node;
|
|
@@ -4762,6 +4488,24 @@ function createLocalStorageBackend() {
|
|
|
4762
4488
|
}
|
|
4763
4489
|
var IDB_DB_NAME = "spindle";
|
|
4764
4490
|
var IDB_DB_VERSION = 1;
|
|
4491
|
+
/** Each object store's key path, and the key path of each of its indexes. */
|
|
4492
|
+
var IDB_STORES = {
|
|
4493
|
+
saves: {
|
|
4494
|
+
keyPath: "meta.id",
|
|
4495
|
+
indexes: {
|
|
4496
|
+
ifid: "meta.ifid",
|
|
4497
|
+
playthroughId: "meta.playthroughId"
|
|
4498
|
+
}
|
|
4499
|
+
},
|
|
4500
|
+
playthroughs: {
|
|
4501
|
+
keyPath: "id",
|
|
4502
|
+
indexes: { ifid: "ifid" }
|
|
4503
|
+
},
|
|
4504
|
+
meta: {
|
|
4505
|
+
keyPath: "key",
|
|
4506
|
+
indexes: {}
|
|
4507
|
+
}
|
|
4508
|
+
};
|
|
4765
4509
|
function createIDBBackend() {
|
|
4766
4510
|
let dbPromise = null;
|
|
4767
4511
|
function openDB() {
|
|
@@ -4770,13 +4514,11 @@ function createIDBBackend() {
|
|
|
4770
4514
|
const req = indexedDB.open(IDB_DB_NAME, IDB_DB_VERSION);
|
|
4771
4515
|
req.onupgradeneeded = () => {
|
|
4772
4516
|
const db = req.result;
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4517
|
+
for (const [name, { keyPath, indexes }] of Object.entries(IDB_STORES)) {
|
|
4518
|
+
if (db.objectStoreNames.contains(name)) continue;
|
|
4519
|
+
const store = db.createObjectStore(name, { keyPath });
|
|
4520
|
+
for (const [index, path] of Object.entries(indexes)) store.createIndex(index, path, { unique: false });
|
|
4777
4521
|
}
|
|
4778
|
-
if (!db.objectStoreNames.contains("playthroughs")) db.createObjectStore("playthroughs", { keyPath: "id" }).createIndex("ifid", "ifid", { unique: false });
|
|
4779
|
-
if (!db.objectStoreNames.contains("meta")) db.createObjectStore("meta", { keyPath: "key" });
|
|
4780
4522
|
};
|
|
4781
4523
|
req.onsuccess = () => resolve(req.result);
|
|
4782
4524
|
req.onerror = () => reject(req.error);
|
|
@@ -4853,31 +4595,363 @@ async function detectBackend() {
|
|
|
4853
4595
|
return createMemoryBackend();
|
|
4854
4596
|
}
|
|
4855
4597
|
/**
|
|
4856
|
-
* Detection in flight. Concurrent first callers share it so they all get the
|
|
4857
|
-
* same backend (the memory fallback makes a new, empty store per detection).
|
|
4598
|
+
* Detection in flight. Concurrent first callers share it so they all get the
|
|
4599
|
+
* same backend (the memory fallback makes a new, empty store per detection).
|
|
4600
|
+
*/
|
|
4601
|
+
var _backendPromise = null;
|
|
4602
|
+
function getBackend() {
|
|
4603
|
+
if (_backend) return Promise.resolve(_backend);
|
|
4604
|
+
if (!_backendPromise) {
|
|
4605
|
+
const pending = detectBackend().then((backend) => {
|
|
4606
|
+
if (_backendPromise === pending) _backend = backend;
|
|
4607
|
+
return backend;
|
|
4608
|
+
});
|
|
4609
|
+
_backendPromise = pending;
|
|
4610
|
+
}
|
|
4611
|
+
return _backendPromise;
|
|
4612
|
+
}
|
|
4613
|
+
function resetBackend() {
|
|
4614
|
+
_backend = null;
|
|
4615
|
+
_backendPromise = null;
|
|
4616
|
+
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Synchronous accessor for the current backend type.
|
|
4619
|
+
* Returns 'memory' if the backend hasn't been detected yet.
|
|
4620
|
+
*/
|
|
4621
|
+
function getBackendType() {
|
|
4622
|
+
return _backend?.type ?? "memory";
|
|
4623
|
+
}
|
|
4624
|
+
//#endregion
|
|
4625
|
+
//#region src/utils/object-path.ts
|
|
4626
|
+
/**
|
|
4627
|
+
* Traverse dot-path segments on an object and return the nested value.
|
|
4628
|
+
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
4629
|
+
* are not story state: unless an object holds one as its own property, it
|
|
4630
|
+
* reads as missing, as setByPath() treats it. Other inherited properties
|
|
4631
|
+
* (class getters, `size` of a Map) are read.
|
|
4632
|
+
*/
|
|
4633
|
+
function getByPath(obj, segments) {
|
|
4634
|
+
let current = obj;
|
|
4635
|
+
for (const seg of segments) {
|
|
4636
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
4637
|
+
if (seg in Object.prototype && !hasOwn(current, seg)) return void 0;
|
|
4638
|
+
current = current[seg];
|
|
4639
|
+
}
|
|
4640
|
+
return current;
|
|
4641
|
+
}
|
|
4642
|
+
/** Array indices ("0", "1", …) and "length": the keys an array holds. */
|
|
4643
|
+
var isArrayKey = (key) => key === "length" || /^(0|[1-9]\d*)$/.test(key);
|
|
4644
|
+
/**
|
|
4645
|
+
* Built-ins whose content is not their properties: a property written into
|
|
4646
|
+
* one would be dropped by clones and saves (and by Immer, for Map and Set).
|
|
4647
|
+
*/
|
|
4648
|
+
var builtinName = (value) => value instanceof Map ? "a Map" : value instanceof Set ? "a Set" : value instanceof Date ? "a Date" : value instanceof RegExp ? "a RegExp" : void 0;
|
|
4649
|
+
/**
|
|
4650
|
+
* Shallow copy that keeps the prototype, so a registered class instance
|
|
4651
|
+
* stays an instance of its class (with the same own keys deepClone copies).
|
|
4652
|
+
*/
|
|
4653
|
+
function shallowCopy(value) {
|
|
4654
|
+
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
4655
|
+
for (const key of Object.keys(value)) Object.defineProperty(copy, key, {
|
|
4656
|
+
value: value[key],
|
|
4657
|
+
enumerable: true,
|
|
4658
|
+
writable: true,
|
|
4659
|
+
configurable: true
|
|
4660
|
+
});
|
|
4661
|
+
return copy;
|
|
4662
|
+
}
|
|
4663
|
+
/**
|
|
4664
|
+
* Set a value at dot-path `segments` below `root`.
|
|
4665
|
+
*
|
|
4666
|
+
* When `root` is an Immer draft, objects Immer does not draft (registered
|
|
4667
|
+
* class instances and everything below them) are copied on the way down and
|
|
4668
|
+
* the copy is assigned to its parent, so the write produces new references
|
|
4669
|
+
* up to the root. Writing into such an object directly would mutate the
|
|
4670
|
+
* instance shared with the previous state and with history, and subscribers
|
|
4671
|
+
* would see no change. Outside a draft (a private working copy) the path is
|
|
4672
|
+
* written in place.
|
|
4673
|
+
*
|
|
4674
|
+
* The path goes through own properties of plain objects, class instances
|
|
4675
|
+
* and arrays (by index); anything else throws a TypeError rather than
|
|
4676
|
+
* writing where no clone or save would see it: an inherited property such
|
|
4677
|
+
* as a method counts as missing, and Map, Set, Date and RegExp values and
|
|
4678
|
+
* other keys of arrays are refused, as is a "__proto__" segment.
|
|
4679
|
+
*/
|
|
4680
|
+
function setByPath(root, segments, value, options = {}) {
|
|
4681
|
+
const parent = walkToParent(root, segments, options.createMissing ?? false);
|
|
4682
|
+
parent[segments[segments.length - 1]] = value;
|
|
4683
|
+
}
|
|
4684
|
+
/**
|
|
4685
|
+
* Delete the property at dot-path `segments` below `root`, copying
|
|
4686
|
+
* undrafted objects on the way down like setByPath(). Does nothing when an
|
|
4687
|
+
* intermediate is missing or not an object, or the property is absent (or
|
|
4688
|
+
* only inherited).
|
|
4689
|
+
*/
|
|
4690
|
+
function deleteByPath(root, segments) {
|
|
4691
|
+
const last = segments[segments.length - 1];
|
|
4692
|
+
checkSegments(segments);
|
|
4693
|
+
let holder = root;
|
|
4694
|
+
for (const seg of segments.slice(0, -1)) {
|
|
4695
|
+
if (holder == null || typeof holder !== "object" || !hasOwn(holder, seg)) return;
|
|
4696
|
+
holder = holder[seg];
|
|
4697
|
+
}
|
|
4698
|
+
if (holder == null || typeof holder !== "object" || !hasOwn(holder, last)) return;
|
|
4699
|
+
const parent = walkToParent(root, segments, false);
|
|
4700
|
+
delete parent[last];
|
|
4701
|
+
}
|
|
4702
|
+
/** Refuse a segment that would reach a prototype instead of story state. */
|
|
4703
|
+
function checkSegments(segments) {
|
|
4704
|
+
if (segments.includes("__proto__")) throw new TypeError(`spindle: Cannot use "__proto__" in a variable path ("${segments.join(".")}")`);
|
|
4705
|
+
}
|
|
4706
|
+
/** Throw unless `holder` can take `key` as story state (see setByPath). */
|
|
4707
|
+
function checkHolder(holder, key, segments, depth) {
|
|
4708
|
+
const kind = builtinName(holder) ?? (Array.isArray(holder) && !isArrayKey(key) ? "an array" : "");
|
|
4709
|
+
if (kind) throw new TypeError(`spindle: Cannot set property "${key}" on ${kind} (at "${segments.slice(0, depth).join(".")}")`);
|
|
4710
|
+
}
|
|
4711
|
+
/**
|
|
4712
|
+
* Walk to the object holding the last of `segments`, copying undrafted
|
|
4713
|
+
* objects when `root` is an Immer draft (see setByPath). A missing or
|
|
4714
|
+
* non-object intermediate is replaced by a plain object when
|
|
4715
|
+
* `createMissing` is set, and throws a TypeError otherwise.
|
|
4716
|
+
*/
|
|
4717
|
+
function walkToParent(root, segments, createMissing) {
|
|
4718
|
+
checkSegments(segments);
|
|
4719
|
+
const copyOnWrite = isDraft(root);
|
|
4720
|
+
let current = root;
|
|
4721
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
4722
|
+
const seg = segments[i];
|
|
4723
|
+
checkHolder(current, seg, segments, i);
|
|
4724
|
+
let next = hasOwn(current, seg) ? current[seg] : void 0;
|
|
4725
|
+
if (next == null || typeof next !== "object") {
|
|
4726
|
+
if (!createMissing) throw new TypeError(`spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? "null" : typeof next} (at "${segments.slice(0, i + 1).join(".")}")`);
|
|
4727
|
+
next = {};
|
|
4728
|
+
current[seg] = next;
|
|
4729
|
+
} else if (copyOnWrite && !isDraft(next) && !builtinName(next)) {
|
|
4730
|
+
next = shallowCopy(next);
|
|
4731
|
+
current[seg] = next;
|
|
4732
|
+
}
|
|
4733
|
+
current = next;
|
|
4734
|
+
}
|
|
4735
|
+
checkHolder(current, segments[segments.length - 1], segments, segments.length - 1);
|
|
4736
|
+
return current;
|
|
4737
|
+
}
|
|
4738
|
+
//#endregion
|
|
4739
|
+
//#region src/structural.ts
|
|
4740
|
+
function isPlainObject(value) {
|
|
4741
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4742
|
+
const proto = Object.getPrototypeOf(value);
|
|
4743
|
+
return proto === Object.prototype || proto === null;
|
|
4744
|
+
}
|
|
4745
|
+
function deepClone(value, options = {}) {
|
|
4746
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4747
|
+
function clone(val) {
|
|
4748
|
+
if (val === null || typeof val !== "object") return val;
|
|
4749
|
+
const obj = val;
|
|
4750
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
4751
|
+
if (val instanceof Date) return new Date(val.getTime());
|
|
4752
|
+
if (val instanceof RegExp) return new RegExp(val.source, val.flags);
|
|
4753
|
+
if (Array.isArray(val)) {
|
|
4754
|
+
const arr = [];
|
|
4755
|
+
seen.set(obj, arr);
|
|
4756
|
+
for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
|
|
4757
|
+
return arr;
|
|
4758
|
+
}
|
|
4759
|
+
if (val instanceof Map) {
|
|
4760
|
+
const copy = /* @__PURE__ */ new Map();
|
|
4761
|
+
seen.set(obj, copy);
|
|
4762
|
+
for (const [k, v] of val) copy.set(clone(k), clone(v));
|
|
4763
|
+
return copy;
|
|
4764
|
+
}
|
|
4765
|
+
if (val instanceof Set) {
|
|
4766
|
+
const copy = /* @__PURE__ */ new Set();
|
|
4767
|
+
seen.set(obj, copy);
|
|
4768
|
+
for (const v of val) copy.add(clone(v));
|
|
4769
|
+
return copy;
|
|
4770
|
+
}
|
|
4771
|
+
const keepsProto = registeredClassName(obj) !== void 0 || isPlainObject(val);
|
|
4772
|
+
if (!keepsProto && options.keepUnregistered) return val;
|
|
4773
|
+
const copy = keepsProto ? Object.create(Object.getPrototypeOf(obj)) : {};
|
|
4774
|
+
seen.set(obj, copy);
|
|
4775
|
+
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
4776
|
+
return copy;
|
|
4777
|
+
}
|
|
4778
|
+
return clone(value);
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4781
|
+
* Structural equality over the value types deepClone() supports: primitives,
|
|
4782
|
+
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
4783
|
+
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
4784
|
+
* are compared by length and index, so a hole equals an undefined element
|
|
4785
|
+
* (deepClone() and save/load turn holes into undefined elements).
|
|
4786
|
+
*/
|
|
4787
|
+
function deepEqual(a, b) {
|
|
4788
|
+
return equal(a, b, /* @__PURE__ */ new Map());
|
|
4789
|
+
}
|
|
4790
|
+
/**
|
|
4791
|
+
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
4792
|
+
* cycle) assumes it equal. Every object may pair with several others, since
|
|
4793
|
+
* cycles of different lengths can still unfold to the same value.
|
|
4794
|
+
*/
|
|
4795
|
+
function equal(a, b, assumed) {
|
|
4796
|
+
if (Object.is(a, b)) return true;
|
|
4797
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
4798
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
4799
|
+
const pairs = assumed.get(a);
|
|
4800
|
+
if (pairs?.has(b)) return true;
|
|
4801
|
+
if (pairs) pairs.add(b);
|
|
4802
|
+
else assumed.set(a, new Set([b]));
|
|
4803
|
+
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
4804
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
4805
|
+
if (a instanceof Map || a instanceof Set) {
|
|
4806
|
+
const bc = b;
|
|
4807
|
+
if (a.size !== bc.size) return false;
|
|
4808
|
+
const ai = a.entries();
|
|
4809
|
+
const bi = bc.entries();
|
|
4810
|
+
for (let x = ai.next(), y = bi.next(); !x.done; x = ai.next(), y = bi.next()) if (!equal(x.value, y.value, assumed)) return false;
|
|
4811
|
+
return true;
|
|
4812
|
+
}
|
|
4813
|
+
if (Array.isArray(a)) {
|
|
4814
|
+
const bc = b;
|
|
4815
|
+
if (a.length !== bc.length) return false;
|
|
4816
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], bc[i], assumed)) return false;
|
|
4817
|
+
return true;
|
|
4818
|
+
}
|
|
4819
|
+
const ao = a;
|
|
4820
|
+
const bo = b;
|
|
4821
|
+
const keys = Object.keys(ao);
|
|
4822
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
4823
|
+
for (const key of keys) if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
4824
|
+
return true;
|
|
4825
|
+
}
|
|
4826
|
+
/**
|
|
4827
|
+
* Objects merged property by property: plain objects and (registered)
|
|
4828
|
+
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
4829
|
+
* replaced as a whole, since their elements have no stable identity to
|
|
4830
|
+
* merge by (a shift moves every index).
|
|
4831
|
+
*/
|
|
4832
|
+
function isMergeable(value) {
|
|
4833
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Date) && !(value instanceof RegExp);
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
4837
|
+
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
4858
4838
|
*/
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4839
|
+
function mergesWith(a, b, arrays) {
|
|
4840
|
+
if (arrays && (Array.isArray(a) || Array.isArray(b))) return Array.isArray(a) && Array.isArray(b);
|
|
4841
|
+
return isMergeable(a) && isMergeable(b) && Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
4842
|
+
}
|
|
4843
|
+
/**
|
|
4844
|
+
* The keys at which `after` differs from `before`, two values that merge
|
|
4845
|
+
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
4846
|
+
* updates are immutable, so a value that kept its identity is no change,
|
|
4847
|
+
* and one rebuilt with equal content (mutation code commits whole values)
|
|
4848
|
+
* is none either. Values that merge are reported as 'nested', to be
|
|
4849
|
+
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
4850
|
+
* merge too: elements at the indices both hold are compared, and the
|
|
4851
|
+
* elements one has beyond the other's length are added or deleted.
|
|
4852
|
+
*/
|
|
4853
|
+
function keyChanges(before, after, arrays) {
|
|
4854
|
+
const changes = [];
|
|
4855
|
+
const compare = (key, b, a) => {
|
|
4856
|
+
if (Object.is(b, a)) return;
|
|
4857
|
+
if (mergesWith(b, a, arrays)) changes.push({
|
|
4858
|
+
key,
|
|
4859
|
+
kind: "nested",
|
|
4860
|
+
before: b,
|
|
4861
|
+
after: a
|
|
4866
4862
|
});
|
|
4867
|
-
|
|
4863
|
+
else if (!deepEqual(b, a)) changes.push({
|
|
4864
|
+
key,
|
|
4865
|
+
kind: "changed",
|
|
4866
|
+
after: a
|
|
4867
|
+
});
|
|
4868
|
+
};
|
|
4869
|
+
if (Array.isArray(after)) {
|
|
4870
|
+
const b = before;
|
|
4871
|
+
const a = after;
|
|
4872
|
+
for (let i = 0; i < Math.min(b.length, a.length); i++) compare(String(i), b[i], a[i]);
|
|
4873
|
+
for (let i = b.length; i < a.length; i++) changes.push({
|
|
4874
|
+
key: String(i),
|
|
4875
|
+
kind: "added",
|
|
4876
|
+
after: a[i]
|
|
4877
|
+
});
|
|
4878
|
+
for (let i = a.length; i < b.length; i++) changes.push({
|
|
4879
|
+
key: String(i),
|
|
4880
|
+
kind: "deleted"
|
|
4881
|
+
});
|
|
4882
|
+
return changes;
|
|
4868
4883
|
}
|
|
4869
|
-
|
|
4884
|
+
for (const key of Object.keys(after)) if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
4885
|
+
else changes.push({
|
|
4886
|
+
key,
|
|
4887
|
+
kind: "added",
|
|
4888
|
+
after: after[key]
|
|
4889
|
+
});
|
|
4890
|
+
for (const key of Object.keys(before)) if (!hasOwn(after, key)) changes.push({
|
|
4891
|
+
key,
|
|
4892
|
+
kind: "deleted"
|
|
4893
|
+
});
|
|
4894
|
+
return changes;
|
|
4870
4895
|
}
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4896
|
+
/**
|
|
4897
|
+
* The property paths where `after` differs from `before`, two objects of
|
|
4898
|
+
* one class: objects that merge (see isMergeable) are compared property by
|
|
4899
|
+
* property, any other value as a whole.
|
|
4900
|
+
*/
|
|
4901
|
+
function diffPaths(before, after) {
|
|
4902
|
+
const changes = [];
|
|
4903
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
4904
|
+
(function walk(b, a, path) {
|
|
4905
|
+
if (ancestors.has(a)) return;
|
|
4906
|
+
ancestors.add(a);
|
|
4907
|
+
for (const change of keyChanges(b, a, false)) {
|
|
4908
|
+
const at = [...path, change.key];
|
|
4909
|
+
if (change.kind === "nested") walk(change.before, change.after, at);
|
|
4910
|
+
else changes.push(change.kind === "deleted" ? {
|
|
4911
|
+
path: at,
|
|
4912
|
+
deleted: true
|
|
4913
|
+
} : {
|
|
4914
|
+
path: at,
|
|
4915
|
+
deleted: false,
|
|
4916
|
+
value: change.after
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
ancestors.delete(a);
|
|
4920
|
+
})(before, after, []);
|
|
4921
|
+
return changes;
|
|
4922
|
+
}
|
|
4923
|
+
/** Whether `target` already holds what `change` would write. */
|
|
4924
|
+
function isApplied(target, change) {
|
|
4925
|
+
const parent = getByPath(target, change.path.slice(0, -1));
|
|
4926
|
+
if (parent === null || typeof parent !== "object") return change.deleted;
|
|
4927
|
+
const key = change.path[change.path.length - 1];
|
|
4928
|
+
if (!hasOwn(parent, key)) return change.deleted;
|
|
4929
|
+
return !change.deleted && deepEqual(parent[key], change.value);
|
|
4930
|
+
}
|
|
4931
|
+
function applyChange(target, change) {
|
|
4932
|
+
if (change.deleted) deleteByPath(target, change.path);
|
|
4933
|
+
else setByPath(target, change.path, change.value);
|
|
4874
4934
|
}
|
|
4875
4935
|
/**
|
|
4876
|
-
*
|
|
4877
|
-
*
|
|
4936
|
+
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
4937
|
+
* index) into `target`, another version of the same object or array.
|
|
4938
|
+
* Values that changed are written whole, as deep copies; for values that
|
|
4939
|
+
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
4940
|
+
* whether it did. Into an array, elements removed from the end are removed
|
|
4941
|
+
* at the same indices and elements added are appended, and changes at
|
|
4942
|
+
* indices `target` lacks are dropped: where `target` was resized, its
|
|
4943
|
+
* indices do not line up with `before`'s.
|
|
4878
4944
|
*/
|
|
4879
|
-
function
|
|
4880
|
-
|
|
4945
|
+
function mergeKeys(target, before, after, mergeNested) {
|
|
4946
|
+
const list = Array.isArray(target) ? target : void 0;
|
|
4947
|
+
for (const change of keyChanges(before, after, true)) {
|
|
4948
|
+
const { key } = change;
|
|
4949
|
+
if (change.kind === "deleted") if (list) list.length = Math.min(list.length, Number(key));
|
|
4950
|
+
else delete target[key];
|
|
4951
|
+
else if (list && change.kind === "added") list.push(deepClone(change.after));
|
|
4952
|
+
else if (list && Number(key) >= list.length) continue;
|
|
4953
|
+
else if (change.kind !== "nested" || !(mergeNested?.(key, change.before, change.after) || deepEqual(change.before, change.after))) target[key] = deepClone(change.after);
|
|
4954
|
+
}
|
|
4881
4955
|
}
|
|
4882
4956
|
//#endregion
|
|
4883
4957
|
//#region src/event-emitter.ts
|
|
@@ -4980,6 +5054,10 @@ function inOrder(op) {
|
|
|
4980
5054
|
function queued(op) {
|
|
4981
5055
|
return (...args) => inOrder(() => op(...args));
|
|
4982
5056
|
}
|
|
5057
|
+
/** An operation passing the save `saveId`, if it is stored, to `read`. */
|
|
5058
|
+
var readingSave = (read) => queued(async (saveId) => read(await (await getBackend()).getSave(saveId)));
|
|
5059
|
+
/** An operation passing the save a slot holds, if any, to `read`. */
|
|
5060
|
+
var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(ifid, slot)));
|
|
4983
5061
|
/**
|
|
4984
5062
|
* Store a new playthrough and make it the story's current one. Pass `id` when
|
|
4985
5063
|
* the caller has already switched to the playthrough (restart() does, so
|
|
@@ -5180,7 +5258,7 @@ function deserializePayload(payload) {
|
|
|
5180
5258
|
}
|
|
5181
5259
|
/** The live payload of a stored save, if there is one. */
|
|
5182
5260
|
var livePayload = (record) => record && deserializePayload(record.payload);
|
|
5183
|
-
|
|
5261
|
+
readingSave(livePayload);
|
|
5184
5262
|
/**
|
|
5185
5263
|
* Delete a save record. If the default slot or a named slot holds it, that
|
|
5186
5264
|
* slot is cleared as well (pointer and slot index), as `deleteSlotSave` would.
|
|
@@ -5336,7 +5414,7 @@ queued(hasQuickSaveNow);
|
|
|
5336
5414
|
async function hasQuickSaveNow(ifid, slot) {
|
|
5337
5415
|
return await slotRecord(ifid, slot) !== void 0;
|
|
5338
5416
|
}
|
|
5339
|
-
|
|
5417
|
+
readingSlot(livePayload);
|
|
5340
5418
|
/**
|
|
5341
5419
|
* Read the save held in a slot (the default slot when `slot` is omitted) for
|
|
5342
5420
|
* the running game to load, and make its playthrough the story's current one
|
|
@@ -5432,7 +5510,7 @@ function toExport(record) {
|
|
|
5432
5510
|
save: record
|
|
5433
5511
|
};
|
|
5434
5512
|
}
|
|
5435
|
-
var exportSave =
|
|
5513
|
+
var exportSave = readingSave(toExport);
|
|
5436
5514
|
/**
|
|
5437
5515
|
* Validate an export against the running story and build the record to store:
|
|
5438
5516
|
* a fresh save ID, and the playthrough record created if it doesn't exist yet.
|
|
@@ -5460,7 +5538,7 @@ function importSave(data, ifid) {
|
|
|
5460
5538
|
* Export the save held in a slot (default autosave slot when `slot` is omitted).
|
|
5461
5539
|
* Returns undefined if the slot is empty.
|
|
5462
5540
|
*/
|
|
5463
|
-
var exportSlotSave =
|
|
5541
|
+
var exportSlotSave = readingSlot(toExport);
|
|
5464
5542
|
/**
|
|
5465
5543
|
* Import an exported save into a slot (default autosave slot when `slot` is
|
|
5466
5544
|
* omitted), replacing any save already in that slot. Applies the same
|
|
@@ -7024,7 +7102,8 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
7024
7102
|
//#region src/components/StoryInterface.tsx
|
|
7025
7103
|
var DEFAULT_MARKUP = "<header class=\"story-menubar\">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}";
|
|
7026
7104
|
function StoryInterface() {
|
|
7027
|
-
const
|
|
7105
|
+
const { storyData } = useStoryFields("storyData");
|
|
7106
|
+
const overridePassage = storyData?.passages.get("StoryInterface");
|
|
7028
7107
|
const markup = overridePassage !== void 0 ? overridePassage.content : DEFAULT_MARKUP;
|
|
7029
7108
|
const nobr = overridePassage?.tags.includes("nobr") ?? false;
|
|
7030
7109
|
const rendered = useMemo(() => {
|
|
@@ -7124,7 +7203,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
7124
7203
|
const onCloseRef = useRef(onClose);
|
|
7125
7204
|
onCloseRef.current = onClose;
|
|
7126
7205
|
const stableOnClose = useCallback(() => onCloseRef.current(), []);
|
|
7127
|
-
const storyData =
|
|
7206
|
+
const { storyData } = useStoryFields("storyData");
|
|
7128
7207
|
const passage = passageName ? storyData?.passages.get(passageName) : void 0;
|
|
7129
7208
|
const markup = passage?.content ?? fallbackMarkup;
|
|
7130
7209
|
const nobr = passage?.tags.includes("nobr") ?? false;
|
|
@@ -7406,6 +7485,14 @@ var settings = {
|
|
|
7406
7485
|
};
|
|
7407
7486
|
//#endregion
|
|
7408
7487
|
//#region src/action-registry.ts
|
|
7488
|
+
/** The fields of a StoryAction that are left out when unset. */
|
|
7489
|
+
var OPTIONAL_ACTION_FIELDS = [
|
|
7490
|
+
"target",
|
|
7491
|
+
"variable",
|
|
7492
|
+
"options",
|
|
7493
|
+
"value",
|
|
7494
|
+
"disabled"
|
|
7495
|
+
];
|
|
7409
7496
|
/**
|
|
7410
7497
|
* The registrations holding each ID, oldest first; the newest is the one
|
|
7411
7498
|
* listed. Generated IDs don't collide, but author IDs can, and one control's
|
|
@@ -7488,11 +7575,7 @@ function useAction(opts) {
|
|
|
7488
7575
|
label: opts.label,
|
|
7489
7576
|
perform: (...args) => performRef.current(...args)
|
|
7490
7577
|
};
|
|
7491
|
-
if (opts
|
|
7492
|
-
if (opts.variable !== void 0) action.variable = opts.variable;
|
|
7493
|
-
if (opts.options !== void 0) action.options = opts.options;
|
|
7494
|
-
if (opts.value !== void 0) action.value = opts.value;
|
|
7495
|
-
if (opts.disabled !== void 0) action.disabled = opts.disabled;
|
|
7578
|
+
for (const field of OPTIONAL_ACTION_FIELDS) if (opts[field] !== void 0) Object.assign(action, { [field]: opts[field] });
|
|
7496
7579
|
return action;
|
|
7497
7580
|
};
|
|
7498
7581
|
const registration = useRef(null);
|
|
@@ -7707,6 +7790,12 @@ function readGroup(params, src, args) {
|
|
|
7707
7790
|
const rest = positional[restIndex];
|
|
7708
7791
|
args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
|
|
7709
7792
|
}
|
|
7793
|
+
/** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
|
|
7794
|
+
var DELAY_PARAMETER = {
|
|
7795
|
+
name: "delay",
|
|
7796
|
+
type: "delay",
|
|
7797
|
+
required: true
|
|
7798
|
+
};
|
|
7710
7799
|
/** Read `rawArgs` into the declared `parameters` (see above). */
|
|
7711
7800
|
function parseMacroArgs(rawArgs, parameters) {
|
|
7712
7801
|
const args = {};
|
|
@@ -7760,15 +7849,19 @@ function parseVarArgs(rawArgs) {
|
|
|
7760
7849
|
placeholder
|
|
7761
7850
|
};
|
|
7762
7851
|
}
|
|
7852
|
+
/** The parameters of an `{option}` sub-macro: its value. */
|
|
7853
|
+
var OPTION_PARAMETERS = [{
|
|
7854
|
+
name: "value",
|
|
7855
|
+
type: "string"
|
|
7856
|
+
}];
|
|
7763
7857
|
/**
|
|
7764
|
-
* Walk AST children to find {option} macro nodes, returning their
|
|
7858
|
+
* Walk AST children to find {option} macro nodes, returning their values.
|
|
7765
7859
|
*/
|
|
7766
7860
|
function extractOptions(children) {
|
|
7767
7861
|
const options = [];
|
|
7768
7862
|
for (const node of children) if (node.type === "macro" && node.name === "option") {
|
|
7769
|
-
const
|
|
7770
|
-
|
|
7771
|
-
options.push(stripped);
|
|
7863
|
+
const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
|
|
7864
|
+
options.push(value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, "$2"));
|
|
7772
7865
|
}
|
|
7773
7866
|
return options;
|
|
7774
7867
|
}
|
|
@@ -7822,13 +7915,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
7822
7915
|
mutate: (code) => executeMutation(code, getValues(), update),
|
|
7823
7916
|
update,
|
|
7824
7917
|
getValues,
|
|
7825
|
-
wrap: (content) =>
|
|
7826
|
-
if (className || id) return h("span", {
|
|
7827
|
-
id,
|
|
7828
|
-
class: className
|
|
7829
|
-
}, content);
|
|
7830
|
-
return h(Fragment, null, content);
|
|
7831
|
-
}
|
|
7918
|
+
wrap: (content) => wrapContent(className, id, content)
|
|
7832
7919
|
};
|
|
7833
7920
|
if (config.merged) {
|
|
7834
7921
|
ctx.merged = useMergedLocals();
|
|
@@ -7959,9 +8046,11 @@ function setOne(draft, name, value) {
|
|
|
7959
8046
|
if (key.includes(".")) setByPath(namespace, key.split("."), value);
|
|
7960
8047
|
else namespace[key] = value;
|
|
7961
8048
|
}
|
|
8049
|
+
/** A method calling the store action `name`, as the store holds it then. */
|
|
8050
|
+
function storeAction(name) {
|
|
8051
|
+
return ((...args) => useStoryStore.getState()[name](...args));
|
|
8052
|
+
}
|
|
7962
8053
|
function createStoryAPI() {
|
|
7963
|
-
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
7964
|
-
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
7965
8054
|
return {
|
|
7966
8055
|
get(name) {
|
|
7967
8056
|
const { isTransient, key } = parseName(name);
|
|
@@ -7981,52 +8070,19 @@ function createStoryAPI() {
|
|
|
7981
8070
|
for (const [k, v] of entries) setOne(draft, k, v);
|
|
7982
8071
|
});
|
|
7983
8072
|
},
|
|
7984
|
-
goto(
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
return useStoryStore.getState().save(slot, custom);
|
|
7998
|
-
},
|
|
7999
|
-
load(slot) {
|
|
8000
|
-
return useStoryStore.getState().load(slot);
|
|
8001
|
-
},
|
|
8002
|
-
hasSave(slot) {
|
|
8003
|
-
return useStoryStore.getState().hasSave(slot);
|
|
8004
|
-
},
|
|
8005
|
-
getSaveInfo(slot) {
|
|
8006
|
-
return useStoryStore.getState().getSaveInfo(slot);
|
|
8007
|
-
},
|
|
8008
|
-
listSaves() {
|
|
8009
|
-
return useStoryStore.getState().listSaves();
|
|
8010
|
-
},
|
|
8011
|
-
deleteSave(slot) {
|
|
8012
|
-
return useStoryStore.getState().deleteSave(slot);
|
|
8013
|
-
},
|
|
8014
|
-
exportSave(slot) {
|
|
8015
|
-
return useStoryStore.getState().exportSave(slot);
|
|
8016
|
-
},
|
|
8017
|
-
importSave(data, slot) {
|
|
8018
|
-
return useStoryStore.getState().importSave(data, slot);
|
|
8019
|
-
},
|
|
8020
|
-
visited: visits.count,
|
|
8021
|
-
hasVisited: visits.has,
|
|
8022
|
-
hasVisitedAny: visits.any,
|
|
8023
|
-
hasVisitedAll: visits.all,
|
|
8024
|
-
rendered: renders.count,
|
|
8025
|
-
hasRendered: renders.has,
|
|
8026
|
-
hasRenderedAny: renders.any,
|
|
8027
|
-
hasRenderedAll: renders.all,
|
|
8028
|
-
currentPassage,
|
|
8029
|
-
previousPassage,
|
|
8073
|
+
goto: storeAction("navigate"),
|
|
8074
|
+
back: storeAction("goBack"),
|
|
8075
|
+
forward: storeAction("goForward"),
|
|
8076
|
+
restart: storeAction("restart"),
|
|
8077
|
+
save: storeAction("save"),
|
|
8078
|
+
load: storeAction("load"),
|
|
8079
|
+
hasSave: storeAction("hasSave"),
|
|
8080
|
+
getSaveInfo: storeAction("getSaveInfo"),
|
|
8081
|
+
listSaves: storeAction("listSaves"),
|
|
8082
|
+
deleteSave: storeAction("deleteSave"),
|
|
8083
|
+
exportSave: storeAction("exportSave"),
|
|
8084
|
+
importSave: storeAction("importSave"),
|
|
8085
|
+
...historyQueries(() => useStoryStore.getState().visitCounts, () => useStoryStore.getState().renderCounts),
|
|
8030
8086
|
get title() {
|
|
8031
8087
|
return useStoryStore.getState().storyData?.name || "";
|
|
8032
8088
|
},
|
|
@@ -8072,15 +8128,9 @@ function createStoryAPI() {
|
|
|
8072
8128
|
estimateSupported: false
|
|
8073
8129
|
};
|
|
8074
8130
|
},
|
|
8075
|
-
clearGameData()
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
clearAllData() {
|
|
8079
|
-
return useStoryStore.getState().clearAllData();
|
|
8080
|
-
},
|
|
8081
|
-
deletePlaythrough(playthroughId) {
|
|
8082
|
-
return useStoryStore.getState().deletePlaythrough(playthroughId);
|
|
8083
|
-
},
|
|
8131
|
+
clearGameData: storeAction("clearGameData"),
|
|
8132
|
+
clearAllData: storeAction("clearAllData"),
|
|
8133
|
+
deletePlaythrough: storeAction("deletePlaythrough"),
|
|
8084
8134
|
get backend() {
|
|
8085
8135
|
return getBackendType();
|
|
8086
8136
|
}
|
|
@@ -8488,26 +8538,51 @@ function astContainsChildren(nodes) {
|
|
|
8488
8538
|
}
|
|
8489
8539
|
//#endregion
|
|
8490
8540
|
//#region src/components/macros/Widget.tsx
|
|
8491
|
-
/**
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8541
|
+
/** A {widget} definition's name, then its `@` parameters. */
|
|
8542
|
+
var WIDGET_PARAMETERS = [{
|
|
8543
|
+
name: "name",
|
|
8544
|
+
type: "text",
|
|
8545
|
+
required: true
|
|
8546
|
+
}, {
|
|
8547
|
+
name: "parameters",
|
|
8548
|
+
type: "expression"
|
|
8549
|
+
}];
|
|
8550
|
+
/**
|
|
8551
|
+
* The widget a {widget} definition's arguments declare: its name and its
|
|
8552
|
+
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
8553
|
+
* Other words, such as `$name`, are not parameters.
|
|
8554
|
+
*/
|
|
8555
|
+
function widgetDef({ name = "", parameters = "" }) {
|
|
8498
8556
|
return {
|
|
8499
|
-
name
|
|
8500
|
-
params:
|
|
8557
|
+
name,
|
|
8558
|
+
params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
|
|
8501
8559
|
};
|
|
8502
8560
|
}
|
|
8561
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
8562
|
+
function parseWidgetDef(rawArgs) {
|
|
8563
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
8564
|
+
}
|
|
8565
|
+
/**
|
|
8566
|
+
* Register the widget a definition declares, with its body. Widgets whose
|
|
8567
|
+
* body renders {@children} take a closing tag, so they are registered as
|
|
8568
|
+
* block macros too: passages parsed later nest their content. A parameter
|
|
8569
|
+
* that no namespace can hold throws (see registerWidget), registering
|
|
8570
|
+
* nothing.
|
|
8571
|
+
*/
|
|
8572
|
+
function registerWidgetDef({ name, params }, body) {
|
|
8573
|
+
const isBlock = astContainsChildren(body);
|
|
8574
|
+
registerWidget(name, body, params, isBlock);
|
|
8575
|
+
if (isBlock) registerBlockMacro(name);
|
|
8576
|
+
}
|
|
8503
8577
|
defineMacro({
|
|
8504
8578
|
name: "widget",
|
|
8505
8579
|
block: true,
|
|
8506
|
-
|
|
8580
|
+
parameters: WIDGET_PARAMETERS,
|
|
8581
|
+
render({ children = [] }, ctx) {
|
|
8507
8582
|
let parsed;
|
|
8508
8583
|
let error;
|
|
8509
8584
|
try {
|
|
8510
|
-
const def =
|
|
8585
|
+
const def = widgetDef(ctx.args);
|
|
8511
8586
|
for (const param of def.params) checkVariableName(param.slice(1), param);
|
|
8512
8587
|
parsed = def;
|
|
8513
8588
|
} catch (err) {
|
|
@@ -8520,10 +8595,7 @@ defineMacro({
|
|
|
8520
8595
|
const childrenKey = JSON.stringify(children);
|
|
8521
8596
|
const paramsKey = params.join(",");
|
|
8522
8597
|
ctx.hooks.useLayoutEffect(() => {
|
|
8523
|
-
if (
|
|
8524
|
-
const isBlock = astContainsChildren(children);
|
|
8525
|
-
registerWidget(name, children, params, isBlock);
|
|
8526
|
-
if (isBlock) registerBlockMacro(name);
|
|
8598
|
+
if (parsed) registerWidgetDef(parsed, children);
|
|
8527
8599
|
}, [
|
|
8528
8600
|
name,
|
|
8529
8601
|
childrenKey,
|
|
@@ -8562,9 +8634,7 @@ defineMacro({
|
|
|
8562
8634
|
merged: true,
|
|
8563
8635
|
render({ rawArgs }, ctx) {
|
|
8564
8636
|
try {
|
|
8565
|
-
|
|
8566
|
-
const display = result == null ? "" : String(result);
|
|
8567
|
-
return ctx.wrap(display);
|
|
8637
|
+
return ctx.wrap(display(ctx.evaluate(rawArgs)));
|
|
8568
8638
|
} catch (err) {
|
|
8569
8639
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
8570
8640
|
macro: "print",
|
|
@@ -8573,8 +8643,7 @@ defineMacro({
|
|
|
8573
8643
|
}
|
|
8574
8644
|
},
|
|
8575
8645
|
text({ rawArgs }, ctx) {
|
|
8576
|
-
|
|
8577
|
-
return result == null ? "" : String(result);
|
|
8646
|
+
return display(ctx.evaluate(rawArgs));
|
|
8578
8647
|
}
|
|
8579
8648
|
});
|
|
8580
8649
|
//#endregion
|
|
@@ -8744,40 +8813,50 @@ defineMacro({
|
|
|
8744
8813
|
}
|
|
8745
8814
|
});
|
|
8746
8815
|
//#endregion
|
|
8816
|
+
//#region src/components/macros/branches.ts
|
|
8817
|
+
/**
|
|
8818
|
+
* The branch a conditional block ({if}, {switch}) shows: the first of
|
|
8819
|
+
* `branches` whose argument `matches`, else the last bare one ({else},
|
|
8820
|
+
* {default}), else null. Throws what `matches` throws.
|
|
8821
|
+
*/
|
|
8822
|
+
function selectBranch(branches, matches) {
|
|
8823
|
+
let fallback = null;
|
|
8824
|
+
for (const branch of branches) if (branch.rawArgs === "") fallback = branch;
|
|
8825
|
+
else if (matches(branch.rawArgs)) return branch;
|
|
8826
|
+
return fallback;
|
|
8827
|
+
}
|
|
8828
|
+
//#endregion
|
|
8747
8829
|
//#region src/components/macros/If.tsx
|
|
8830
|
+
/**
|
|
8831
|
+
* The {if}/{elseif}/{else} branch whose condition holds, else the {else}
|
|
8832
|
+
* branch, else null: the chain ends at its first bare branch. Throws what a
|
|
8833
|
+
* condition throws.
|
|
8834
|
+
*/
|
|
8835
|
+
function selectIfBranch(branches, evaluate) {
|
|
8836
|
+
const end = branches.findIndex((branch) => branch.rawArgs === "");
|
|
8837
|
+
return selectBranch(end < 0 ? branches : branches.slice(0, end + 1), (expr) => Boolean(evaluate(expr)));
|
|
8838
|
+
}
|
|
8748
8839
|
defineMacro({
|
|
8749
8840
|
name: "if",
|
|
8750
8841
|
block: true,
|
|
8751
8842
|
interpolate: true,
|
|
8752
8843
|
merged: true,
|
|
8753
8844
|
render({ branches = [] }, ctx) {
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
children
|
|
8845
|
+
let branch;
|
|
8846
|
+
try {
|
|
8847
|
+
branch = selectIfBranch(branches, ctx.evaluate);
|
|
8848
|
+
} catch (err) {
|
|
8849
|
+
return /* @__PURE__ */ jsx(MacroError, {
|
|
8850
|
+
macro: "if",
|
|
8851
|
+
error: err
|
|
8762
8852
|
});
|
|
8763
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
8764
|
-
}
|
|
8765
|
-
for (const branch of branches) {
|
|
8766
|
-
if (branch.rawArgs === "") return renderBranch(branch);
|
|
8767
|
-
try {
|
|
8768
|
-
if (ctx.evaluate(branch.rawArgs)) return renderBranch(branch);
|
|
8769
|
-
} catch (err) {
|
|
8770
|
-
return /* @__PURE__ */ jsx(MacroError, {
|
|
8771
|
-
macro: "if",
|
|
8772
|
-
error: err
|
|
8773
|
-
});
|
|
8774
|
-
}
|
|
8775
8853
|
}
|
|
8776
|
-
return null;
|
|
8854
|
+
if (!branch) return null;
|
|
8855
|
+
return wrapContent(ctx.resolve(branch.className), ctx.resolve(branch.id), ctx.renderNodes(branch.children));
|
|
8777
8856
|
},
|
|
8778
8857
|
text({ branches = [] }, ctx) {
|
|
8779
|
-
|
|
8780
|
-
return "";
|
|
8858
|
+
const branch = selectIfBranch(branches, ctx.evaluate);
|
|
8859
|
+
return branch ? ctx.renderText(branch.children) : "";
|
|
8781
8860
|
}
|
|
8782
8861
|
});
|
|
8783
8862
|
//#endregion
|
|
@@ -8973,11 +9052,7 @@ defineMacro({
|
|
|
8973
9052
|
name: "repeat",
|
|
8974
9053
|
block: true,
|
|
8975
9054
|
interpolate: true,
|
|
8976
|
-
parameters: [
|
|
8977
|
-
name: "delay",
|
|
8978
|
-
type: "delay",
|
|
8979
|
-
required: true
|
|
8980
|
-
}],
|
|
9055
|
+
parameters: [DELAY_PARAMETER],
|
|
8981
9056
|
render({ children = [] }, ctx) {
|
|
8982
9057
|
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
8983
9058
|
const delay = ctx.args.delay ?? 0;
|
|
@@ -9144,14 +9219,8 @@ defineMacro({
|
|
|
9144
9219
|
* The first {case} branch whose value equals `value`, else the {default}
|
|
9145
9220
|
* branch, else null. Throws what a case expression throws.
|
|
9146
9221
|
*/
|
|
9147
|
-
function
|
|
9148
|
-
|
|
9149
|
-
for (let i = 1; i < branches.length; i++) {
|
|
9150
|
-
const branch = branches[i];
|
|
9151
|
-
if (branch.rawArgs === "") defaultBranch = branch;
|
|
9152
|
-
else if (value === evaluate(branch.rawArgs)) return branch;
|
|
9153
|
-
}
|
|
9154
|
-
return defaultBranch;
|
|
9222
|
+
function selectCase(value, branches, evaluate) {
|
|
9223
|
+
return selectBranch(branches.slice(1), (expr) => value === evaluate(expr));
|
|
9155
9224
|
}
|
|
9156
9225
|
defineMacro({
|
|
9157
9226
|
name: "switch",
|
|
@@ -9174,7 +9243,7 @@ defineMacro({
|
|
|
9174
9243
|
}
|
|
9175
9244
|
let branch;
|
|
9176
9245
|
try {
|
|
9177
|
-
branch =
|
|
9246
|
+
branch = selectCase(switchValue, branches, ctx.evaluate);
|
|
9178
9247
|
} catch (err) {
|
|
9179
9248
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
9180
9249
|
macro: "case",
|
|
@@ -9184,21 +9253,24 @@ defineMacro({
|
|
|
9184
9253
|
return branch && /* @__PURE__ */ jsx(Fragment$1, { children: ctx.renderNodes(branch.children) });
|
|
9185
9254
|
},
|
|
9186
9255
|
text({ rawArgs, branches = [] }, ctx) {
|
|
9187
|
-
const branch =
|
|
9256
|
+
const branch = selectCase(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
9188
9257
|
return branch ? ctx.renderText(branch.children) : "";
|
|
9189
9258
|
}
|
|
9190
9259
|
});
|
|
9191
9260
|
//#endregion
|
|
9192
9261
|
//#region src/components/macros/Timed.tsx
|
|
9262
|
+
/** The parameters of {timed} and of each {next}: the delay before it shows. */
|
|
9263
|
+
var TIMED_PARAMETERS = [DELAY_PARAMETER];
|
|
9193
9264
|
defineMacro({
|
|
9194
9265
|
name: "timed",
|
|
9195
9266
|
subMacros: ["next"],
|
|
9196
9267
|
interpolate: true,
|
|
9268
|
+
parameters: TIMED_PARAMETERS,
|
|
9197
9269
|
render({ branches = [] }, ctx) {
|
|
9198
9270
|
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
9199
9271
|
const sections = useMemo(() => {
|
|
9200
9272
|
return branches.map((branch) => ({
|
|
9201
|
-
delay: branch.rawArgs
|
|
9273
|
+
delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
|
|
9202
9274
|
nodes: branch.children,
|
|
9203
9275
|
className: branch.className,
|
|
9204
9276
|
id: branch.id
|
|
@@ -9216,15 +9288,7 @@ defineMacro({
|
|
|
9216
9288
|
}, [visibleIndex, sections]);
|
|
9217
9289
|
if (visibleIndex < 0) return ctx.wrap(null);
|
|
9218
9290
|
const section = sections[visibleIndex];
|
|
9219
|
-
|
|
9220
|
-
const sectionClass = ctx.resolve(section.className);
|
|
9221
|
-
const sectionId = ctx.resolve(section.id);
|
|
9222
|
-
const inner = sectionClass || sectionId ? /* @__PURE__ */ jsx("span", {
|
|
9223
|
-
id: sectionId,
|
|
9224
|
-
class: sectionClass,
|
|
9225
|
-
children: content
|
|
9226
|
-
}) : content;
|
|
9227
|
-
return ctx.wrap(inner);
|
|
9291
|
+
return ctx.wrap(wrapContent(ctx.resolve(section.className), ctx.resolve(section.id), ctx.renderNodes(section.nodes)));
|
|
9228
9292
|
}
|
|
9229
9293
|
});
|
|
9230
9294
|
//#endregion
|
|
@@ -9245,11 +9309,7 @@ defineMacro({
|
|
|
9245
9309
|
name: "type",
|
|
9246
9310
|
block: true,
|
|
9247
9311
|
interpolate: true,
|
|
9248
|
-
parameters: [
|
|
9249
|
-
name: "delay",
|
|
9250
|
-
type: "delay",
|
|
9251
|
-
required: true
|
|
9252
|
-
}],
|
|
9312
|
+
parameters: [DELAY_PARAMETER],
|
|
9253
9313
|
render({ children = [] }, ctx) {
|
|
9254
9314
|
const { useState, useEffect, useRef } = ctx.hooks;
|
|
9255
9315
|
const speed = ctx.args.delay ?? 0;
|
|
@@ -9864,7 +9924,7 @@ defineMacro({
|
|
|
9864
9924
|
required: true
|
|
9865
9925
|
}],
|
|
9866
9926
|
render(_props, ctx) {
|
|
9867
|
-
const storyData =
|
|
9927
|
+
const { storyData } = useStoryFields("storyData");
|
|
9868
9928
|
const { inline } = ctx.args;
|
|
9869
9929
|
const passageName = evaluatePassageName(ctx.args.passage, ctx.evaluate);
|
|
9870
9930
|
const passage = storyData?.passages.get(passageName);
|
|
@@ -9931,6 +9991,26 @@ defineMacro({
|
|
|
9931
9991
|
});
|
|
9932
9992
|
//#endregion
|
|
9933
9993
|
//#region src/components/macros/input-macro.ts
|
|
9994
|
+
/** The story variable a `storeVar` macro binds: its first parameter. */
|
|
9995
|
+
var VARIABLE_PARAMETER = {
|
|
9996
|
+
name: "variable",
|
|
9997
|
+
type: "variable",
|
|
9998
|
+
required: true
|
|
9999
|
+
};
|
|
10000
|
+
/**
|
|
10001
|
+
* Register the automation action of a macro bound to a story variable: keyed
|
|
10002
|
+
* by the variable (or `key`), with the variable's value unless `action`
|
|
10003
|
+
* gives another.
|
|
10004
|
+
*/
|
|
10005
|
+
function useVariableAction(ctx, action) {
|
|
10006
|
+
return ctx.useAction({
|
|
10007
|
+
key: `$${ctx.varName}`,
|
|
10008
|
+
authorId: ctx.id,
|
|
10009
|
+
variable: ctx.varName,
|
|
10010
|
+
value: ctx.value,
|
|
10011
|
+
...action
|
|
10012
|
+
});
|
|
10013
|
+
}
|
|
9934
10014
|
/**
|
|
9935
10015
|
* Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
|
|
9936
10016
|
* `type` (or, without one, a `<textarea>`) bound to the story variable.
|
|
@@ -9942,30 +10022,22 @@ function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? St
|
|
|
9942
10022
|
defineMacro({
|
|
9943
10023
|
name,
|
|
9944
10024
|
storeVar: true,
|
|
9945
|
-
parameters: [{
|
|
9946
|
-
name: "variable",
|
|
9947
|
-
type: "variable",
|
|
9948
|
-
required: true
|
|
9949
|
-
}, {
|
|
10025
|
+
parameters: [VARIABLE_PARAMETER, {
|
|
9950
10026
|
name: "placeholder",
|
|
9951
10027
|
type: "string"
|
|
9952
10028
|
}],
|
|
9953
10029
|
render(_props, ctx) {
|
|
9954
10030
|
const placeholder = ctx.args.placeholder ?? "";
|
|
9955
|
-
ctx
|
|
10031
|
+
useVariableAction(ctx, {
|
|
9956
10032
|
type: name,
|
|
9957
|
-
key: `$${ctx.varName}`,
|
|
9958
|
-
authorId: ctx.id,
|
|
9959
10033
|
label: placeholder || ctx.varName,
|
|
9960
|
-
variable: ctx.varName,
|
|
9961
|
-
value: ctx.value,
|
|
9962
10034
|
perform: (v) => ctx.setValue(toValue(v))
|
|
9963
10035
|
});
|
|
9964
10036
|
return h(type ? "input" : "textarea", {
|
|
9965
10037
|
type,
|
|
9966
10038
|
id: ctx.id,
|
|
9967
10039
|
class: ctx.cls,
|
|
9968
|
-
value:
|
|
10040
|
+
value: display(ctx.value),
|
|
9969
10041
|
placeholder,
|
|
9970
10042
|
onInput: (e) => ctx.setValue(toValue(e.target.value))
|
|
9971
10043
|
});
|
|
@@ -9986,22 +10058,15 @@ defineInputMacro("textarea");
|
|
|
9986
10058
|
defineMacro({
|
|
9987
10059
|
name: "checkbox",
|
|
9988
10060
|
storeVar: true,
|
|
9989
|
-
parameters: [{
|
|
9990
|
-
name: "variable",
|
|
9991
|
-
type: "variable",
|
|
9992
|
-
required: true
|
|
9993
|
-
}, {
|
|
10061
|
+
parameters: [VARIABLE_PARAMETER, {
|
|
9994
10062
|
name: "label",
|
|
9995
10063
|
type: "text"
|
|
9996
10064
|
}],
|
|
9997
10065
|
render(_props, ctx) {
|
|
9998
10066
|
const label = ctx.args.label ?? "";
|
|
9999
|
-
ctx
|
|
10067
|
+
useVariableAction(ctx, {
|
|
10000
10068
|
type: "checkbox",
|
|
10001
|
-
key: `$${ctx.varName}`,
|
|
10002
|
-
authorId: ctx.id,
|
|
10003
10069
|
label: label || ctx.varName || "",
|
|
10004
|
-
variable: ctx.varName,
|
|
10005
10070
|
value: !!ctx.value,
|
|
10006
10071
|
perform: (v) => ctx.setValue(v !== void 0 ? !!v : !ctx.value)
|
|
10007
10072
|
});
|
|
@@ -10022,11 +10087,7 @@ defineMacro({
|
|
|
10022
10087
|
name: "radiobutton",
|
|
10023
10088
|
storeVar: true,
|
|
10024
10089
|
parameters: [
|
|
10025
|
-
|
|
10026
|
-
name: "variable",
|
|
10027
|
-
type: "variable",
|
|
10028
|
-
required: true
|
|
10029
|
-
},
|
|
10090
|
+
VARIABLE_PARAMETER,
|
|
10030
10091
|
{
|
|
10031
10092
|
name: "value",
|
|
10032
10093
|
type: "text",
|
|
@@ -10040,13 +10101,10 @@ defineMacro({
|
|
|
10040
10101
|
render(_props, ctx) {
|
|
10041
10102
|
const radioValue = ctx.args.value ?? "";
|
|
10042
10103
|
const label = ctx.args.label ?? "";
|
|
10043
|
-
ctx
|
|
10104
|
+
useVariableAction(ctx, {
|
|
10044
10105
|
type: "radiobutton",
|
|
10045
10106
|
key: `$${ctx.varName}:${radioValue}`,
|
|
10046
|
-
authorId: ctx.id,
|
|
10047
10107
|
label: label || radioValue,
|
|
10048
|
-
variable: ctx.varName,
|
|
10049
|
-
value: ctx.value,
|
|
10050
10108
|
perform: () => ctx.setValue(radioValue)
|
|
10051
10109
|
});
|
|
10052
10110
|
return /* @__PURE__ */ jsxs("label", {
|
|
@@ -10067,21 +10125,13 @@ defineMacro({
|
|
|
10067
10125
|
name: "listbox",
|
|
10068
10126
|
subMacros: ["option"],
|
|
10069
10127
|
storeVar: true,
|
|
10070
|
-
parameters: [
|
|
10071
|
-
name: "variable",
|
|
10072
|
-
type: "variable",
|
|
10073
|
-
required: true
|
|
10074
|
-
}],
|
|
10128
|
+
parameters: [VARIABLE_PARAMETER],
|
|
10075
10129
|
render({ children = [] }, ctx) {
|
|
10076
10130
|
const options = ctx.extractOptions(children);
|
|
10077
|
-
ctx
|
|
10131
|
+
useVariableAction(ctx, {
|
|
10078
10132
|
type: "listbox",
|
|
10079
|
-
key: `$${ctx.varName}`,
|
|
10080
|
-
authorId: ctx.id,
|
|
10081
10133
|
label: ctx.varName,
|
|
10082
|
-
variable: ctx.varName,
|
|
10083
10134
|
options,
|
|
10084
|
-
value: ctx.value,
|
|
10085
10135
|
perform: (v) => {
|
|
10086
10136
|
if (v !== void 0) ctx.setValue(String(v));
|
|
10087
10137
|
}
|
|
@@ -10089,7 +10139,7 @@ defineMacro({
|
|
|
10089
10139
|
return /* @__PURE__ */ jsx("select", {
|
|
10090
10140
|
id: ctx.id,
|
|
10091
10141
|
class: ctx.cls,
|
|
10092
|
-
value:
|
|
10142
|
+
value: display(ctx.value),
|
|
10093
10143
|
onChange: (e) => ctx.setValue(e.target.value),
|
|
10094
10144
|
children: options.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
10095
10145
|
value: opt,
|
|
@@ -10104,11 +10154,7 @@ defineMacro({
|
|
|
10104
10154
|
name: "cycle",
|
|
10105
10155
|
block: true,
|
|
10106
10156
|
storeVar: true,
|
|
10107
|
-
parameters: [
|
|
10108
|
-
name: "variable",
|
|
10109
|
-
type: "variable",
|
|
10110
|
-
required: true
|
|
10111
|
-
}],
|
|
10157
|
+
parameters: [VARIABLE_PARAMETER],
|
|
10112
10158
|
render({ children = [] }, ctx) {
|
|
10113
10159
|
const options = ctx.extractOptions(children);
|
|
10114
10160
|
const handleClick = () => {
|
|
@@ -10117,14 +10163,10 @@ defineMacro({
|
|
|
10117
10163
|
const nextIndex = (options.indexOf(String(current)) + 1) % options.length;
|
|
10118
10164
|
ctx.setValue(options[nextIndex]);
|
|
10119
10165
|
};
|
|
10120
|
-
ctx
|
|
10166
|
+
useVariableAction(ctx, {
|
|
10121
10167
|
type: "cycle",
|
|
10122
|
-
key: `$${ctx.varName}`,
|
|
10123
|
-
authorId: ctx.id,
|
|
10124
10168
|
label: ctx.value == null ? options[0] || "" : String(ctx.value),
|
|
10125
|
-
variable: ctx.varName,
|
|
10126
10169
|
options,
|
|
10127
|
-
value: ctx.value,
|
|
10128
10170
|
perform: (v) => {
|
|
10129
10171
|
if (v !== void 0) ctx.setValue(v);
|
|
10130
10172
|
else handleClick();
|
|
@@ -10170,6 +10212,20 @@ function parsePassage(passage) {
|
|
|
10170
10212
|
function renderPassageContent(passage) {
|
|
10171
10213
|
return renderNodes(parsePassage(passage), passage.tags.includes("nobr") ? { nobr: true } : void 0);
|
|
10172
10214
|
}
|
|
10215
|
+
/**
|
|
10216
|
+
* The content of a special passage shown with every passage (PassageHeader,
|
|
10217
|
+
* PassageFooter, PassageDone), or null. One that fails to render is logged
|
|
10218
|
+
* and left out, so it can't take the passage down with it.
|
|
10219
|
+
*/
|
|
10220
|
+
function renderSpecialPassage(passage) {
|
|
10221
|
+
if (!passage) return null;
|
|
10222
|
+
try {
|
|
10223
|
+
return renderPassageContent(passage);
|
|
10224
|
+
} catch (err) {
|
|
10225
|
+
console.error(`spindle: Error in ${passage.name}:`, err);
|
|
10226
|
+
return null;
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10173
10229
|
var CODE_PASSAGES = new Set([
|
|
10174
10230
|
"PassageReady",
|
|
10175
10231
|
"PassageHeader",
|
|
@@ -10177,7 +10233,7 @@ var CODE_PASSAGES = new Set([
|
|
|
10177
10233
|
"PassageDone"
|
|
10178
10234
|
]);
|
|
10179
10235
|
function Passage({ passage, dataTransition, navigationId }) {
|
|
10180
|
-
const storyData =
|
|
10236
|
+
const { storyData } = useStoryFields("storyData");
|
|
10181
10237
|
const isCodePassage = CODE_PASSAGES.has(passage.name);
|
|
10182
10238
|
const [doneReady, setDoneReady] = useState(false);
|
|
10183
10239
|
const content = useMemo(() => {
|
|
@@ -10197,40 +10253,17 @@ function Passage({ passage, dataTransition, navigationId }) {
|
|
|
10197
10253
|
});
|
|
10198
10254
|
}
|
|
10199
10255
|
}, [passage.content, passage.name]);
|
|
10200
|
-
const
|
|
10201
|
-
const
|
|
10202
|
-
const
|
|
10203
|
-
const
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
return renderPassageContent(headerPassage);
|
|
10207
|
-
} catch (err) {
|
|
10208
|
-
console.error("spindle: Error in PassageHeader:", err);
|
|
10209
|
-
return null;
|
|
10210
|
-
}
|
|
10211
|
-
}, [headerPassage?.content]);
|
|
10212
|
-
const footerContent = useMemo(() => {
|
|
10213
|
-
if (!footerPassage) return null;
|
|
10214
|
-
try {
|
|
10215
|
-
return renderPassageContent(footerPassage);
|
|
10216
|
-
} catch (err) {
|
|
10217
|
-
console.error("spindle: Error in PassageFooter:", err);
|
|
10218
|
-
return null;
|
|
10219
|
-
}
|
|
10220
|
-
}, [footerPassage?.content]);
|
|
10256
|
+
const special = (name) => isCodePassage ? void 0 : storyData?.passages.get(name);
|
|
10257
|
+
const headerPassage = special("PassageHeader");
|
|
10258
|
+
const footerPassage = special("PassageFooter");
|
|
10259
|
+
const donePassage = special("PassageDone");
|
|
10260
|
+
const headerContent = useMemo(() => renderSpecialPassage(headerPassage), [headerPassage?.content]);
|
|
10261
|
+
const footerContent = useMemo(() => renderSpecialPassage(footerPassage), [footerPassage?.content]);
|
|
10221
10262
|
useEffect(() => {
|
|
10222
10263
|
if (donePassage) setDoneReady(true);
|
|
10223
10264
|
return () => setDoneReady(false);
|
|
10224
10265
|
}, [passage.name]);
|
|
10225
|
-
const doneContent = useMemo(() =>
|
|
10226
|
-
if (!doneReady || !donePassage) return null;
|
|
10227
|
-
try {
|
|
10228
|
-
return renderPassageContent(donePassage);
|
|
10229
|
-
} catch (err) {
|
|
10230
|
-
console.error("spindle: Error in PassageDone:", err);
|
|
10231
|
-
return null;
|
|
10232
|
-
}
|
|
10233
|
-
}, [doneReady, donePassage?.content]);
|
|
10266
|
+
const doneContent = useMemo(() => doneReady ? renderSpecialPassage(donePassage) : null, [doneReady, donePassage?.content]);
|
|
10234
10267
|
const elRef = useRef(null);
|
|
10235
10268
|
useLayoutEffect(() => {
|
|
10236
10269
|
if (navigationId !== void 0) markPassageRendered(navigationId);
|
|
@@ -10631,16 +10664,12 @@ function boot() {
|
|
|
10631
10664
|
for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
|
|
10632
10665
|
const widgetAST = buildAST(tokenize(passage.content));
|
|
10633
10666
|
for (const node of widgetAST) if (node.type === "macro" && node.name === "widget" && node.rawArgs) {
|
|
10634
|
-
const
|
|
10635
|
-
const children = node.children;
|
|
10636
|
-
const isBlock = astContainsChildren(children);
|
|
10667
|
+
const def = parseWidgetDef(node.rawArgs);
|
|
10637
10668
|
try {
|
|
10638
|
-
|
|
10669
|
+
registerWidgetDef(def, node.children);
|
|
10639
10670
|
} catch (err) {
|
|
10640
|
-
console.error(`spindle: widget "${
|
|
10641
|
-
continue;
|
|
10671
|
+
console.error(`spindle: widget "${def.name}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`);
|
|
10642
10672
|
}
|
|
10643
|
-
if (isBlock) registerBlockMacro(widgetName);
|
|
10644
10673
|
}
|
|
10645
10674
|
}
|
|
10646
10675
|
let prevNavigationId = useStoryStore.getState().navigationId;
|