@rohal12/spindle 0.59.0 → 0.59.1
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 +119 -90
- package/dist/pkg/story-variables.js +1 -1
- package/package.json +1 -1
- package/src/automation/runner.ts +2 -1
- package/src/code-check.ts +2 -1
- package/src/components/macros/MacroLink.tsx +2 -1
- package/src/components/macros/Meter.tsx +5 -1
- package/src/saves/storage.ts +14 -1
- package/src/store.ts +29 -7
- package/src/story-variables.ts +25 -63
- package/src/structural.ts +67 -21
package/dist/pkg/headless.js
CHANGED
|
@@ -13080,7 +13080,15 @@ function createIDBBackend() {
|
|
|
13080
13080
|
const rows = name === "meta";
|
|
13081
13081
|
/** Run one request in a transaction of its own. */
|
|
13082
13082
|
const request = async (mode, make) => {
|
|
13083
|
-
|
|
13083
|
+
const tx = (await openDB()).transaction(name, mode);
|
|
13084
|
+
const result = idbReq(make(tx.objectStore(name)));
|
|
13085
|
+
if (mode === "readonly") return result;
|
|
13086
|
+
const committed = new Promise((resolve, reject) => {
|
|
13087
|
+
tx.oncomplete = () => resolve();
|
|
13088
|
+
tx.onabort = () => reject(tx.error ?? new DOMException("Transaction aborted", "AbortError"));
|
|
13089
|
+
});
|
|
13090
|
+
const [value] = await Promise.all([result, committed]);
|
|
13091
|
+
return value;
|
|
13084
13092
|
};
|
|
13085
13093
|
return {
|
|
13086
13094
|
async get(key) {
|
|
@@ -13319,15 +13327,22 @@ var ERROR_HIDDEN_KEYS = [
|
|
|
13319
13327
|
"errors"
|
|
13320
13328
|
];
|
|
13321
13329
|
function deepClone(value, options = {}) {
|
|
13322
|
-
const seen = /* @__PURE__ */ new Map();
|
|
13330
|
+
const seen = options.seen ?? /* @__PURE__ */ new Map();
|
|
13323
13331
|
/** Record `copy` as the copy of `obj` before its contents are copied. */
|
|
13324
13332
|
const keep = (obj, copy) => {
|
|
13325
13333
|
seen.set(obj, copy);
|
|
13326
13334
|
return copy;
|
|
13327
13335
|
};
|
|
13328
|
-
/**
|
|
13336
|
+
/**
|
|
13337
|
+
* Copy the own (enumerable) keys of `obj` into `copy`, symbol keys too: a
|
|
13338
|
+
* save refuses them, which a copy must not hide.
|
|
13339
|
+
*/
|
|
13329
13340
|
function copyKeys(obj, copy) {
|
|
13330
13341
|
for (const key of Object.keys(obj)) setOwn(copy, key, clone(obj[key]));
|
|
13342
|
+
for (const sym of Object.getOwnPropertySymbols(obj)) {
|
|
13343
|
+
if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) continue;
|
|
13344
|
+
copy[sym] = clone(obj[sym]);
|
|
13345
|
+
}
|
|
13331
13346
|
return copy;
|
|
13332
13347
|
}
|
|
13333
13348
|
function cloneBuiltin(val) {
|
|
@@ -13410,65 +13425,83 @@ function loosePairs() {
|
|
|
13410
13425
|
/**
|
|
13411
13426
|
* Pairs that also require the same reference structure: each object of one
|
|
13412
13427
|
* value pairs with exactly one of the other, so shared references and
|
|
13413
|
-
* cycles are shared at the same places.
|
|
13428
|
+
* cycles are shared at the same places. `ab` and `ba` hold the pairs (and
|
|
13429
|
+
* may hold some already); those made are listed in `added`.
|
|
13414
13430
|
*/
|
|
13415
|
-
function strictPairs() {
|
|
13416
|
-
const
|
|
13417
|
-
const
|
|
13418
|
-
const pairs = (a, b) => {
|
|
13431
|
+
function strictPairs(ab = /* @__PURE__ */ new Map(), ba = /* @__PURE__ */ new Map()) {
|
|
13432
|
+
const added = [];
|
|
13433
|
+
const pairs = ((a, b) => {
|
|
13419
13434
|
const known = ab.get(a);
|
|
13420
13435
|
if (known !== void 0) return known === b;
|
|
13421
13436
|
if (ba.has(b)) return false;
|
|
13422
13437
|
ab.set(a, b);
|
|
13423
13438
|
ba.set(b, a);
|
|
13424
|
-
|
|
13439
|
+
added.push([a, b]);
|
|
13440
|
+
});
|
|
13425
13441
|
pairs.strict = true;
|
|
13442
|
+
pairs.added = added;
|
|
13426
13443
|
return pairs;
|
|
13427
13444
|
}
|
|
13428
13445
|
/**
|
|
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
13446
|
* `curr` with each part that equals (see deepEqualStrict) the part of
|
|
13437
13447
|
* `prev` at the same place replaced by `prev`'s: the history moments of a
|
|
13438
13448
|
* save then share what did not change between them, which a save stores
|
|
13439
13449
|
* once. Containers on the way to a change are copied; neither value is
|
|
13440
|
-
* changed.
|
|
13450
|
+
* changed. The result has the reference structure of `curr`: what `curr`
|
|
13451
|
+
* shares or cycles through stays so, and what it does not share stays
|
|
13452
|
+
* apart, whichever parts are reused.
|
|
13441
13453
|
*/
|
|
13442
13454
|
function shareEqual(prev, curr) {
|
|
13455
|
+
if (!isObjectValue(prev) || !isObjectValue(curr)) return curr;
|
|
13456
|
+
/** The object of the result standing for each object of `curr` met. */
|
|
13443
13457
|
const done = /* @__PURE__ */ new Map();
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13458
|
+
/** The object of `curr` each object of `prev` in the result stands for. */
|
|
13459
|
+
const used = /* @__PURE__ */ new Map();
|
|
13460
|
+
/** `value` for the result: copied, as far as it is not met already. */
|
|
13461
|
+
const fresh = (value) => isObjectValue(value) ? deepClone(value, {
|
|
13462
|
+
keepUnregistered: true,
|
|
13463
|
+
seen: done
|
|
13464
|
+
}) : value;
|
|
13465
|
+
/**
|
|
13466
|
+
* Whether `p` and `c` are equal and so can stand for each other, which
|
|
13467
|
+
* needs each object in them to stand for one object only, in the whole
|
|
13468
|
+
* result. Then every pair of objects they hold is placed in the result.
|
|
13469
|
+
*/
|
|
13470
|
+
function reuse(p, c) {
|
|
13471
|
+
const pairs = strictPairs(used, done);
|
|
13472
|
+
if (equal(p, c, pairs)) return true;
|
|
13473
|
+
for (const [a, b] of pairs.added) {
|
|
13474
|
+
used.delete(a);
|
|
13475
|
+
done.delete(b);
|
|
13450
13476
|
}
|
|
13477
|
+
return false;
|
|
13478
|
+
}
|
|
13479
|
+
function share(p, c) {
|
|
13480
|
+
if (!isObjectValue(c)) return c;
|
|
13481
|
+
const placed = done.get(c);
|
|
13482
|
+
if (placed !== void 0) return placed;
|
|
13483
|
+
if (!isObjectValue(p)) return fresh(c);
|
|
13484
|
+
if (reuse(p, c)) return p;
|
|
13451
13485
|
if (c instanceof Map && p instanceof Map) {
|
|
13452
13486
|
const out = /* @__PURE__ */ new Map();
|
|
13453
13487
|
done.set(c, out);
|
|
13454
|
-
for (const [k, v] of c) out.set(
|
|
13488
|
+
for (const [k, v] of c) out.set(fresh(k), share(p.get(k), v));
|
|
13455
13489
|
return out;
|
|
13456
13490
|
}
|
|
13457
13491
|
if (Array.isArray(c) && Array.isArray(p)) {
|
|
13458
13492
|
const out = new Array(c.length);
|
|
13459
13493
|
done.set(c, out);
|
|
13460
|
-
for (let i = 0; i < c.length; i++) if (i in c) out[i] =
|
|
13494
|
+
for (let i = 0; i < c.length; i++) if (i in c) out[i] = share(p[i], c[i]);
|
|
13461
13495
|
return out;
|
|
13462
13496
|
}
|
|
13463
13497
|
if (mergesWith(p, c, false)) {
|
|
13464
13498
|
const out = Object.create(Object.getPrototypeOf(c));
|
|
13465
13499
|
done.set(c, out);
|
|
13466
13500
|
const cr = c;
|
|
13467
|
-
for (const key of Object.keys(cr)) setOwn(out, key, hasOwn(p, key) ?
|
|
13501
|
+
for (const key of Object.keys(cr)) setOwn(out, key, share(hasOwn(p, key) ? p[key] : void 0, cr[key]));
|
|
13468
13502
|
return out;
|
|
13469
13503
|
}
|
|
13470
|
-
|
|
13471
|
-
return c;
|
|
13504
|
+
return fresh(c);
|
|
13472
13505
|
}
|
|
13473
13506
|
return share(prev, curr);
|
|
13474
13507
|
}
|
|
@@ -14942,7 +14975,10 @@ var lastNavigationVars = createNamespace();
|
|
|
14942
14975
|
* object) or deleted. A recorded value is a deep copy, so that changing a
|
|
14943
14976
|
* live value in place (a class instance is not frozen) cannot change
|
|
14944
14977
|
* history; instances of unregistered classes are kept, as the store keeps
|
|
14945
|
-
* them (a save then refuses them).
|
|
14978
|
+
* them (a save then refuses them). The copies are made together, so what
|
|
14979
|
+
* variables share (an object, a cycle) stays shared in them, and a variable
|
|
14980
|
+
* left as it is that shares with a replaced one is recorded again, to share
|
|
14981
|
+
* with its copy.
|
|
14946
14982
|
*/
|
|
14947
14983
|
function computeVarPatches(prev, curr) {
|
|
14948
14984
|
const forward = [];
|
|
@@ -14950,11 +14986,33 @@ function computeVarPatches(prev, curr) {
|
|
|
14950
14986
|
key,
|
|
14951
14987
|
deleted: true
|
|
14952
14988
|
});
|
|
14953
|
-
|
|
14989
|
+
const seen = /* @__PURE__ */ new Map();
|
|
14990
|
+
const record = (key) => forward.push({
|
|
14954
14991
|
key,
|
|
14955
14992
|
deleted: false,
|
|
14956
|
-
value: deepClone(curr[key], {
|
|
14993
|
+
value: deepClone(curr[key], {
|
|
14994
|
+
keepUnregistered: true,
|
|
14995
|
+
seen
|
|
14996
|
+
})
|
|
14957
14997
|
});
|
|
14998
|
+
const unchanged = [];
|
|
14999
|
+
for (const key of Object.keys(curr)) if (!hasOwn(prev, key) || !Object.is(prev[key], curr[key])) record(key);
|
|
15000
|
+
else if (typeof curr[key] === "object" && curr[key] !== null) unchanged.push(key);
|
|
15001
|
+
for (let found = seen.size > 0; found;) {
|
|
15002
|
+
found = false;
|
|
15003
|
+
for (const key of unchanged) {
|
|
15004
|
+
const probe = /* @__PURE__ */ new Map();
|
|
15005
|
+
deepClone(curr[key], {
|
|
15006
|
+
keepUnregistered: true,
|
|
15007
|
+
seen: probe
|
|
15008
|
+
});
|
|
15009
|
+
if (![...probe.keys()].some((object) => seen.has(object))) continue;
|
|
15010
|
+
record(key);
|
|
15011
|
+
unchanged.splice(unchanged.indexOf(key), 1);
|
|
15012
|
+
found = true;
|
|
15013
|
+
break;
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
14958
15016
|
return { forward };
|
|
14959
15017
|
}
|
|
14960
15018
|
/**
|
|
@@ -17364,10 +17422,6 @@ function parameterLookup(macros) {
|
|
|
17364
17422
|
}
|
|
17365
17423
|
/** What a `string` or `text` argument holds when its parameter doesn't say. */
|
|
17366
17424
|
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
17425
|
/** The goal of the code an argument of this type holds, if it is code. */
|
|
17372
17426
|
function codeGoal(param) {
|
|
17373
17427
|
if (param.type === "expression" || param.type === "passage") return "expression";
|
|
@@ -17564,7 +17618,7 @@ function* argPieces(args, offset, params, macro, label) {
|
|
|
17564
17618
|
};
|
|
17565
17619
|
}
|
|
17566
17620
|
}
|
|
17567
|
-
yield* visit(params, values);
|
|
17621
|
+
yield* [...visit(params, values)].sort((a, b) => a.offset - b.offset);
|
|
17568
17622
|
}
|
|
17569
17623
|
/**
|
|
17570
17624
|
* Index of `part` in `text` from `from` on, else anywhere, else `from`: the
|
|
@@ -17819,7 +17873,6 @@ function declarationRegex(sigil) {
|
|
|
17819
17873
|
var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
|
|
17820
17874
|
/** Quoted first argument of an input macro naming a story variable. */
|
|
17821
17875
|
var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
|
|
17822
|
-
var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
17823
17876
|
var VALID_VAR_TYPES = new Set([
|
|
17824
17877
|
"number",
|
|
17825
17878
|
"string",
|
|
@@ -17882,30 +17935,14 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
17882
17935
|
return schema;
|
|
17883
17936
|
}
|
|
17884
17937
|
/**
|
|
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
17938
|
* Validate a single variable reference path (e.g. "player.health") against
|
|
17901
17939
|
* the schema. Returns an error message or null if valid.
|
|
17902
17940
|
*/
|
|
17903
|
-
function validateRef(ref, schema
|
|
17941
|
+
function validateRef(ref, schema) {
|
|
17904
17942
|
const parts = ref.split(".");
|
|
17905
17943
|
const rootName = parts[0];
|
|
17906
17944
|
const nameError = variableNameError(rootName, "$" + rootName);
|
|
17907
17945
|
if (nameError) return nameError;
|
|
17908
|
-
if (forLocals.has(rootName)) return null;
|
|
17909
17946
|
const rootSchema = schema.get(rootName);
|
|
17910
17947
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
|
17911
17948
|
let current = rootSchema;
|
|
@@ -17961,8 +17998,9 @@ function scanInterpolations(text, onRef, storeVarMacros = NO_STORE_VAR_MACROS) {
|
|
|
17961
17998
|
* Report `$var` references in JavaScript code, lexed as the expression
|
|
17962
17999
|
* engine lexes it (`lexJs`): references in code, but none inside string or
|
|
17963
18000
|
* regex literals or comments, nor property names (`a.$b`, `{ $b: 1 }`).
|
|
17964
|
-
* The text of string and template literals is scanned
|
|
17965
|
-
*
|
|
18001
|
+
* The text of string and template literals is not scanned: a string in code
|
|
18002
|
+
* is literal text unless a macro argument that holds markup is read as such
|
|
18003
|
+
* (see scanArgs); the `${…}` parts of template literals are code.
|
|
17966
18004
|
*/
|
|
17967
18005
|
function scanCode(code, onRef, goal = "expression") {
|
|
17968
18006
|
const parsed = parseOrError(code, goal);
|
|
@@ -17975,37 +18013,17 @@ function scanCode(code, onRef, goal = "expression") {
|
|
|
17975
18013
|
VAR_PATH_RE.lastIndex = ref.start;
|
|
17976
18014
|
onRef(VAR_PATH_RE.exec(code)[1]);
|
|
17977
18015
|
}
|
|
17978
|
-
for (const text of parsed.strings) scanInterpolations(text, onRef);
|
|
17979
18016
|
}
|
|
17980
18017
|
/** `scanCode` for code acorn can't parse: lexed leniently. */
|
|
17981
18018
|
function scanCodeLeniently(code, onRef, goal) {
|
|
17982
|
-
/** Open template literals: their text so far, null in an interpolation. */
|
|
17983
|
-
const templates = [];
|
|
17984
18019
|
lexJs(code, {
|
|
17985
18020
|
variable(sigil, _name, index) {
|
|
17986
18021
|
if (sigil !== "$") return;
|
|
17987
18022
|
VAR_PATH_RE.lastIndex = index;
|
|
17988
18023
|
onRef(VAR_PATH_RE.exec(code)[1]);
|
|
17989
18024
|
},
|
|
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
|
-
}
|
|
18025
|
+
literal() {}
|
|
18007
18026
|
}, goal);
|
|
18008
|
-
for (const t of templates) if (t.text) scanInterpolations(t.text, onRef);
|
|
18009
18027
|
}
|
|
18010
18028
|
/**
|
|
18011
18029
|
* Report the `$var` references a passage evaluates at runtime: `{$var}`
|
|
@@ -18017,14 +18035,23 @@ function collectPassageRefs(content, storeVarMacros, onRef, parametersOf) {
|
|
|
18017
18035
|
withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef, parametersOf));
|
|
18018
18036
|
}
|
|
18019
18037
|
/**
|
|
18020
|
-
* Report the references in the
|
|
18021
|
-
* (see ParameterDef.holds)
|
|
18022
|
-
*
|
|
18023
|
-
*
|
|
18038
|
+
* Report the references in the arguments of a macro by what its parameters
|
|
18039
|
+
* say each holds (see ParameterDef.holds): code is scanned as code and
|
|
18040
|
+
* markup as markup, but literal text (a placeholder, a passage name) holds
|
|
18041
|
+
* none. A macro without declared parameters, or arguments that do not
|
|
18042
|
+
* parse, are scanned as code.
|
|
18024
18043
|
*/
|
|
18025
|
-
function
|
|
18026
|
-
if (!
|
|
18027
|
-
|
|
18044
|
+
function scanArgs(token, params, onRef) {
|
|
18045
|
+
if (!token.rawArgs) return;
|
|
18046
|
+
if (params) {
|
|
18047
|
+
const pieces = [...argPieces(token.rawArgs, 0, params, token.name, "")];
|
|
18048
|
+
if (!pieces.some((piece) => piece.kind === "argument-error")) {
|
|
18049
|
+
for (const piece of pieces) if (piece.kind === "code") scanCode(piece.code, onRef, piece.goal);
|
|
18050
|
+
else if (piece.kind === "text") scanInterpolations(piece.text, onRef);
|
|
18051
|
+
return;
|
|
18052
|
+
}
|
|
18053
|
+
}
|
|
18054
|
+
scanCode(token.rawArgs, onRef);
|
|
18028
18055
|
}
|
|
18029
18056
|
/** Report the `$var` references in the tokens of `content`. */
|
|
18030
18057
|
function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf = () => void 0) {
|
|
@@ -18036,12 +18063,12 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf =
|
|
|
18036
18063
|
else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) if (isCodeAttribute(name)) scanSigilReferences(value, onRef);
|
|
18037
18064
|
else scanInterpolations(value, onRef, storeVarMacros);
|
|
18038
18065
|
else if (token.type === "macro" && !token.isClose) {
|
|
18039
|
-
|
|
18040
|
-
scanStringCode(token, parametersOf(token.name), onRef);
|
|
18066
|
+
scanArgs(token, parametersOf(token.name), onRef);
|
|
18041
18067
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
18042
18068
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
|
|
18043
18069
|
const quoted = QUOTED_VAR_ARG_RE.exec(first);
|
|
18044
18070
|
if (quoted) onRef(quoted[1]);
|
|
18071
|
+
else if (!/^["'`]/.test(first)) scanCode(first, onRef);
|
|
18045
18072
|
}
|
|
18046
18073
|
if (token.name === "do") {
|
|
18047
18074
|
let close = t + 1;
|
|
@@ -18071,9 +18098,8 @@ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_M
|
|
|
18071
18098
|
const storeVarSet = new Set(Array.from(storeVarMacros, (m) => m.toLowerCase()));
|
|
18072
18099
|
for (const [name, passage] of passages) {
|
|
18073
18100
|
if (NOT_MARKUP.has(name)) continue;
|
|
18074
|
-
const forLocals = extractForLocals(passage.content);
|
|
18075
18101
|
collectPassageRefs(passage.content, storeVarSet, (ref) => {
|
|
18076
|
-
const error = validateRef(ref, schema
|
|
18102
|
+
const error = validateRef(ref, schema);
|
|
18077
18103
|
if (error) errors.push(`Passage "${name}": ${error}`);
|
|
18078
18104
|
}, parametersOf);
|
|
18079
18105
|
}
|
|
@@ -18336,7 +18362,10 @@ defineMacro({
|
|
|
18336
18362
|
],
|
|
18337
18363
|
render(_props, ctx) {
|
|
18338
18364
|
try {
|
|
18339
|
-
const { currentExpr, maxExpr, labelMode } = meterArgs(
|
|
18365
|
+
const { currentExpr, maxExpr, labelMode } = meterArgs({
|
|
18366
|
+
...ctx.args,
|
|
18367
|
+
label: ctx.resolve?.(ctx.args.label)
|
|
18368
|
+
});
|
|
18340
18369
|
const current = Number(ctx.evaluate(currentExpr));
|
|
18341
18370
|
const max = Number(ctx.evaluate(maxExpr));
|
|
18342
18371
|
const pct = max === 0 ? 0 : Math.max(0, Math.min(100, current / max * 100));
|
|
@@ -18737,7 +18766,7 @@ defineMacro({
|
|
|
18737
18766
|
type: "passage"
|
|
18738
18767
|
}],
|
|
18739
18768
|
render({ children = [] }, ctx) {
|
|
18740
|
-
const display = ctx.args.text ?? "";
|
|
18769
|
+
const display = ctx.resolve?.(ctx.args.text ?? "") ?? "";
|
|
18741
18770
|
let passage = null;
|
|
18742
18771
|
let failure;
|
|
18743
18772
|
if (ctx.args.passage !== void 0) try {
|
|
@@ -10566,7 +10566,7 @@ function* argPieces(args, offset, params, macro, label) {
|
|
|
10566
10566
|
};
|
|
10567
10567
|
}
|
|
10568
10568
|
}
|
|
10569
|
-
yield* visit(params, values);
|
|
10569
|
+
yield* [...visit(params, values)].sort((a, b) => a.offset - b.offset);
|
|
10570
10570
|
}
|
|
10571
10571
|
/**
|
|
10572
10572
|
* Index of `part` in `text` from `from` on, else anywhere, else `from`: the
|
package/package.json
CHANGED
package/src/automation/runner.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { StoryAPI } from '../story-api';
|
|
9
9
|
import type { StoryAction } from '../action-registry';
|
|
10
10
|
import { errorMessage } from '../utils/error-message';
|
|
11
|
+
import { deepEqual } from '../structural';
|
|
11
12
|
|
|
12
13
|
export interface RunOptions {
|
|
13
14
|
onStep?: (stepIndex: number, step: AutomationStep) => void;
|
|
@@ -88,7 +89,7 @@ async function executeStep(
|
|
|
88
89
|
if (assert.variables) {
|
|
89
90
|
for (const [key, expected] of Object.entries(assert.variables)) {
|
|
90
91
|
const actual = story.get(key);
|
|
91
|
-
if (actual
|
|
92
|
+
if (!deepEqual(actual, expected)) {
|
|
92
93
|
errors.push({
|
|
93
94
|
step: index,
|
|
94
95
|
message: `Variable "${key}": expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
package/src/code-check.ts
CHANGED
|
@@ -16,7 +16,8 @@ defineMacro({
|
|
|
16
16
|
{ name: 'passage', type: 'passage' },
|
|
17
17
|
],
|
|
18
18
|
render({ children = [] }, ctx) {
|
|
19
|
-
|
|
19
|
+
// The label is markup: resolved once, for the element and its action
|
|
20
|
+
const display = ctx.resolve?.(ctx.args.text ?? '') ?? '';
|
|
20
21
|
let passage: string | null = null;
|
|
21
22
|
// Boxed: anything can be thrown, including null and other falsy values
|
|
22
23
|
let failure: { error: unknown } | undefined;
|
|
@@ -44,7 +44,11 @@ defineMacro({
|
|
|
44
44
|
],
|
|
45
45
|
render(_props, ctx) {
|
|
46
46
|
try {
|
|
47
|
-
const { currentExpr, maxExpr, labelMode } = meterArgs(
|
|
47
|
+
const { currentExpr, maxExpr, labelMode } = meterArgs({
|
|
48
|
+
...ctx.args,
|
|
49
|
+
// The label is markup: resolved before reading none, % or a unit
|
|
50
|
+
label: ctx.resolve?.(ctx.args.label),
|
|
51
|
+
});
|
|
48
52
|
const current = Number(ctx.evaluate!(currentExpr));
|
|
49
53
|
const max = Number(ctx.evaluate!(maxExpr));
|
|
50
54
|
const pct =
|
package/src/saves/storage.ts
CHANGED
|
@@ -315,7 +315,20 @@ function createIDBBackend(): StorageBackend {
|
|
|
315
315
|
make: (store: IDBObjectStore) => IDBRequest<R>,
|
|
316
316
|
): Promise<R> => {
|
|
317
317
|
const db = await openDB();
|
|
318
|
-
|
|
318
|
+
const tx = db.transaction(name, mode);
|
|
319
|
+
const result = idbReq(make(tx.objectStore(name)));
|
|
320
|
+
// A request can succeed and its transaction still roll back, so a
|
|
321
|
+
// write is done only when the transaction completes.
|
|
322
|
+
if (mode === 'readonly') return result;
|
|
323
|
+
const committed = new Promise<void>((resolve, reject) => {
|
|
324
|
+
tx.oncomplete = () => resolve();
|
|
325
|
+
tx.onabort = () =>
|
|
326
|
+
reject(
|
|
327
|
+
tx.error ?? new DOMException('Transaction aborted', 'AbortError'),
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
const [value] = await Promise.all([result, committed]);
|
|
331
|
+
return value;
|
|
319
332
|
};
|
|
320
333
|
|
|
321
334
|
return {
|
package/src/store.ts
CHANGED
|
@@ -120,7 +120,10 @@ let lastNavigationVars: Namespace = createNamespace();
|
|
|
120
120
|
* object) or deleted. A recorded value is a deep copy, so that changing a
|
|
121
121
|
* live value in place (a class instance is not frozen) cannot change
|
|
122
122
|
* history; instances of unregistered classes are kept, as the store keeps
|
|
123
|
-
* them (a save then refuses them).
|
|
123
|
+
* them (a save then refuses them). The copies are made together, so what
|
|
124
|
+
* variables share (an object, a cycle) stays shared in them, and a variable
|
|
125
|
+
* left as it is that shares with a replaced one is recorded again, to share
|
|
126
|
+
* with its copy.
|
|
124
127
|
*/
|
|
125
128
|
function computeVarPatches(
|
|
126
129
|
prev: Record<string, unknown>,
|
|
@@ -132,13 +135,32 @@ function computeVarPatches(
|
|
|
132
135
|
// whose inherited `constructor` is no variable
|
|
133
136
|
if (!hasOwn(curr, key)) forward.push({ key, deleted: true });
|
|
134
137
|
}
|
|
138
|
+
const seen = new Map<object, object>();
|
|
139
|
+
const record = (key: string) =>
|
|
140
|
+
forward.push({
|
|
141
|
+
key,
|
|
142
|
+
deleted: false,
|
|
143
|
+
value: deepClone(curr[key], { keepUnregistered: true, seen }),
|
|
144
|
+
});
|
|
145
|
+
const unchanged: string[] = [];
|
|
135
146
|
for (const key of Object.keys(curr)) {
|
|
136
|
-
if (!hasOwn(prev, key) || !Object.is(prev[key], curr[key]))
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
if (!hasOwn(prev, key) || !Object.is(prev[key], curr[key])) record(key);
|
|
148
|
+
else if (typeof curr[key] === 'object' && curr[key] !== null) {
|
|
149
|
+
unchanged.push(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Probe an unchanged variable with a copy of its own: it shares with a
|
|
153
|
+
// recorded one if the copy meets an object already copied
|
|
154
|
+
for (let found = seen.size > 0; found; ) {
|
|
155
|
+
found = false;
|
|
156
|
+
for (const key of unchanged) {
|
|
157
|
+
const probe = new Map<object, object>();
|
|
158
|
+
deepClone(curr[key], { keepUnregistered: true, seen: probe });
|
|
159
|
+
if (![...probe.keys()].some((object) => seen.has(object))) continue;
|
|
160
|
+
record(key);
|
|
161
|
+
unchanged.splice(unchanged.indexOf(key), 1);
|
|
162
|
+
found = true;
|
|
163
|
+
break;
|
|
142
164
|
}
|
|
143
165
|
}
|
|
144
166
|
return { forward };
|