@rohal12/spindle 0.59.0 → 0.59.2
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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +360 -131
- package/dist/pkg/story-variables.js +21 -13
- package/package.json +1 -1
- package/src/automation/runner.ts +2 -1
- package/src/code-check.ts +11 -1
- package/src/components/PassageDialog.tsx +4 -0
- package/src/components/macros/MacroLink.tsx +2 -1
- package/src/components/macros/Meter.tsx +5 -1
- package/src/components/macros/Type.tsx +3 -1
- package/src/components/macros/macro-args.ts +5 -4
- package/src/execute-mutation.ts +118 -18
- package/src/saves/save-manager.ts +4 -3
- package/src/saves/storage.ts +30 -2
- package/src/store.ts +32 -9
- package/src/story-variables.ts +38 -65
- package/src/structural.ts +212 -24
- package/src/triggers.ts +22 -2
- package/src/utils/uuid.ts +18 -0
package/dist/pkg/headless.js
CHANGED
|
@@ -32,6 +32,26 @@ function create() {
|
|
|
32
32
|
return (initializer) => bindPreact(createStore()(initializer));
|
|
33
33
|
}
|
|
34
34
|
//#endregion
|
|
35
|
+
//#region src/utils/uuid.ts
|
|
36
|
+
/**
|
|
37
|
+
* A random UUID (v4). `crypto.randomUUID` exists only in secure contexts, so
|
|
38
|
+
* on a plain HTTP origin the ID is built from `crypto.getRandomValues`.
|
|
39
|
+
*/
|
|
40
|
+
function randomUUID() {
|
|
41
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
42
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
43
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
44
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
45
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
46
|
+
return [
|
|
47
|
+
hex.slice(0, 4).join(""),
|
|
48
|
+
hex.slice(4, 6).join(""),
|
|
49
|
+
hex.slice(6, 8).join(""),
|
|
50
|
+
hex.slice(8, 10).join(""),
|
|
51
|
+
hex.slice(10).join("")
|
|
52
|
+
].join("-");
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
35
55
|
//#region src/markup/tokens.ts
|
|
36
56
|
/** Variable sigils: story ($), temporary (_), local (@), transient (%). */
|
|
37
57
|
var SIGIL_SCOPES$1 = {
|
|
@@ -13061,7 +13081,14 @@ function createIDBBackend() {
|
|
|
13061
13081
|
for (const [index, path] of Object.entries(indexes)) store.createIndex(index, path, { unique: false });
|
|
13062
13082
|
}
|
|
13063
13083
|
};
|
|
13064
|
-
req.onsuccess = () =>
|
|
13084
|
+
req.onsuccess = () => {
|
|
13085
|
+
const db = req.result;
|
|
13086
|
+
db.onversionchange = () => {
|
|
13087
|
+
db.close();
|
|
13088
|
+
dbPromise = null;
|
|
13089
|
+
};
|
|
13090
|
+
resolve(db);
|
|
13091
|
+
};
|
|
13065
13092
|
req.onerror = () => reject(req.error);
|
|
13066
13093
|
});
|
|
13067
13094
|
return dbPromise;
|
|
@@ -13080,7 +13107,15 @@ function createIDBBackend() {
|
|
|
13080
13107
|
const rows = name === "meta";
|
|
13081
13108
|
/** Run one request in a transaction of its own. */
|
|
13082
13109
|
const request = async (mode, make) => {
|
|
13083
|
-
|
|
13110
|
+
const tx = (await openDB()).transaction(name, mode);
|
|
13111
|
+
const result = idbReq(make(tx.objectStore(name)));
|
|
13112
|
+
if (mode === "readonly") return result;
|
|
13113
|
+
const committed = new Promise((resolve, reject) => {
|
|
13114
|
+
tx.oncomplete = () => resolve();
|
|
13115
|
+
tx.onabort = () => reject(tx.error ?? new DOMException("Transaction aborted", "AbortError"));
|
|
13116
|
+
});
|
|
13117
|
+
const [value] = await Promise.all([result, committed]);
|
|
13118
|
+
return value;
|
|
13084
13119
|
};
|
|
13085
13120
|
return {
|
|
13086
13121
|
async get(key) {
|
|
@@ -13109,6 +13144,7 @@ function createIDBBackend() {
|
|
|
13109
13144
|
const req = indexedDB.deleteDatabase(IDB_DB_NAME);
|
|
13110
13145
|
req.onsuccess = () => resolve();
|
|
13111
13146
|
req.onerror = () => reject(req.error);
|
|
13147
|
+
req.onblocked = () => reject(/* @__PURE__ */ new Error("Cannot clear the saved data: another tab or window of this site still has it open. Close the other tabs and try again."));
|
|
13112
13148
|
});
|
|
13113
13149
|
});
|
|
13114
13150
|
}
|
|
@@ -13319,15 +13355,22 @@ var ERROR_HIDDEN_KEYS = [
|
|
|
13319
13355
|
"errors"
|
|
13320
13356
|
];
|
|
13321
13357
|
function deepClone(value, options = {}) {
|
|
13322
|
-
const seen = /* @__PURE__ */ new Map();
|
|
13358
|
+
const seen = options.seen ?? /* @__PURE__ */ new Map();
|
|
13323
13359
|
/** Record `copy` as the copy of `obj` before its contents are copied. */
|
|
13324
13360
|
const keep = (obj, copy) => {
|
|
13325
13361
|
seen.set(obj, copy);
|
|
13326
13362
|
return copy;
|
|
13327
13363
|
};
|
|
13328
|
-
/**
|
|
13364
|
+
/**
|
|
13365
|
+
* Copy the own (enumerable) keys of `obj` into `copy`, symbol keys too: a
|
|
13366
|
+
* save refuses them, which a copy must not hide.
|
|
13367
|
+
*/
|
|
13329
13368
|
function copyKeys(obj, copy) {
|
|
13330
13369
|
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
13370
|
+
for (const sym of Object.getOwnPropertySymbols(obj)) {
|
|
13371
|
+
if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) continue;
|
|
13372
|
+
copy[sym] = clone(obj[sym]);
|
|
13373
|
+
}
|
|
13331
13374
|
return copy;
|
|
13332
13375
|
}
|
|
13333
13376
|
function cloneBuiltin(val) {
|
|
@@ -13410,65 +13453,83 @@ function loosePairs() {
|
|
|
13410
13453
|
/**
|
|
13411
13454
|
* Pairs that also require the same reference structure: each object of one
|
|
13412
13455
|
* value pairs with exactly one of the other, so shared references and
|
|
13413
|
-
* cycles are shared at the same places.
|
|
13456
|
+
* cycles are shared at the same places. `ab` and `ba` hold the pairs (and
|
|
13457
|
+
* may hold some already); those made are listed in `added`.
|
|
13414
13458
|
*/
|
|
13415
|
-
function strictPairs() {
|
|
13416
|
-
const
|
|
13417
|
-
const
|
|
13418
|
-
const pairs = (a, b) => {
|
|
13459
|
+
function strictPairs(ab = /* @__PURE__ */ new Map(), ba = /* @__PURE__ */ new Map()) {
|
|
13460
|
+
const added = [];
|
|
13461
|
+
const pairs = ((a, b) => {
|
|
13419
13462
|
const known = ab.get(a);
|
|
13420
13463
|
if (known !== void 0) return known === b;
|
|
13421
13464
|
if (ba.has(b)) return false;
|
|
13422
13465
|
ab.set(a, b);
|
|
13423
13466
|
ba.set(b, a);
|
|
13424
|
-
|
|
13467
|
+
added.push([a, b]);
|
|
13468
|
+
});
|
|
13425
13469
|
pairs.strict = true;
|
|
13470
|
+
pairs.added = added;
|
|
13426
13471
|
return pairs;
|
|
13427
13472
|
}
|
|
13428
13473
|
/**
|
|
13429
|
-
* Whether `a` and `b` are equal as deepEqual() has it and also share
|
|
13430
|
-
* references at the same places (a save of one loads as the other).
|
|
13431
|
-
*/
|
|
13432
|
-
function deepEqualStrict(a, b) {
|
|
13433
|
-
return equal(a, b, strictPairs());
|
|
13434
|
-
}
|
|
13435
|
-
/**
|
|
13436
13474
|
* `curr` with each part that equals (see deepEqualStrict) the part of
|
|
13437
13475
|
* `prev` at the same place replaced by `prev`'s: the history moments of a
|
|
13438
13476
|
* save then share what did not change between them, which a save stores
|
|
13439
13477
|
* once. Containers on the way to a change are copied; neither value is
|
|
13440
|
-
* changed.
|
|
13478
|
+
* changed. The result has the reference structure of `curr`: what `curr`
|
|
13479
|
+
* shares or cycles through stays so, and what it does not share stays
|
|
13480
|
+
* apart, whichever parts are reused.
|
|
13441
13481
|
*/
|
|
13442
13482
|
function shareEqual(prev, curr) {
|
|
13483
|
+
if (!isObjectValue(prev) || !isObjectValue(curr)) return curr;
|
|
13484
|
+
/** The object of the result standing for each object of `curr` met. */
|
|
13443
13485
|
const done = /* @__PURE__ */ new Map();
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13486
|
+
/** The object of `curr` each object of `prev` in the result stands for. */
|
|
13487
|
+
const used = /* @__PURE__ */ new Map();
|
|
13488
|
+
/** `value` for the result: copied, as far as it is not met already. */
|
|
13489
|
+
const fresh = (value) => isObjectValue(value) ? deepClone(value, {
|
|
13490
|
+
keepUnregistered: true,
|
|
13491
|
+
seen: done
|
|
13492
|
+
}) : value;
|
|
13493
|
+
/**
|
|
13494
|
+
* Whether `p` and `c` are equal and so can stand for each other, which
|
|
13495
|
+
* needs each object in them to stand for one object only, in the whole
|
|
13496
|
+
* result. Then every pair of objects they hold is placed in the result.
|
|
13497
|
+
*/
|
|
13498
|
+
function reuse(p, c) {
|
|
13499
|
+
const pairs = strictPairs(used, done);
|
|
13500
|
+
if (equal(p, c, pairs)) return true;
|
|
13501
|
+
for (const [a, b] of pairs.added) {
|
|
13502
|
+
used.delete(a);
|
|
13503
|
+
done.delete(b);
|
|
13450
13504
|
}
|
|
13505
|
+
return false;
|
|
13506
|
+
}
|
|
13507
|
+
function share(p, c) {
|
|
13508
|
+
if (!isObjectValue(c)) return c;
|
|
13509
|
+
const placed = done.get(c);
|
|
13510
|
+
if (placed !== void 0) return placed;
|
|
13511
|
+
if (!isObjectValue(p)) return fresh(c);
|
|
13512
|
+
if (reuse(p, c)) return p;
|
|
13451
13513
|
if (c instanceof Map && p instanceof Map) {
|
|
13452
13514
|
const out = /* @__PURE__ */ new Map();
|
|
13453
13515
|
done.set(c, out);
|
|
13454
|
-
for (const [k, v] of c) out.set(
|
|
13516
|
+
for (const [k, v] of c) out.set(fresh(k), share(p.get(k), v));
|
|
13455
13517
|
return out;
|
|
13456
13518
|
}
|
|
13457
13519
|
if (Array.isArray(c) && Array.isArray(p)) {
|
|
13458
13520
|
const out = new Array(c.length);
|
|
13459
13521
|
done.set(c, out);
|
|
13460
|
-
for (let i = 0; i < c.length; i++) if (i in c) out[i] =
|
|
13522
|
+
for (let i = 0; i < c.length; i++) if (i in c) out[i] = share(p[i], c[i]);
|
|
13461
13523
|
return out;
|
|
13462
13524
|
}
|
|
13463
13525
|
if (mergesWith(p, c, false)) {
|
|
13464
13526
|
const out = Object.create(Object.getPrototypeOf(c));
|
|
13465
13527
|
done.set(c, out);
|
|
13466
13528
|
const cr = c;
|
|
13467
|
-
for (const key of Object.keys(cr)) setOwn(out, key, hasOwn(p, key) ?
|
|
13529
|
+
for (const key of Object.keys(cr)) setOwn(out, key, share(hasOwn(p, key) ? p[key] : void 0, cr[key]));
|
|
13468
13530
|
return out;
|
|
13469
13531
|
}
|
|
13470
|
-
|
|
13471
|
-
return c;
|
|
13532
|
+
return fresh(c);
|
|
13472
13533
|
}
|
|
13473
13534
|
return share(prev, curr);
|
|
13474
13535
|
}
|
|
@@ -13484,7 +13545,11 @@ function equalBytes(a, b) {
|
|
|
13484
13545
|
function equalBuiltin(a, b, assumed) {
|
|
13485
13546
|
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
13486
13547
|
if (a instanceof RegExp) return String(a) === String(b);
|
|
13487
|
-
if (ArrayBuffer.isView(a)
|
|
13548
|
+
if (ArrayBuffer.isView(a)) {
|
|
13549
|
+
const v = b;
|
|
13550
|
+
return a.byteOffset === v.byteOffset && a.byteLength === v.byteLength && equal(a.buffer, v.buffer, assumed);
|
|
13551
|
+
}
|
|
13552
|
+
if (a instanceof ArrayBuffer) return equalBytes(a, b);
|
|
13488
13553
|
if (a instanceof URL || a instanceof URLSearchParams) return String(a) === String(b);
|
|
13489
13554
|
if (isBoxed(a)) return Object.is(a.valueOf(), b.valueOf());
|
|
13490
13555
|
if (isTemporal(a)) return String(a) === String(b);
|
|
@@ -13621,6 +13686,91 @@ function diffPaths(before, after) {
|
|
|
13621
13686
|
})(before, after, []);
|
|
13622
13687
|
return changes;
|
|
13623
13688
|
}
|
|
13689
|
+
var pathKey = (path) => JSON.stringify(path);
|
|
13690
|
+
/** Whether `path`, or one of its ancestors, is one of `keys`. */
|
|
13691
|
+
function underChange(path, keys) {
|
|
13692
|
+
for (let n = 1; n <= path.length; n++) if (keys.has(pathKey(path.slice(0, n)))) return true;
|
|
13693
|
+
return false;
|
|
13694
|
+
}
|
|
13695
|
+
/**
|
|
13696
|
+
* Where each object (or array, or other value) of `root` first appears, in
|
|
13697
|
+
* depth-first order. Plain objects are entered; arrays and other values are
|
|
13698
|
+
* leaves. A path at or below one of `skip` is left out.
|
|
13699
|
+
*/
|
|
13700
|
+
function firstPaths(root, skip = /* @__PURE__ */ new Set()) {
|
|
13701
|
+
const found = /* @__PURE__ */ new Map();
|
|
13702
|
+
(function walk(node, path) {
|
|
13703
|
+
for (const key of Object.keys(node)) {
|
|
13704
|
+
const value = node[key];
|
|
13705
|
+
if (!isObjectValue(value) || found.has(value)) continue;
|
|
13706
|
+
const at = [...path, key];
|
|
13707
|
+
if (skip.has(pathKey(at))) continue;
|
|
13708
|
+
found.set(value, at);
|
|
13709
|
+
if (isMergeable(value)) walk(value, at);
|
|
13710
|
+
}
|
|
13711
|
+
})(root, []);
|
|
13712
|
+
return found;
|
|
13713
|
+
}
|
|
13714
|
+
/**
|
|
13715
|
+
* The paths whose object is another one in the references of `after` than in
|
|
13716
|
+
* `before`, as to where its first appearance is: `$a = $b` makes `a` another
|
|
13717
|
+
* name for the object of `b` even where both hold equal content, and a
|
|
13718
|
+
* `diffPaths` over the content finds nothing. Only paths both hold an object
|
|
13719
|
+
* at are reported, parents before children; each is a write of the object
|
|
13720
|
+
* `after` holds.
|
|
13721
|
+
*/
|
|
13722
|
+
function aliasChanges(before, after) {
|
|
13723
|
+
const earlier = firstPaths(before);
|
|
13724
|
+
const later = firstPaths(after);
|
|
13725
|
+
const changes = [];
|
|
13726
|
+
(function walk(b, a, path) {
|
|
13727
|
+
for (const key of Object.keys(a)) {
|
|
13728
|
+
if (!hasOwn(b, key)) continue;
|
|
13729
|
+
const x = b[key];
|
|
13730
|
+
const y = a[key];
|
|
13731
|
+
if (!isObjectValue(x) || !isObjectValue(y)) continue;
|
|
13732
|
+
const at = [...path, key];
|
|
13733
|
+
const wasAt = earlier.get(x);
|
|
13734
|
+
const isAt = later.get(y);
|
|
13735
|
+
const k = pathKey(at);
|
|
13736
|
+
if (pathKey(wasAt) !== pathKey(isAt)) changes.push({
|
|
13737
|
+
path: at,
|
|
13738
|
+
deleted: false,
|
|
13739
|
+
value: y,
|
|
13740
|
+
alias: true
|
|
13741
|
+
});
|
|
13742
|
+
if (isMergeable(x) && mergesWith(x, y, false) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
|
|
13743
|
+
}
|
|
13744
|
+
})(before, after, []);
|
|
13745
|
+
return changes;
|
|
13746
|
+
}
|
|
13747
|
+
/**
|
|
13748
|
+
* Where the objects of `source` are, other than at or below the paths of
|
|
13749
|
+
* `changes` (which write something new there), for `existingObjects`.
|
|
13750
|
+
*/
|
|
13751
|
+
function locateObjects(source, changes) {
|
|
13752
|
+
return firstPaths(source, new Set(changes.map((c) => pathKey(c.path))));
|
|
13753
|
+
}
|
|
13754
|
+
/**
|
|
13755
|
+
* A map for `deepClone`'s `seen`: an object of the source `paths` locate
|
|
13756
|
+
* (see locateObjects) is copied as the object at the same path in `target`,
|
|
13757
|
+
* so a value that refers to an object the store holds refers to it again
|
|
13758
|
+
* after the copy, not to a copy of it. Objects are looked up in `target`
|
|
13759
|
+
* when met, not up front.
|
|
13760
|
+
*/
|
|
13761
|
+
function existingObjects(paths, target) {
|
|
13762
|
+
return new class extends Map {
|
|
13763
|
+
has(obj) {
|
|
13764
|
+
if (super.has(obj)) return true;
|
|
13765
|
+
const path = paths.get(obj);
|
|
13766
|
+
if (!path) return false;
|
|
13767
|
+
const found = getByPath(target, path);
|
|
13768
|
+
if (!(isObjectValue(found) && (Array.isArray(found) ? Array.isArray(obj) : Object.getPrototypeOf(found) === Object.getPrototypeOf(obj)))) return false;
|
|
13769
|
+
super.set(obj, found);
|
|
13770
|
+
return true;
|
|
13771
|
+
}
|
|
13772
|
+
}();
|
|
13773
|
+
}
|
|
13624
13774
|
/** Whether `target` already holds what `change` would write. */
|
|
13625
13775
|
function isApplied(target, change) {
|
|
13626
13776
|
const parent = getByPath(target, change.path.slice(0, -1));
|
|
@@ -13765,7 +13915,7 @@ var readingSlot = (read) => queued(async (ifid, slot) => read(await slotRecord(i
|
|
|
13765
13915
|
* saves issued before this resolves are tagged with it).
|
|
13766
13916
|
*/
|
|
13767
13917
|
var startNewPlaythrough = queued(startNewPlaythroughNow);
|
|
13768
|
-
async function startNewPlaythroughNow(ifid, id =
|
|
13918
|
+
async function startNewPlaythroughNow(ifid, id = randomUUID()) {
|
|
13769
13919
|
const backend = await getBackend();
|
|
13770
13920
|
const num = await nextPlaythroughNumber(ifid);
|
|
13771
13921
|
const record = {
|
|
@@ -13889,7 +14039,7 @@ var createSave = queued(createSaveNow);
|
|
|
13889
14039
|
async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
|
|
13890
14040
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13891
14041
|
const record = storedRecord({
|
|
13892
|
-
id:
|
|
14042
|
+
id: randomUUID(),
|
|
13893
14043
|
ifid,
|
|
13894
14044
|
playthroughId,
|
|
13895
14045
|
createdAt: now,
|
|
@@ -14215,7 +14365,7 @@ async function prepareImport(data, ifid) {
|
|
|
14215
14365
|
checkSaveExport(data);
|
|
14216
14366
|
if (data.ifid !== ifid) throw new Error(`Save is from a different story (expected IFID ${ifid}, got ${data.ifid})`);
|
|
14217
14367
|
const record = deepClone(data.save);
|
|
14218
|
-
record.meta.id =
|
|
14368
|
+
record.meta.id = randomUUID();
|
|
14219
14369
|
record.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14220
14370
|
record.meta.ifid = ifid;
|
|
14221
14371
|
await ensurePlaythrough(ifid, record.meta.playthroughId, record.meta.createdAt);
|
|
@@ -14405,6 +14555,32 @@ function readState() {
|
|
|
14405
14555
|
}
|
|
14406
14556
|
var cloneValue = (value) => deepClone(value, { keepUnregistered: true });
|
|
14407
14557
|
/**
|
|
14558
|
+
* A cloner of values written into `target`. The references among the values
|
|
14559
|
+
* it copies are kept (`$b = $a` writes one array twice: the two paths get
|
|
14560
|
+
* one copy), and, given where the objects of the source are (see
|
|
14561
|
+
* locateObjects), an object that is one the target holds already is that
|
|
14562
|
+
* one, not a copy: `$a.self = $a` refers to the root of `a` in the store.
|
|
14563
|
+
*/
|
|
14564
|
+
function clonerFor(target, paths) {
|
|
14565
|
+
const seen = paths ? existingObjects(paths, target) : /* @__PURE__ */ new Map();
|
|
14566
|
+
return (value) => deepClone(value, {
|
|
14567
|
+
keepUnregistered: true,
|
|
14568
|
+
seen
|
|
14569
|
+
});
|
|
14570
|
+
}
|
|
14571
|
+
/** One cloner per target copy, made when first used. */
|
|
14572
|
+
var clonersFor = (paths) => {
|
|
14573
|
+
const made = /* @__PURE__ */ new Map();
|
|
14574
|
+
return (target) => {
|
|
14575
|
+
let cloner = made.get(target);
|
|
14576
|
+
if (!cloner) {
|
|
14577
|
+
cloner = clonerFor(target, paths?.(target));
|
|
14578
|
+
made.set(target, cloner);
|
|
14579
|
+
}
|
|
14580
|
+
return cloner;
|
|
14581
|
+
};
|
|
14582
|
+
};
|
|
14583
|
+
/**
|
|
14408
14584
|
* A frozen copy of a value read from a working copy, matching what reads
|
|
14409
14585
|
* from the (frozen) store return: writing to it fails instead of changing
|
|
14410
14586
|
* the code's pending state behind its back.
|
|
@@ -14421,12 +14597,12 @@ function frozenCopy(value) {
|
|
|
14421
14597
|
* path (the code replaced or deleted an intermediate object), its root is
|
|
14422
14598
|
* replaced with the store's root as written.
|
|
14423
14599
|
*/
|
|
14424
|
-
function mirror(draft, ns, change, scopes = activeScopes) {
|
|
14600
|
+
function mirror(draft, ns, change, scopes = activeScopes, cloner = clonerOf) {
|
|
14425
14601
|
const root = change.path[0];
|
|
14426
14602
|
for (const scope of scopes) for (const copy of [scope.work[ns], scope.base[ns]]) try {
|
|
14427
14603
|
applyChange(copy, change.deleted ? change : {
|
|
14428
14604
|
...change,
|
|
14429
|
-
value:
|
|
14605
|
+
value: cloner(copy)(change.value)
|
|
14430
14606
|
});
|
|
14431
14607
|
} catch {
|
|
14432
14608
|
const stored = draft[ns][root];
|
|
@@ -14434,6 +14610,8 @@ function mirror(draft, ns, change, scopes = activeScopes) {
|
|
|
14434
14610
|
else delete copy[root];
|
|
14435
14611
|
}
|
|
14436
14612
|
}
|
|
14613
|
+
/** A cloner of a copy of its own (no references shared with others). */
|
|
14614
|
+
var clonerOf = (target) => clonerFor(target);
|
|
14437
14615
|
var NAMESPACE_KEYS$1 = new Set(NAMESPACES);
|
|
14438
14616
|
/**
|
|
14439
14617
|
* Runs store updates on the running code's working copies. They stay the
|
|
@@ -14533,11 +14711,12 @@ function routeStoreUpdate(recipe) {
|
|
|
14533
14711
|
const own = patches.filter((p) => p.path[0] === ns);
|
|
14534
14712
|
return [ns, own.length ? writtenPaths(view[ns], next[ns], own) : []];
|
|
14535
14713
|
});
|
|
14536
|
-
const
|
|
14714
|
+
const cloners = clonersFor();
|
|
14715
|
+
const owned = (change, target) => change.deleted ? change : {
|
|
14537
14716
|
...change,
|
|
14538
|
-
value:
|
|
14717
|
+
value: cloners(target)(change.value)
|
|
14539
14718
|
};
|
|
14540
|
-
for (const [ns, list] of changes) for (const change of list) applyChange(inner.work[ns], owned(change));
|
|
14719
|
+
for (const [ns, list] of changes) for (const change of list) applyChange(inner.work[ns], owned(change, inner.work[ns]));
|
|
14541
14720
|
return (draft) => {
|
|
14542
14721
|
const target = draft;
|
|
14543
14722
|
for (const key of Object.keys(next)) {
|
|
@@ -14548,12 +14727,12 @@ function routeStoreUpdate(recipe) {
|
|
|
14548
14727
|
for (const [ns, list] of changes) for (const change of list) {
|
|
14549
14728
|
const root = change.path[0];
|
|
14550
14729
|
try {
|
|
14551
|
-
applyChange(namespaces[ns], owned(change));
|
|
14730
|
+
applyChange(namespaces[ns], owned(change, namespaces[ns]));
|
|
14552
14731
|
} catch {
|
|
14553
14732
|
if (hasOwn(inner.work[ns], root)) namespaces[ns][root] = cloneValue(inner.work[ns][root]);
|
|
14554
14733
|
else delete namespaces[ns][root];
|
|
14555
14734
|
}
|
|
14556
|
-
mirror(namespaces, ns, change);
|
|
14735
|
+
mirror(namespaces, ns, change, activeScopes, cloners);
|
|
14557
14736
|
}
|
|
14558
14737
|
};
|
|
14559
14738
|
}
|
|
@@ -14581,7 +14760,7 @@ function commitScopes(commits) {
|
|
|
14581
14760
|
const all = commits.map(({ scope, keepRunning }) => ({
|
|
14582
14761
|
scope,
|
|
14583
14762
|
keepRunning,
|
|
14584
|
-
changes: NAMESPACES.map((ns) => [ns,
|
|
14763
|
+
changes: NAMESPACES.map((ns) => [ns, changesOf(scope.base[ns], scope.work[ns])])
|
|
14585
14764
|
}));
|
|
14586
14765
|
const changed = all.filter(({ changes }) => changes.some(([, list]) => list.length > 0));
|
|
14587
14766
|
if (changed.length === 0) return;
|
|
@@ -14593,29 +14772,53 @@ function commitScopes(commits) {
|
|
|
14593
14772
|
committing = false;
|
|
14594
14773
|
}
|
|
14595
14774
|
function commitRecipe(draft) {
|
|
14596
|
-
all.forEach(({ scope,
|
|
14597
|
-
const
|
|
14775
|
+
all.forEach(({ scope, changes }, i) => {
|
|
14776
|
+
const paths = new Map(changes.map(([ns, list]) => [ns, locateObjects(scope.work[ns], list)]));
|
|
14777
|
+
const own = new Map(changes.map(([ns]) => [ns, clonerFor(draft[ns], paths.get(ns))]));
|
|
14778
|
+
const enclosing = all.slice(0, i).map((c) => c.scope);
|
|
14779
|
+
const enclosingCloners = new Map(changes.map(([ns]) => [ns, clonersFor(() => paths.get(ns))]));
|
|
14598
14780
|
for (const [ns, list] of changes) {
|
|
14599
14781
|
const replaced = /* @__PURE__ */ new Set();
|
|
14600
14782
|
for (const change of list) {
|
|
14601
14783
|
const root = change.path[0];
|
|
14602
|
-
if (replaced.has(root)
|
|
14784
|
+
if (replaced.has(root)) continue;
|
|
14603
14785
|
try {
|
|
14604
|
-
|
|
14786
|
+
if (change.deleted) {
|
|
14787
|
+
if (!isApplied(draft[ns], change)) applyChange(draft[ns], change);
|
|
14788
|
+
continue;
|
|
14789
|
+
}
|
|
14790
|
+
const value = change.alias ? own.get(ns)(change.value) : void 0;
|
|
14791
|
+
if (change.alias ? Object.is(getByPath(draft[ns], change.path), value) : isApplied(draft[ns], change)) continue;
|
|
14792
|
+
applyChange(draft[ns], {
|
|
14605
14793
|
...change,
|
|
14606
|
-
value: own(change.value)
|
|
14794
|
+
value: change.alias ? value : own.get(ns)(change.value)
|
|
14607
14795
|
});
|
|
14608
14796
|
} catch {
|
|
14609
|
-
draft[ns][root] = own(scope.work[ns][root]);
|
|
14797
|
+
draft[ns][root] = own.get(ns)(scope.work[ns][root]);
|
|
14610
14798
|
replaced.add(root);
|
|
14611
14799
|
}
|
|
14612
14800
|
}
|
|
14613
|
-
const
|
|
14614
|
-
for (const change of list) mirror(draft, ns, change, enclosing);
|
|
14801
|
+
for (const change of list) mirror(draft, ns, change, enclosing, enclosingCloners.get(ns));
|
|
14615
14802
|
}
|
|
14616
14803
|
});
|
|
14617
14804
|
}
|
|
14618
14805
|
}
|
|
14806
|
+
/**
|
|
14807
|
+
* The changes of the code to a namespace: its property paths that differ
|
|
14808
|
+
* (see diffPaths), and those that hold equal content but another object
|
|
14809
|
+
* than they did (see aliasChanges), those first.
|
|
14810
|
+
*/
|
|
14811
|
+
function changesOf(base, work) {
|
|
14812
|
+
const written = diffPaths(base, work);
|
|
14813
|
+
const keys = new Set(written.map((c) => pathKey(c.path)));
|
|
14814
|
+
const aliases = [];
|
|
14815
|
+
for (const change of aliasChanges(base, work)) {
|
|
14816
|
+
if (underChange(change.path, keys)) continue;
|
|
14817
|
+
keys.add(pathKey(change.path));
|
|
14818
|
+
aliases.push(change);
|
|
14819
|
+
}
|
|
14820
|
+
return [...aliases, ...written];
|
|
14821
|
+
}
|
|
14619
14822
|
/** Every running mutation, to commit with its code going on. */
|
|
14620
14823
|
var running = () => activeScopes.map((scope) => ({
|
|
14621
14824
|
scope,
|
|
@@ -14851,15 +15054,33 @@ function registerDialogHost(callbacks) {
|
|
|
14851
15054
|
if (dialogHostCallbacks === callbacks) dialogHostCallbacks = null;
|
|
14852
15055
|
};
|
|
14853
15056
|
}
|
|
15057
|
+
/**
|
|
15058
|
+
* The dialogs on display, bottom first, whoever opened them: the trigger
|
|
15059
|
+
* host, `{dialog}` or a menubar button (each PassageDialog registers itself
|
|
15060
|
+
* while mounted), as the function that closes each.
|
|
15061
|
+
*/
|
|
15062
|
+
var openDialogs = [];
|
|
15063
|
+
/** Register a displayed dialog; returns the function that unregisters it. */
|
|
15064
|
+
function registerOpenDialog(close) {
|
|
15065
|
+
openDialogs.push(close);
|
|
15066
|
+
return () => {
|
|
15067
|
+
const i = openDialogs.lastIndexOf(close);
|
|
15068
|
+
if (i >= 0) openDialogs.splice(i, 1);
|
|
15069
|
+
};
|
|
15070
|
+
}
|
|
15071
|
+
/** Close the topmost dialog. */
|
|
14854
15072
|
function closeCurrentDialog() {
|
|
14855
|
-
|
|
15073
|
+
const top = openDialogs[openDialogs.length - 1];
|
|
15074
|
+
if (top) top();
|
|
15075
|
+
else dialogHostCallbacks?.close();
|
|
14856
15076
|
}
|
|
14857
15077
|
function closeAllOpenDialogs() {
|
|
14858
15078
|
clearDialogQueue();
|
|
15079
|
+
for (const close of [...openDialogs].reverse()) close();
|
|
14859
15080
|
dialogHostCallbacks?.closeAll();
|
|
14860
15081
|
}
|
|
14861
15082
|
function isDialogShowing() {
|
|
14862
|
-
return dialogHostCallbacks?.isOpen() ?? false;
|
|
15083
|
+
return openDialogs.length > 0 || (dialogHostCallbacks?.isOpen() ?? false);
|
|
14863
15084
|
}
|
|
14864
15085
|
//#endregion
|
|
14865
15086
|
//#region src/runtime-errors.ts
|
|
@@ -14942,7 +15163,10 @@ var lastNavigationVars = createNamespace();
|
|
|
14942
15163
|
* object) or deleted. A recorded value is a deep copy, so that changing a
|
|
14943
15164
|
* live value in place (a class instance is not frozen) cannot change
|
|
14944
15165
|
* history; instances of unregistered classes are kept, as the store keeps
|
|
14945
|
-
* them (a save then refuses them).
|
|
15166
|
+
* them (a save then refuses them). The copies are made together, so what
|
|
15167
|
+
* variables share (an object, a cycle) stays shared in them, and a variable
|
|
15168
|
+
* left as it is that shares with a replaced one is recorded again, to share
|
|
15169
|
+
* with its copy.
|
|
14946
15170
|
*/
|
|
14947
15171
|
function computeVarPatches(prev, curr) {
|
|
14948
15172
|
const forward = [];
|
|
@@ -14950,11 +15174,33 @@ function computeVarPatches(prev, curr) {
|
|
|
14950
15174
|
key,
|
|
14951
15175
|
deleted: true
|
|
14952
15176
|
});
|
|
14953
|
-
|
|
15177
|
+
const seen = /* @__PURE__ */ new Map();
|
|
15178
|
+
const record = (key) => forward.push({
|
|
14954
15179
|
key,
|
|
14955
15180
|
deleted: false,
|
|
14956
|
-
value: deepClone(curr[key], {
|
|
15181
|
+
value: deepClone(curr[key], {
|
|
15182
|
+
keepUnregistered: true,
|
|
15183
|
+
seen
|
|
15184
|
+
})
|
|
14957
15185
|
});
|
|
15186
|
+
const unchanged = [];
|
|
15187
|
+
for (const key of Object.keys(curr)) if (!hasOwn(prev, key) || !Object.is(prev[key], curr[key])) record(key);
|
|
15188
|
+
else if (typeof curr[key] === "object" && curr[key] !== null) unchanged.push(key);
|
|
15189
|
+
for (let found = seen.size > 0; found;) {
|
|
15190
|
+
found = false;
|
|
15191
|
+
for (const key of unchanged) {
|
|
15192
|
+
const probe = /* @__PURE__ */ new Map();
|
|
15193
|
+
deepClone(curr[key], {
|
|
15194
|
+
keepUnregistered: true,
|
|
15195
|
+
seen: probe
|
|
15196
|
+
});
|
|
15197
|
+
if (![...probe.keys()].some((object) => seen.has(object))) continue;
|
|
15198
|
+
record(key);
|
|
15199
|
+
unchanged.splice(unchanged.indexOf(key), 1);
|
|
15200
|
+
found = true;
|
|
15201
|
+
break;
|
|
15202
|
+
}
|
|
15203
|
+
}
|
|
14958
15204
|
return { forward };
|
|
14959
15205
|
}
|
|
14960
15206
|
/**
|
|
@@ -15273,7 +15519,7 @@ function switchToPlaythrough(id, stored) {
|
|
|
15273
15519
|
}
|
|
15274
15520
|
/** Move the running game to a new playthrough at once (see restart). */
|
|
15275
15521
|
function switchToNewPlaythrough(ifid) {
|
|
15276
|
-
const id =
|
|
15522
|
+
const id = randomUUID();
|
|
15277
15523
|
switchToPlaythrough(id, startNewPlaythrough(ifid, id).catch((err) => {
|
|
15278
15524
|
console.error("spindle: failed to start new playthrough", err);
|
|
15279
15525
|
}));
|
|
@@ -15786,7 +16032,7 @@ var useStoryStore = create()(storyStore((set, get) => ({
|
|
|
15786
16032
|
const ifid = storyData.ifid;
|
|
15787
16033
|
const current = knownPlaythroughId();
|
|
15788
16034
|
const established = playthroughSetup;
|
|
15789
|
-
const replacementId =
|
|
16035
|
+
const replacementId = randomUUID();
|
|
15790
16036
|
const deletion = deletePlaythroughData(ifid, playthroughId, {
|
|
15791
16037
|
current: current || established,
|
|
15792
16038
|
id: replacementId
|
|
@@ -16022,6 +16268,7 @@ function PassageDialog({ passageName, fallbackMarkup, panelClass, onClose, dismi
|
|
|
16022
16268
|
}, [markup, nobr]);
|
|
16023
16269
|
const panelRef = useRef(null);
|
|
16024
16270
|
useModalFocus(panelRef, ".dialog-body", dismissible, stableOnClose);
|
|
16271
|
+
useLayoutEffect(() => registerOpenDialog(stableOnClose), [stableOnClose]);
|
|
16025
16272
|
useLayoutEffect(() => {
|
|
16026
16273
|
if (panelRef.current) emitFromRender("dialogrender", passageName ?? "", panelRef.current);
|
|
16027
16274
|
}, []);
|
|
@@ -16619,7 +16866,7 @@ function unset(param) {
|
|
|
16619
16866
|
return param.type === "options" ? {} : void 0;
|
|
16620
16867
|
}
|
|
16621
16868
|
var WORD_RE = /\w+/y;
|
|
16622
|
-
var
|
|
16869
|
+
var NUMBER_RE = /[-+]?(?:\d+\.?\d*|\.\d+)/y;
|
|
16623
16870
|
var WORD_OR_PUNCT_RE = /\S+/y;
|
|
16624
16871
|
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
16625
16872
|
function matchAt(re, src, pos) {
|
|
@@ -16628,7 +16875,8 @@ function matchAt(re, src, pos) {
|
|
|
16628
16875
|
}
|
|
16629
16876
|
/**
|
|
16630
16877
|
* Read keyword options (`goto "X" priority 5 once`). A keyword takes the
|
|
16631
|
-
* quoted string (with or without whitespace before it) or
|
|
16878
|
+
* quoted string (with or without whitespace before it) or number (signed,
|
|
16879
|
+
* fractional) after
|
|
16632
16880
|
* it as its value; keywords that aren't declared, and their values, are
|
|
16633
16881
|
* skipped. A keyword of a `string` parameter must be followed by a quoted
|
|
16634
16882
|
* string.
|
|
@@ -16647,7 +16895,7 @@ function readOptions(params, src, { at, spans }) {
|
|
|
16647
16895
|
let j = i;
|
|
16648
16896
|
while (j < src.length && isWhitespace(src[j])) j++;
|
|
16649
16897
|
const quoted = readQuoted(src, j);
|
|
16650
|
-
const digits = quoted || j === i ? null : matchAt(
|
|
16898
|
+
const digits = quoted || j === i ? null : matchAt(NUMBER_RE, src, j);
|
|
16651
16899
|
if (quoted) {
|
|
16652
16900
|
val = quoted.value;
|
|
16653
16901
|
i = quoted.end;
|
|
@@ -17364,10 +17612,6 @@ function parameterLookup(macros) {
|
|
|
17364
17612
|
}
|
|
17365
17613
|
/** What a `string` or `text` argument holds when its parameter doesn't say. */
|
|
17366
17614
|
var holdsByDefault = (interpolate) => interpolate ? "markup" : "text";
|
|
17367
|
-
/** Whether a parameter in `params` (options too) holds code in a string. */
|
|
17368
|
-
function holdsCode(params) {
|
|
17369
|
-
return params.some((p) => p.holds === "expression" || p.holds === "statements" || !!p.parameters && holdsCode(p.parameters));
|
|
17370
|
-
}
|
|
17371
17615
|
/** The goal of the code an argument of this type holds, if it is code. */
|
|
17372
17616
|
function codeGoal(param) {
|
|
17373
17617
|
if (param.type === "expression" || param.type === "passage") return "expression";
|
|
@@ -17390,15 +17634,22 @@ function* codeAndText(src, tokens, parametersOf) {
|
|
|
17390
17634
|
const closeOf = (t, name) => tokens.findIndex((c, k) => k > t && c.type === "macro" && c.isClose && c.name.toLowerCase() === name);
|
|
17391
17635
|
for (let t = 0; t < tokens.length; t++) {
|
|
17392
17636
|
const token = tokens[t];
|
|
17393
|
-
if (token.type === "link")
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17637
|
+
if (token.type === "link") {
|
|
17638
|
+
yield {
|
|
17639
|
+
kind: "passage",
|
|
17640
|
+
name: token.target,
|
|
17641
|
+
offset: token.targetStart,
|
|
17642
|
+
length: token.targetEnd - token.targetStart,
|
|
17643
|
+
macro: "link",
|
|
17644
|
+
label: src.slice(token.start, token.end)
|
|
17645
|
+
};
|
|
17646
|
+
if (token.display.includes("{")) yield {
|
|
17647
|
+
kind: "text",
|
|
17648
|
+
text: token.display,
|
|
17649
|
+
offset: locate(src, token.display, token.start),
|
|
17650
|
+
where: "In the label of a link: "
|
|
17651
|
+
};
|
|
17652
|
+
} else if (token.type === "expression") yield {
|
|
17402
17653
|
kind: "code",
|
|
17403
17654
|
code: token.expression,
|
|
17404
17655
|
offset: token.end - 1 - token.expression.length,
|
|
@@ -17564,7 +17815,7 @@ function* argPieces(args, offset, params, macro, label) {
|
|
|
17564
17815
|
};
|
|
17565
17816
|
}
|
|
17566
17817
|
}
|
|
17567
|
-
yield* visit(params, values);
|
|
17818
|
+
yield* [...visit(params, values)].sort((a, b) => a.offset - b.offset);
|
|
17568
17819
|
}
|
|
17569
17820
|
/**
|
|
17570
17821
|
* Index of `part` in `text` from `from` on, else anywhere, else `from`: the
|
|
@@ -17819,7 +18070,6 @@ function declarationRegex(sigil) {
|
|
|
17819
18070
|
var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
|
|
17820
18071
|
/** Quoted first argument of an input macro naming a story variable. */
|
|
17821
18072
|
var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
|
|
17822
|
-
var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
17823
18073
|
var VALID_VAR_TYPES = new Set([
|
|
17824
18074
|
"number",
|
|
17825
18075
|
"string",
|
|
@@ -17882,30 +18132,14 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
17882
18132
|
return schema;
|
|
17883
18133
|
}
|
|
17884
18134
|
/**
|
|
17885
|
-
* Extract for-loop local variable names from passage content.
|
|
17886
|
-
* `{for @item of ...}` → "item"
|
|
17887
|
-
* `{for @index, @item of ...}` → "index", "item"
|
|
17888
|
-
*/
|
|
17889
|
-
function extractForLocals(content) {
|
|
17890
|
-
const locals = /* @__PURE__ */ new Set();
|
|
17891
|
-
let match;
|
|
17892
|
-
FOR_LOCAL_RE.lastIndex = 0;
|
|
17893
|
-
while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
|
|
17894
|
-
locals.add(match[1]);
|
|
17895
|
-
if (match[2]) locals.add(match[2]);
|
|
17896
|
-
}
|
|
17897
|
-
return locals;
|
|
17898
|
-
}
|
|
17899
|
-
/**
|
|
17900
18135
|
* Validate a single variable reference path (e.g. "player.health") against
|
|
17901
18136
|
* the schema. Returns an error message or null if valid.
|
|
17902
18137
|
*/
|
|
17903
|
-
function validateRef(ref, schema
|
|
18138
|
+
function validateRef(ref, schema) {
|
|
17904
18139
|
const parts = ref.split(".");
|
|
17905
18140
|
const rootName = parts[0];
|
|
17906
18141
|
const nameError = variableNameError(rootName, "$" + rootName);
|
|
17907
18142
|
if (nameError) return nameError;
|
|
17908
|
-
if (forLocals.has(rootName)) return null;
|
|
17909
18143
|
const rootSchema = schema.get(rootName);
|
|
17910
18144
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
|
17911
18145
|
let current = rootSchema;
|
|
@@ -17961,8 +18195,9 @@ function scanInterpolations(text, onRef, storeVarMacros = NO_STORE_VAR_MACROS) {
|
|
|
17961
18195
|
* Report `$var` references in JavaScript code, lexed as the expression
|
|
17962
18196
|
* engine lexes it (`lexJs`): references in code, but none inside string or
|
|
17963
18197
|
* regex literals or comments, nor property names (`a.$b`, `{ $b: 1 }`).
|
|
17964
|
-
* The text of string and template literals is scanned
|
|
17965
|
-
*
|
|
18198
|
+
* The text of string and template literals is not scanned: a string in code
|
|
18199
|
+
* is literal text unless a macro argument that holds markup is read as such
|
|
18200
|
+
* (see scanArgs); the `${…}` parts of template literals are code.
|
|
17966
18201
|
*/
|
|
17967
18202
|
function scanCode(code, onRef, goal = "expression") {
|
|
17968
18203
|
const parsed = parseOrError(code, goal);
|
|
@@ -17975,37 +18210,17 @@ function scanCode(code, onRef, goal = "expression") {
|
|
|
17975
18210
|
VAR_PATH_RE.lastIndex = ref.start;
|
|
17976
18211
|
onRef(VAR_PATH_RE.exec(code)[1]);
|
|
17977
18212
|
}
|
|
17978
|
-
for (const text of parsed.strings) scanInterpolations(text, onRef);
|
|
17979
18213
|
}
|
|
17980
18214
|
/** `scanCode` for code acorn can't parse: lexed leniently. */
|
|
17981
18215
|
function scanCodeLeniently(code, onRef, goal) {
|
|
17982
|
-
/** Open template literals: their text so far, null in an interpolation. */
|
|
17983
|
-
const templates = [];
|
|
17984
18216
|
lexJs(code, {
|
|
17985
18217
|
variable(sigil, _name, index) {
|
|
17986
18218
|
if (sigil !== "$") return;
|
|
17987
18219
|
VAR_PATH_RE.lastIndex = index;
|
|
17988
18220
|
onRef(VAR_PATH_RE.exec(code)[1]);
|
|
17989
18221
|
},
|
|
17990
|
-
literal(
|
|
17991
|
-
const template = templates[templates.length - 1];
|
|
17992
|
-
if (template?.nesting === nesting) if (template.text === null) template.text = "";
|
|
17993
|
-
else if (text === "`" || text === "${") {
|
|
17994
|
-
scanInterpolations(template.text, onRef);
|
|
17995
|
-
if (text === "`") templates.pop();
|
|
17996
|
-
else template.text = null;
|
|
17997
|
-
} else template.text += text;
|
|
17998
|
-
else if (text === "`") templates.push({
|
|
17999
|
-
nesting,
|
|
18000
|
-
text: ""
|
|
18001
|
-
});
|
|
18002
|
-
else if (text[0] === "\"" || text[0] === "'") {
|
|
18003
|
-
const { closed } = scanStringLiteral(text, 0);
|
|
18004
|
-
scanInterpolations(text.slice(1, closed ? -1 : void 0), onRef);
|
|
18005
|
-
}
|
|
18006
|
-
}
|
|
18222
|
+
literal() {}
|
|
18007
18223
|
}, goal);
|
|
18008
|
-
for (const t of templates) if (t.text) scanInterpolations(t.text, onRef);
|
|
18009
18224
|
}
|
|
18010
18225
|
/**
|
|
18011
18226
|
* Report the `$var` references a passage evaluates at runtime: `{$var}`
|
|
@@ -18017,14 +18232,23 @@ function collectPassageRefs(content, storeVarMacros, onRef, parametersOf) {
|
|
|
18017
18232
|
withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef, parametersOf));
|
|
18018
18233
|
}
|
|
18019
18234
|
/**
|
|
18020
|
-
* Report the references in the
|
|
18021
|
-
* (see ParameterDef.holds)
|
|
18022
|
-
*
|
|
18023
|
-
*
|
|
18235
|
+
* Report the references in the arguments of a macro by what its parameters
|
|
18236
|
+
* say each holds (see ParameterDef.holds): code is scanned as code and
|
|
18237
|
+
* markup as markup, but literal text (a placeholder, a passage name) holds
|
|
18238
|
+
* none. A macro without declared parameters, or arguments that do not
|
|
18239
|
+
* parse, are scanned as code.
|
|
18024
18240
|
*/
|
|
18025
|
-
function
|
|
18026
|
-
if (!
|
|
18027
|
-
|
|
18241
|
+
function scanArgs(token, params, onRef) {
|
|
18242
|
+
if (!token.rawArgs) return;
|
|
18243
|
+
if (params) {
|
|
18244
|
+
const pieces = [...argPieces(token.rawArgs, 0, params, token.name, "")];
|
|
18245
|
+
if (!pieces.some((piece) => piece.kind === "argument-error")) {
|
|
18246
|
+
for (const piece of pieces) if (piece.kind === "code") scanCode(piece.code, onRef, piece.goal);
|
|
18247
|
+
else if (piece.kind === "text") scanInterpolations(piece.text, onRef);
|
|
18248
|
+
return;
|
|
18249
|
+
}
|
|
18250
|
+
}
|
|
18251
|
+
scanCode(token.rawArgs, onRef);
|
|
18028
18252
|
}
|
|
18029
18253
|
/** Report the `$var` references in the tokens of `content`. */
|
|
18030
18254
|
function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf = () => void 0) {
|
|
@@ -18032,22 +18256,25 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf =
|
|
|
18032
18256
|
const token = tokens[t];
|
|
18033
18257
|
if (token.type === "variable") {
|
|
18034
18258
|
if (token.scope === "variable" && token.name) onRef(token.name);
|
|
18035
|
-
} else if (token.type === "
|
|
18259
|
+
} else if (token.type === "link") scanInterpolations(token.display, onRef);
|
|
18260
|
+
else if (token.type === "expression") scanCode(token.expression, onRef);
|
|
18036
18261
|
else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) if (isCodeAttribute(name)) scanSigilReferences(value, onRef);
|
|
18037
18262
|
else scanInterpolations(value, onRef, storeVarMacros);
|
|
18038
18263
|
else if (token.type === "macro" && !token.isClose) {
|
|
18039
|
-
|
|
18040
|
-
|
|
18264
|
+
scanArgs(token, parametersOf(token.name), onRef);
|
|
18265
|
+
if (token.className) scanInterpolations(token.className, onRef);
|
|
18266
|
+
if (token.id) scanInterpolations(token.id, onRef);
|
|
18041
18267
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
18042
18268
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
|
|
18043
18269
|
const quoted = QUOTED_VAR_ARG_RE.exec(first);
|
|
18044
18270
|
if (quoted) onRef(quoted[1]);
|
|
18271
|
+
else if (!/^["'`]/.test(first)) scanCode(first, onRef);
|
|
18045
18272
|
}
|
|
18046
|
-
if (token.name === "do") {
|
|
18273
|
+
if (token.name.toLowerCase() === "do") {
|
|
18047
18274
|
let close = t + 1;
|
|
18048
18275
|
while (close < tokens.length) {
|
|
18049
18276
|
const c = tokens[close];
|
|
18050
|
-
if (c.type === "macro" && c.isClose && c.name === "do") break;
|
|
18277
|
+
if (c.type === "macro" && c.isClose && c.name.toLowerCase() === "do") break;
|
|
18051
18278
|
close++;
|
|
18052
18279
|
}
|
|
18053
18280
|
if (close < tokens.length) {
|
|
@@ -18071,9 +18298,8 @@ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_M
|
|
|
18071
18298
|
const storeVarSet = new Set(Array.from(storeVarMacros, (m) => m.toLowerCase()));
|
|
18072
18299
|
for (const [name, passage] of passages) {
|
|
18073
18300
|
if (NOT_MARKUP.has(name)) continue;
|
|
18074
|
-
const forLocals = extractForLocals(passage.content);
|
|
18075
18301
|
collectPassageRefs(passage.content, storeVarSet, (ref) => {
|
|
18076
|
-
const error = validateRef(ref, schema
|
|
18302
|
+
const error = validateRef(ref, schema);
|
|
18077
18303
|
if (error) errors.push(`Passage "${name}": ${error}`);
|
|
18078
18304
|
}, parametersOf);
|
|
18079
18305
|
}
|
|
@@ -18336,7 +18562,10 @@ defineMacro({
|
|
|
18336
18562
|
],
|
|
18337
18563
|
render(_props, ctx) {
|
|
18338
18564
|
try {
|
|
18339
|
-
const { currentExpr, maxExpr, labelMode } = meterArgs(
|
|
18565
|
+
const { currentExpr, maxExpr, labelMode } = meterArgs({
|
|
18566
|
+
...ctx.args,
|
|
18567
|
+
label: ctx.resolve?.(ctx.args.label)
|
|
18568
|
+
});
|
|
18340
18569
|
const current = Number(ctx.evaluate(currentExpr));
|
|
18341
18570
|
const max = Number(ctx.evaluate(maxExpr));
|
|
18342
18571
|
const pct = max === 0 ? 0 : Math.max(0, Math.min(100, current / max * 100));
|
|
@@ -18737,7 +18966,7 @@ defineMacro({
|
|
|
18737
18966
|
type: "passage"
|
|
18738
18967
|
}],
|
|
18739
18968
|
render({ children = [] }, ctx) {
|
|
18740
|
-
const display = ctx.args.text ?? "";
|
|
18969
|
+
const display = ctx.resolve?.(ctx.args.text ?? "") ?? "";
|
|
18741
18970
|
let passage = null;
|
|
18742
18971
|
let failure;
|
|
18743
18972
|
if (ctx.args.passage !== void 0) try {
|
|
@@ -18884,7 +19113,7 @@ defineMacro({
|
|
|
18884
19113
|
visibleCharsRef.current = visibleChars;
|
|
18885
19114
|
useEffect(() => {
|
|
18886
19115
|
if (containerRef.current) setTotalChars((containerRef.current.textContent || "").length);
|
|
18887
|
-
}
|
|
19116
|
+
});
|
|
18888
19117
|
useEffect(() => {
|
|
18889
19118
|
if (totalChars === 0) return;
|
|
18890
19119
|
if (visibleCharsRef.current >= totalChars) return;
|