@rohal12/spindle 0.58.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.
@@ -103,24 +103,41 @@ var LINK_SEPARATORS = [
103
103
  ["<-", true]
104
104
  ];
105
105
  var NO_SELECTORS = {};
106
- function parseLink(inner) {
106
+ /**
107
+ * A link's display text and target, from its text `inner` (at `at` in the
108
+ * input), with where the target is written.
109
+ */
110
+ function parseLink(inner, at) {
111
+ /** The trimmed text from `start` to `end` of inner, and where it is. */
112
+ const part = (start, end) => {
113
+ const raw = inner.slice(start, end);
114
+ const value = raw.trim();
115
+ const from = at + start + (value ? raw.indexOf(value) : 0);
116
+ return {
117
+ value,
118
+ start: from,
119
+ end: from + value.length
120
+ };
121
+ };
107
122
  for (const [separator, targetFirst] of LINK_SEPARATORS) {
108
123
  const idx = inner.indexOf(separator);
109
124
  if (idx === -1) continue;
110
- const before = inner.slice(0, idx).trim();
111
- const after = inner.slice(idx + separator.length).trim();
112
- return targetFirst ? {
113
- display: after,
114
- target: before
115
- } : {
116
- display: before,
117
- target: after
125
+ const before = part(0, idx);
126
+ const after = part(idx + separator.length, inner.length);
127
+ const [display, target] = targetFirst ? [after, before] : [before, after];
128
+ return {
129
+ display: display.value,
130
+ target: target.value,
131
+ targetStart: target.start,
132
+ targetEnd: target.end
118
133
  };
119
134
  }
120
- const trimmed = inner.trim();
135
+ const whole = part(0, inner.length);
121
136
  return {
122
- display: trimmed,
123
- target: trimmed
137
+ display: whole.value,
138
+ target: whole.value,
139
+ targetStart: whole.start,
140
+ targetEnd: whole.end
124
141
  };
125
142
  }
126
143
  function quoteArg(value) {
@@ -202,6 +219,8 @@ function tokenOf(x) {
202
219
  type: "link",
203
220
  display: x.display,
204
221
  target: x.target,
222
+ targetStart: x.targetStart,
223
+ targetEnd: x.targetEnd,
205
224
  ...span
206
225
  }, x);
207
226
  case "variable": return withSelectors({
@@ -733,8 +752,8 @@ function peg$parse(input, options) {
733
752
  function peg$f48() {
734
753
  return !textMode;
735
754
  }
736
- function peg$f49(s, inner) {
737
- return withSelectors(lexeme("link", location(), parseLink(inner)), s);
755
+ function peg$f49(s, at, inner) {
756
+ return withSelectors(lexeme("link", location(), parseLink(inner, at)), s);
738
757
  }
739
758
  function peg$f50() {
740
759
  return !textMode;
@@ -1783,7 +1802,7 @@ function peg$parse(input, options) {
1783
1802
  return s0;
1784
1803
  }
1785
1804
  function peg$parseLinkLexeme() {
1786
- let s0, s1, s2, s3, s4, s5;
1805
+ let s0, s1, s2, s3, s4, s5, s6;
1787
1806
  s0 = peg$currPos;
1788
1807
  peg$savedPos = peg$currPos;
1789
1808
  s1 = peg$f48();
@@ -1800,19 +1819,20 @@ function peg$parse(input, options) {
1800
1819
  if (s2 !== peg$FAILED) {
1801
1820
  s3 = peg$parseSelectors();
1802
1821
  if (s3 !== peg$FAILED) {
1803
- s4 = peg$currPos;
1804
- s5 = peg$parseLinkText();
1805
- s4 = input.substring(s4, peg$currPos);
1822
+ s4 = peg$parseHere();
1823
+ s5 = peg$currPos;
1824
+ s6 = peg$parseLinkText();
1825
+ s5 = input.substring(s5, peg$currPos);
1806
1826
  if (input.substr(peg$currPos, 2) === peg$c5) {
1807
- s5 = peg$c5;
1827
+ s6 = peg$c5;
1808
1828
  peg$currPos += 2;
1809
1829
  } else {
1810
- s5 = peg$FAILED;
1830
+ s6 = peg$FAILED;
1811
1831
  if (peg$silentFails === 0) peg$fail(peg$e9);
1812
1832
  }
1813
- if (s5 !== peg$FAILED) {
1833
+ if (s6 !== peg$FAILED) {
1814
1834
  peg$savedPos = s0;
1815
- s0 = peg$f49(s3, s4);
1835
+ s0 = peg$f49(s3, s4, s5);
1816
1836
  } else {
1817
1837
  peg$currPos = s0;
1818
1838
  s0 = peg$FAILED;
@@ -9441,6 +9461,22 @@ function regexEnd(src, start) {
9441
9461
  return -1;
9442
9462
  }
9443
9463
  /**
9464
+ * The value of `src` when it is exactly one `"…"` or `'…'` string literal,
9465
+ * as JavaScript reads it (`"\u0048all"` is `Hall`, `"\1"` is U+0001 in
9466
+ * the non-strict code passages run), otherwise `null`: other code, or a
9467
+ * literal that is not well-formed (`"\u{110000}"`).
9468
+ */
9469
+ function stringLiteralValue(src) {
9470
+ if (src[0] !== "\"" && src[0] !== "'") return null;
9471
+ try {
9472
+ const p = tokenizerAt(src, 0, "expression");
9473
+ p.nextToken();
9474
+ return p.type === types$1.string && p.end === src.length ? p.value : null;
9475
+ } catch {
9476
+ return null;
9477
+ }
9478
+ }
9479
+ /**
9444
9480
  * Scan the `"…"` or `'…'` string literal opening at `start`. `end` is the
9445
9481
  * index just past its closing quote, or `src.length` when it is unterminated
9446
9482
  * (`closed` false). A backslash escapes the character after it, so a quote
@@ -10146,20 +10182,45 @@ var PARAMETER_TYPES = [
10146
10182
  "options"
10147
10183
  ];
10148
10184
  /**
10185
+ * What the value of a `string` or `text` argument holds, for the check at
10186
+ * story start and for tooling (see code-check.ts):
10187
+ * - `markup`: markup the macro renders (`{button}`'s label): its markup
10188
+ * is checked.
10189
+ * - `text`: plain text the macro uses as written (`{checkbox}`'s label).
10190
+ * - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
10191
+ * - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
10192
+ * is checked as code, and its variable references against the schema.
10193
+ *
10194
+ * Without it, the argument of a macro with `interpolate` holds `markup`,
10195
+ * any other `text`.
10196
+ */
10197
+ var STRING_HOLDS = [
10198
+ "markup",
10199
+ "text",
10200
+ "passage",
10201
+ "expression",
10202
+ "statements"
10203
+ ];
10204
+ /**
10149
10205
  * Throw if a parameter `macro` declares (or an option of one) has no type,
10150
10206
  * or one that isn't a ParameterType: arguments are read by their type, and
10151
- * there is no default.
10207
+ * there is no default. Throw too if it declares what it `holds` wrongly.
10152
10208
  */
10153
10209
  function checkParameterTypes(macro, parameters) {
10210
+ const holdsFix = `Give a \`string\` or \`text\` parameter one of ${STRING_HOLDS.join(", ")} to hold (see docs/custom-macros.md#what-a-string-holds).`;
10154
10211
  for (const param of parameters) {
10155
10212
  const type = param.type;
10156
- if (!PARAMETER_TYPES.includes(type)) {
10157
- const problem = type === void 0 ? "has no type" : `has the unknown type ${JSON.stringify(type)}`;
10158
- throw new Error(`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. Give it one of the types ${PARAMETER_TYPES.join(", ")} (see docs/custom-macros.md#parameter-types), or declare no parameters and read props.rawArgs.`);
10159
- }
10213
+ if (!PARAMETER_TYPES.includes(type)) throw parameterError(macro, param, type === void 0 ? "has no type" : `has the unknown type ${JSON.stringify(type)}`, `Give it one of the types ${PARAMETER_TYPES.join(", ")} (see docs/custom-macros.md#parameter-types), or declare no parameters and read props.rawArgs.`);
10214
+ const holds = param.holds;
10215
+ if (holds !== void 0 && type !== "string" && type !== "text") throw parameterError(macro, param, `is of the type ${type}, which holds what it is`, holdsFix);
10216
+ if (holds !== void 0 && !STRING_HOLDS.includes(holds)) throw parameterError(macro, param, `holds the unknown ${JSON.stringify(holds)}`, holdsFix);
10160
10217
  if (param.parameters) checkParameterTypes(macro, param.parameters);
10161
10218
  }
10162
10219
  }
10220
+ /** The error for the parameter `param` of `macro`: what is wrong, and the fix. */
10221
+ function parameterError(macro, param, problem, fix) {
10222
+ return /* @__PURE__ */ new Error(`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ${fix}`);
10223
+ }
10163
10224
  var metadataRegistry = new NameMap();
10164
10225
  function registerMacroMetadata(name, metadata) {
10165
10226
  metadataRegistry.set(name, metadata);
@@ -10242,37 +10303,6 @@ function readWholeQuoted(src) {
10242
10303
  const quoted = readQuoted(src, 0);
10243
10304
  return quoted && quoted.end === src.length ? quoted.value : null;
10244
10305
  }
10245
- var JS_SIMPLE_ESCAPES = {
10246
- n: "\n",
10247
- t: " ",
10248
- r: "\r",
10249
- b: "\b",
10250
- f: "\f",
10251
- v: "\v",
10252
- "0": "\0"
10253
- };
10254
- /** Undo every escape of a JavaScript string literal's body. */
10255
- function decodeJsEscapes(body) {
10256
- return body.replace(/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\n\r\u2028\u2029])|([\s\S]))/g, (_, braced, unicode, hex, lineBreak, other) => {
10257
- if (lineBreak !== void 0) return "";
10258
- const code = braced ?? unicode ?? hex;
10259
- if (code !== void 0) {
10260
- const point = parseInt(code, 16);
10261
- return point <= 1114111 ? String.fromCodePoint(point) : "";
10262
- }
10263
- return JS_SIMPLE_ESCAPES[other] ?? other;
10264
- });
10265
- }
10266
- /**
10267
- * The value of `src` when it is exactly one `"…"` or `'…'` string, read as
10268
- * the JavaScript literal it is (`"\u0048all"` is `Hall`), otherwise `null`.
10269
- * `readWholeQuoted` keeps other escapes as written, for labels and code.
10270
- */
10271
- function readWholeJsString(src) {
10272
- const quoted = readQuoted(src, 0);
10273
- if (!quoted || quoted.end !== src.length) return null;
10274
- return decodeJsEscapes(src.slice(1, -1));
10275
- }
10276
10306
  /**
10277
10307
  * Strip an optional quote from each end of an unquoted-or-loosely-quoted
10278
10308
  * argument (`"Red`, `Red'`), leaving at least one character. This is the
@@ -13050,7 +13080,15 @@ function createIDBBackend() {
13050
13080
  const rows = name === "meta";
13051
13081
  /** Run one request in a transaction of its own. */
13052
13082
  const request = async (mode, make) => {
13053
- return idbReq(make((await openDB()).transaction(name, mode).objectStore(name)));
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;
13054
13092
  };
13055
13093
  return {
13056
13094
  async get(key) {
@@ -13289,15 +13327,22 @@ var ERROR_HIDDEN_KEYS = [
13289
13327
  "errors"
13290
13328
  ];
13291
13329
  function deepClone(value, options = {}) {
13292
- const seen = /* @__PURE__ */ new Map();
13330
+ const seen = options.seen ?? /* @__PURE__ */ new Map();
13293
13331
  /** Record `copy` as the copy of `obj` before its contents are copied. */
13294
13332
  const keep = (obj, copy) => {
13295
13333
  seen.set(obj, copy);
13296
13334
  return copy;
13297
13335
  };
13298
- /** Copy the own (enumerable) keys of `obj` into `copy`. */
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
+ */
13299
13340
  function copyKeys(obj, copy) {
13300
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
+ }
13301
13346
  return copy;
13302
13347
  }
13303
13348
  function cloneBuiltin(val) {
@@ -13380,65 +13425,83 @@ function loosePairs() {
13380
13425
  /**
13381
13426
  * Pairs that also require the same reference structure: each object of one
13382
13427
  * value pairs with exactly one of the other, so shared references and
13383
- * 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`.
13384
13430
  */
13385
- function strictPairs() {
13386
- const ab = /* @__PURE__ */ new Map();
13387
- const ba = /* @__PURE__ */ new Map();
13388
- const pairs = (a, b) => {
13431
+ function strictPairs(ab = /* @__PURE__ */ new Map(), ba = /* @__PURE__ */ new Map()) {
13432
+ const added = [];
13433
+ const pairs = ((a, b) => {
13389
13434
  const known = ab.get(a);
13390
13435
  if (known !== void 0) return known === b;
13391
13436
  if (ba.has(b)) return false;
13392
13437
  ab.set(a, b);
13393
13438
  ba.set(b, a);
13394
- };
13439
+ added.push([a, b]);
13440
+ });
13395
13441
  pairs.strict = true;
13442
+ pairs.added = added;
13396
13443
  return pairs;
13397
13444
  }
13398
13445
  /**
13399
- * Whether `a` and `b` are equal as deepEqual() has it and also share
13400
- * references at the same places (a save of one loads as the other).
13401
- */
13402
- function deepEqualStrict(a, b) {
13403
- return equal(a, b, strictPairs());
13404
- }
13405
- /**
13406
13446
  * `curr` with each part that equals (see deepEqualStrict) the part of
13407
13447
  * `prev` at the same place replaced by `prev`'s: the history moments of a
13408
13448
  * save then share what did not change between them, which a save stores
13409
13449
  * once. Containers on the way to a change are copied; neither value is
13410
- * changed. Shared references and cycles within `curr` stay so.
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.
13411
13453
  */
13412
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. */
13413
13457
  const done = /* @__PURE__ */ new Map();
13414
- function share(p, c) {
13415
- if (p === c || !isObjectValue(c) || !isObjectValue(p)) return c;
13416
- if (done.has(c)) return done.get(c);
13417
- if (deepEqualStrict(p, c)) {
13418
- done.set(c, p);
13419
- return p;
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);
13420
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;
13421
13485
  if (c instanceof Map && p instanceof Map) {
13422
13486
  const out = /* @__PURE__ */ new Map();
13423
13487
  done.set(c, out);
13424
- for (const [k, v] of c) out.set(k, p.has(k) ? share(p.get(k), v) : v);
13488
+ for (const [k, v] of c) out.set(fresh(k), share(p.get(k), v));
13425
13489
  return out;
13426
13490
  }
13427
13491
  if (Array.isArray(c) && Array.isArray(p)) {
13428
13492
  const out = new Array(c.length);
13429
13493
  done.set(c, out);
13430
- for (let i = 0; i < c.length; i++) if (i in c) out[i] = i < p.length ? share(p[i], c[i]) : c[i];
13494
+ for (let i = 0; i < c.length; i++) if (i in c) out[i] = share(p[i], c[i]);
13431
13495
  return out;
13432
13496
  }
13433
13497
  if (mergesWith(p, c, false)) {
13434
13498
  const out = Object.create(Object.getPrototypeOf(c));
13435
13499
  done.set(c, out);
13436
13500
  const cr = c;
13437
- for (const key of Object.keys(cr)) setOwn(out, key, hasOwn(p, key) ? share(p[key], cr[key]) : cr[key]);
13501
+ for (const key of Object.keys(cr)) setOwn(out, key, share(hasOwn(p, key) ? p[key] : void 0, cr[key]));
13438
13502
  return out;
13439
13503
  }
13440
- done.set(c, c);
13441
- return c;
13504
+ return fresh(c);
13442
13505
  }
13443
13506
  return share(prev, curr);
13444
13507
  }
@@ -14912,7 +14975,10 @@ var lastNavigationVars = createNamespace();
14912
14975
  * object) or deleted. A recorded value is a deep copy, so that changing a
14913
14976
  * live value in place (a class instance is not frozen) cannot change
14914
14977
  * history; instances of unregistered classes are kept, as the store keeps
14915
- * 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.
14916
14982
  */
14917
14983
  function computeVarPatches(prev, curr) {
14918
14984
  const forward = [];
@@ -14920,11 +14986,33 @@ function computeVarPatches(prev, curr) {
14920
14986
  key,
14921
14987
  deleted: true
14922
14988
  });
14923
- for (const key of Object.keys(curr)) if (!hasOwn(prev, key) || !Object.is(prev[key], curr[key])) forward.push({
14989
+ const seen = /* @__PURE__ */ new Map();
14990
+ const record = (key) => forward.push({
14924
14991
  key,
14925
14992
  deleted: false,
14926
- value: deepClone(curr[key], { keepUnregistered: true })
14993
+ value: deepClone(curr[key], {
14994
+ keepUnregistered: true,
14995
+ seen
14996
+ })
14927
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
+ }
14928
15016
  return { forward };
14929
15017
  }
14930
15018
  /**
@@ -16567,7 +16655,9 @@ var MacroArgumentError = class extends Error {
16567
16655
  this.name = "MacroArgumentError";
16568
16656
  }
16569
16657
  };
16570
- function readValue(param, src) {
16658
+ /** Read `src`, at `place`, as `param` says. */
16659
+ function readValue(param, src, place) {
16660
+ place.spans?.set(param, [place.at, place.at + src.length]);
16571
16661
  switch (param.type) {
16572
16662
  case "string": {
16573
16663
  const value = readWholeQuoted(src);
@@ -16578,7 +16668,7 @@ function readValue(param, src) {
16578
16668
  case "names": return src.split(",").map((name) => name.trim());
16579
16669
  case "delay": return parseDelay(src);
16580
16670
  case "number": return Number(src);
16581
- case "options": return readOptions(param.parameters ?? [], src);
16671
+ case "options": return readOptions(param.parameters ?? [], src, place);
16582
16672
  default: return src;
16583
16673
  }
16584
16674
  }
@@ -16596,11 +16686,12 @@ function matchAt(re, src, pos) {
16596
16686
  }
16597
16687
  /**
16598
16688
  * Read keyword options (`goto "X" priority 5 once`). A keyword takes the
16599
- * quoted string or digit run after it as its value; keywords that aren't
16600
- * declared, and their values, are skipped. A keyword of a `string` parameter
16601
- * must be followed by a quoted string.
16689
+ * quoted string (with or without whitespace before it) or digit run after
16690
+ * it as its value; keywords that aren't declared, and their values, are
16691
+ * skipped. A keyword of a `string` parameter must be followed by a quoted
16692
+ * string.
16602
16693
  */
16603
- function readOptions(params, src) {
16694
+ function readOptions(params, src, { at, spans }) {
16604
16695
  const options = {};
16605
16696
  let i = 0;
16606
16697
  while (i < src.length) {
@@ -16611,22 +16702,20 @@ function readOptions(params, src) {
16611
16702
  }
16612
16703
  i += key.length;
16613
16704
  let val;
16614
- let quoted = null;
16615
16705
  let j = i;
16616
16706
  while (j < src.length && isWhitespace(src[j])) j++;
16617
- if (j > i) {
16618
- quoted = readQuoted(src, j);
16619
- const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
16620
- if (quoted) {
16621
- val = quoted.value;
16622
- i = quoted.end;
16623
- } else if (digits) {
16624
- val = digits;
16625
- i = j + digits.length;
16626
- }
16707
+ const quoted = readQuoted(src, j);
16708
+ const digits = quoted || j === i ? null : matchAt(DIGITS_RE, src, j);
16709
+ if (quoted) {
16710
+ val = quoted.value;
16711
+ i = quoted.end;
16712
+ } else if (digits) {
16713
+ val = digits;
16714
+ i = j + digits.length;
16627
16715
  }
16628
16716
  const param = params.find((p) => p.name === key);
16629
- if (param?.type === "string" && (!quoted || val === void 0)) throw new MacroArgumentError(param, matchAt(WORD_OR_PUNCT_RE, src, j) ?? "");
16717
+ if (param && param.type !== "flag" && val !== void 0) spans?.set(param, [at + j, at + i]);
16718
+ if (param?.type === "string" && !quoted) throw new MacroArgumentError(param, matchAt(WORD_OR_PUNCT_RE, src, j) ?? "");
16630
16719
  if (param?.type === "flag") options[key] = true;
16631
16720
  else if (param?.type === "number") options[key] = Number(val ?? 0);
16632
16721
  else if (param) options[key] = val;
@@ -16636,21 +16725,21 @@ function readOptions(params, src) {
16636
16725
  /** `inline %name` reads a transient variable; `inline % 2` is modulo. */
16637
16726
  var LEADING_OPERATOR_RE = /^(?:[-+*/&|^=<>?:,.]|%(?![A-Za-z_]))/;
16638
16727
  /**
16639
- * The rest of `src` without the flag `word` as its last or first term, or
16640
- * `null` if it has no such flag. Next to a binary operator the word is an
16641
- * operand (`"a" + inline`), not a flag; the closing `/` of a regex literal
16642
- * is not one.
16728
+ * Where the rest of `src` is without the flag `word` as its last or first
16729
+ * term, or `null` if it has no such flag. Next to a binary operator the
16730
+ * word is an operand (`"a" + inline`), not a flag; the closing `/` of a
16731
+ * regex literal is not one.
16643
16732
  */
16644
16733
  function takeFlag(src, word) {
16645
16734
  const spans = terms(src);
16646
16735
  if (spans.length < 2) return null;
16647
16736
  if (slice(src, spans[spans.length - 1]) === word) {
16648
- const rest = src.slice(0, spans[spans.length - 2][1]);
16649
- if (!endsWithOperator(rest)) return rest;
16737
+ const rest = [0, spans[spans.length - 2][1]];
16738
+ if (!endsWithOperator(slice(src, rest))) return rest;
16650
16739
  }
16651
16740
  if (slice(src, spans[0]) === word) {
16652
- const rest = src.slice(spans[1][0]);
16653
- if (!LEADING_OPERATOR_RE.test(rest)) return rest;
16741
+ const rest = [spans[1][0], src.length];
16742
+ if (!LEADING_OPERATOR_RE.test(slice(src, rest))) return rest;
16654
16743
  }
16655
16744
  return null;
16656
16745
  }
@@ -16678,42 +16767,55 @@ function findSeparator(src, word) {
16678
16767
  return null;
16679
16768
  }
16680
16769
  var takesRest = (param) => param.type === "expression" || param.type === "statements" || param.type === "passage" || param.type === "text";
16681
- /** Read the parameters of one group from `src` (`null`: no text) into `args`. */
16682
- function readGroup(params, src, args) {
16683
- let text = src?.trim() ?? "";
16770
+ /** The text of `src` without the whitespace around it, and where it is. */
16771
+ function trimmed(src, at) {
16772
+ const text = src.trim();
16773
+ return [text, text ? at + src.indexOf(text) : at];
16774
+ }
16775
+ /**
16776
+ * Read the parameters of one group from `src` (`null`: no text), which is
16777
+ * at `at` in the raw arguments, into `args`.
16778
+ */
16779
+ function readGroup(params, src, args, { at: srcAt, spans }) {
16780
+ let [text, at] = trimmed(src ?? "", srcAt);
16684
16781
  for (const flag of params.filter((p) => p.type === "flag")) {
16685
16782
  const rest = src === null ? null : takeFlag(text, flag.name);
16686
16783
  args[flag.name] = rest !== null;
16687
- text = rest ?? text;
16784
+ if (rest) [text, at] = trimmed(slice(text, rest), at + rest[0]);
16688
16785
  }
16689
16786
  const positional = params.filter((p) => p.type !== "flag");
16690
16787
  if (positional.length === 0) return;
16691
16788
  const lastText = positional.findIndex((p, i) => takesRest(p) && !positional.slice(i + 1).some(takesRest));
16692
16789
  const restIndex = lastText < 0 ? positional.length - 1 : lastText;
16693
- const read = (param, value) => value === "" ? unset(param) : readValue(param, value);
16790
+ /** Read `param` from the text from `start` to `end`. */
16791
+ const read = (param, [start, end]) => start === end ? unset(param) : readValue(param, text.slice(start, end), {
16792
+ at: at + start,
16793
+ spans
16794
+ });
16694
16795
  if (positional.length === 1) {
16695
- args[positional[0].name] = read(positional[0], text);
16796
+ args[positional[0].name] = read(positional[0], [0, text.length]);
16696
16797
  return;
16697
16798
  }
16698
- const spans = terms(text);
16799
+ const parts = terms(text);
16699
16800
  let from = 0;
16700
- let to = spans.length;
16701
- for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
16702
- const readTrailing = (param, value) => {
16801
+ let to = parts.length;
16802
+ for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, parts[from++]) : unset(param);
16803
+ const readTrailing = (param, span) => {
16703
16804
  try {
16704
- return read(param, value);
16805
+ return read(param, span);
16705
16806
  } catch (error) {
16706
16807
  if (error instanceof MacroArgumentError) return void 0;
16707
16808
  throw error;
16708
16809
  }
16709
16810
  };
16710
16811
  for (const param of positional.slice(restIndex + 1).reverse()) {
16711
- const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? readTrailing(param, slice(text, spans[to - 1])) : void 0;
16812
+ const value = from < to && !endsWithOperator(text.slice(0, parts[to - 1][0])) ? readTrailing(param, parts[to - 1]) : void 0;
16712
16813
  args[param.name] = value ?? unset(param);
16713
16814
  if (value !== void 0) to--;
16815
+ else spans?.delete(param);
16714
16816
  }
16715
16817
  const rest = positional[restIndex];
16716
- args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
16818
+ args[rest.name] = from < to ? read(rest, [parts[from][0], parts[to - 1][1]]) : unset(rest);
16717
16819
  }
16718
16820
  /** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
16719
16821
  var DELAY_PARAMETER = {
@@ -16721,32 +16823,43 @@ var DELAY_PARAMETER = {
16721
16823
  type: "delay",
16722
16824
  required: true
16723
16825
  };
16724
- /** Read `rawArgs` into the declared `parameters` (see above). */
16725
- function parseMacroArgs(rawArgs, parameters) {
16826
+ /**
16827
+ * Read `rawArgs` into the declared `parameters` (see above). With `spans`,
16828
+ * note there where each argument set was read.
16829
+ */
16830
+ function parseMacroArgs(rawArgs, parameters, spans) {
16726
16831
  const args = {};
16727
- let rest = rawArgs.trim();
16832
+ let [rest, restAt] = trimmed(rawArgs, 0);
16728
16833
  let groupStart = 0;
16729
16834
  parameters.forEach((separator, i) => {
16730
16835
  if (separator.type !== "separator") return;
16731
16836
  const at = rest === null ? null : findSeparator(rest, separator.name);
16732
16837
  args[separator.name] = at !== null;
16733
16838
  const before = at ? rest.slice(0, at[0]) : rest;
16734
- readGroup(parameters.slice(groupStart, i), before, args);
16839
+ readGroup(parameters.slice(groupStart, i), before, args, {
16840
+ at: restAt,
16841
+ spans
16842
+ });
16735
16843
  rest = at ? rest.slice(at[1]) : null;
16844
+ if (at) restAt += at[1];
16736
16845
  groupStart = i + 1;
16737
16846
  });
16738
- readGroup(parameters.slice(groupStart), rest, args);
16847
+ readGroup(parameters.slice(groupStart), rest, args, {
16848
+ at: restAt,
16849
+ spans
16850
+ });
16739
16851
  return args;
16740
16852
  }
16741
16853
  /**
16742
16854
  * Read a `passage` argument as written (`{goto "Hall"}`, `{goto $room}`): a
16743
- * quoted string is the name its JavaScript literal has (`"\u0048all"` is
16744
- * `Hall`), anything else an expression, whose value is the name when it
16855
+ * string literal is the name JavaScript reads from it (`"\u0048all"` is
16856
+ * `Hall`), anything else (a literal that is not well-formed too) an
16857
+ * expression, whose value is the name when it
16745
16858
  * runs (see evaluatePassageName).
16746
16859
  */
16747
16860
  function passageTarget(arg) {
16748
16861
  const expression = arg.trim();
16749
- const name = readWholeJsString(expression);
16862
+ const name = stringLiteralValue(expression);
16750
16863
  return name === null ? {
16751
16864
  kind: "expression",
16752
16865
  expression
@@ -16795,6 +16908,7 @@ function parseVarArgs(rawArgs) {
16795
16908
  var OPTION_PARAMETERS = [{
16796
16909
  name: "value",
16797
16910
  type: "string",
16911
+ holds: "text",
16798
16912
  required: true
16799
16913
  }];
16800
16914
  /** The parameters of the built-in sub-macros that take arguments. */
@@ -17269,30 +17383,59 @@ var CONDITION_MACROS = new Set([
17269
17383
  "elseif",
17270
17384
  "case"
17271
17385
  ]);
17272
- /** Code inside the quoted strings of a macro's arguments, by parameter. */
17273
- var CODE_IN_STRINGS = new Map([["watch", {
17274
- condition: "expression",
17275
- run: "statements"
17276
- }]]);
17277
- /** The `string` parameters of a macro that name a passage. */
17278
- var PASSAGE_STRINGS = new Map([["watch", ["goto", "dialog"]]]);
17279
- /**
17280
- * The `text` and `string` parameters of a macro that the runtime keeps as
17281
- * written, not as markup: an option's value, the name of a watcher.
17282
- */
17283
- var LITERAL_STRINGS = new Map([
17284
- ["option", ["value"]],
17285
- ["watch", ["name"]],
17286
- ["unwatch", ["name"]]
17287
- ]);
17288
17386
  /** Block macros whose body is the name of a passage. */
17289
17387
  var PASSAGE_BODIES = new Set(["dialog"]);
17388
+ /**
17389
+ * `params` with each `string` and `text` parameter (options too) saying
17390
+ * what it holds: what it declares, else `holds`.
17391
+ */
17392
+ function withHolds(params, holds) {
17393
+ return params.map((param) => {
17394
+ if (param.parameters) return {
17395
+ ...param,
17396
+ parameters: withHolds(param.parameters, holds)
17397
+ };
17398
+ return (param.type === "string" || param.type === "text") && !param.holds ? {
17399
+ ...param,
17400
+ holds
17401
+ } : param;
17402
+ });
17403
+ }
17404
+ /**
17405
+ * The declared parameters of `macros`, by name in any case, and of the
17406
+ * built-in sub-macros (`{option}`): what the story-start check, reference
17407
+ * collection and tooling read arguments by. Each `string` and `text`
17408
+ * parameter says what it holds: what it declares, else `markup` for a macro
17409
+ * with `interpolate` (which can resolve markup) and `text` for any other.
17410
+ */
17411
+ function parameterLookup(macros) {
17412
+ const parameters = new NameMap();
17413
+ for (const { name, parameters: params, interpolate } of macros) if (params) parameters.set(name, withHolds(params, holdsByDefault(interpolate)));
17414
+ return (name) => {
17415
+ let params = parameters.get(name);
17416
+ if (!params) {
17417
+ const sub = subMacroParameters(name);
17418
+ if (sub) parameters.set(name, params = withHolds(sub, "text"));
17419
+ }
17420
+ return params;
17421
+ };
17422
+ }
17423
+ /** What a `string` or `text` argument holds when its parameter doesn't say. */
17424
+ var holdsByDefault = (interpolate) => interpolate ? "markup" : "text";
17290
17425
  /** The goal of the code an argument of this type holds, if it is code. */
17291
17426
  function codeGoal(param) {
17292
17427
  if (param.type === "expression" || param.type === "passage") return "expression";
17293
17428
  if (param.type === "statements") return "statements";
17294
17429
  }
17295
17430
  /**
17431
+ * What a `string` or `text` argument holds: what its parameter says (see
17432
+ * parameterLookup), else markup, which is checked.
17433
+ */
17434
+ function stringHolds(param) {
17435
+ if (param.type !== "string" && param.type !== "text") return void 0;
17436
+ return param.holds ?? "markup";
17437
+ }
17438
+ /**
17296
17439
  * The code and the markup-holding text in `tokens`, the tokens of `src`.
17297
17440
  * `parametersOf` gives the declared parameters of a macro, if it has any.
17298
17441
  */
@@ -17304,8 +17447,8 @@ function* codeAndText(src, tokens, parametersOf) {
17304
17447
  if (token.type === "link") yield {
17305
17448
  kind: "passage",
17306
17449
  name: token.target,
17307
- offset: locate(src, token.target, token.start),
17308
- length: token.target.length,
17450
+ offset: token.targetStart,
17451
+ length: token.targetEnd - token.targetStart,
17309
17452
  macro: "link",
17310
17453
  label: src.slice(token.start, token.end)
17311
17454
  };
@@ -17316,32 +17459,35 @@ function* codeAndText(src, tokens, parametersOf) {
17316
17459
  goal: "expression",
17317
17460
  label: `{${token.expression}}`
17318
17461
  };
17319
- else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) {
17320
- const at = locate(src, value, token.start);
17321
- if (!isCodeAttribute(name)) {
17322
- yield {
17323
- kind: "text",
17324
- text: value,
17325
- offset: at,
17326
- where: `In the ${name} attribute of <${token.tag}>: `
17327
- };
17328
- continue;
17329
- }
17330
- let from = 0;
17331
- for (const part of splitSigilTemplate(value)) {
17332
- if (!("expr" in part)) continue;
17333
- const k = locate(value, part.expr, from);
17334
- from = k + part.expr.length;
17335
- yield {
17336
- kind: "code",
17337
- code: part.expr,
17338
- offset: at + k,
17339
- goal: "expression",
17340
- label: `{${part.expr}} in the ${name} attribute of <${token.tag}>`
17341
- };
17462
+ else if (token.type === "html") {
17463
+ let cursor = token.start;
17464
+ for (const [name, value] of Object.entries(token.attributes)) {
17465
+ const at = locate(src, value, cursor);
17466
+ cursor = at + value.length;
17467
+ if (!isCodeAttribute(name)) {
17468
+ yield {
17469
+ kind: "text",
17470
+ text: value,
17471
+ offset: at,
17472
+ where: `In the ${name} attribute of <${token.tag}>: `
17473
+ };
17474
+ continue;
17475
+ }
17476
+ let from = 0;
17477
+ for (const part of splitSigilTemplate(value)) {
17478
+ if (!("expr" in part)) continue;
17479
+ const k = locate(value, part.expr, from);
17480
+ from = k + part.expr.length;
17481
+ yield {
17482
+ kind: "code",
17483
+ code: part.expr,
17484
+ offset: at + k,
17485
+ goal: "expression",
17486
+ label: `{${part.expr}} in the ${name} attribute of <${token.tag}>`
17487
+ };
17488
+ }
17342
17489
  }
17343
- }
17344
- else if (token.type === "macro" && !token.isClose) {
17490
+ } else if (token.type === "macro" && !token.isClose) {
17345
17491
  const name = token.name.toLowerCase();
17346
17492
  if (name === "do") {
17347
17493
  const close = closeOf(t, name);
@@ -17361,12 +17507,13 @@ function* codeAndText(src, tokens, parametersOf) {
17361
17507
  const close = closeOf(t, name);
17362
17508
  const body = tokens.slice(t + 1, close);
17363
17509
  if (close > t + 1 && body.every((b) => b.type === "text")) {
17364
- const passage = src.slice(token.end, tokens[close].start).trim().replace(/^["']|["']$/g, "");
17510
+ const text = src.slice(token.end, tokens[close].start);
17511
+ const written = text.trim();
17365
17512
  yield {
17366
17513
  kind: "passage",
17367
- name: passage,
17368
- offset: locate(src, passage, token.end),
17369
- length: passage.length,
17514
+ name: written.replace(/^["']|["']$/g, ""),
17515
+ offset: token.end + text.indexOf(written),
17516
+ length: written.length,
17370
17517
  macro: name,
17371
17518
  label: src.slice(token.start, token.end)
17372
17519
  };
@@ -17374,7 +17521,8 @@ function* codeAndText(src, tokens, parametersOf) {
17374
17521
  }
17375
17522
  if (!args) continue;
17376
17523
  const label = `{${token.name} ${args}}`;
17377
- const argsAt = locate(src, args, token.start + 1);
17524
+ const tag = src.slice(token.start, token.end - 1);
17525
+ const argsAt = token.start + tag.trimEnd().length - args.length;
17378
17526
  if (CONDITION_MACROS.has(name)) {
17379
17527
  yield {
17380
17528
  kind: "code",
@@ -17393,8 +17541,9 @@ function* codeAndText(src, tokens, parametersOf) {
17393
17541
  /** The code and text in the arguments `args` (at `offset`) of `macro`. */
17394
17542
  function* argPieces(args, offset, params, macro, label) {
17395
17543
  let values;
17544
+ const spans = /* @__PURE__ */ new Map();
17396
17545
  try {
17397
- values = parseMacroArgs(args, params);
17546
+ values = parseMacroArgs(args, params, spans);
17398
17547
  } catch (error) {
17399
17548
  if (!(error instanceof MacroArgumentError)) throw error;
17400
17549
  yield {
@@ -17405,11 +17554,7 @@ function* argPieces(args, offset, params, macro, label) {
17405
17554
  };
17406
17555
  return;
17407
17556
  }
17408
- const key = macro.toLowerCase();
17409
- const inStrings = CODE_IN_STRINGS.get(key) ?? {};
17410
- const passageStrings = PASSAGE_STRINGS.get(key) ?? [];
17411
- const literalStrings = LITERAL_STRINGS.get(key) ?? [];
17412
- let cursor = 0;
17557
+ const name = macro.toLowerCase();
17413
17558
  /** The pieces of the parameters `list`, with their values in `from`. */
17414
17559
  function* visit(list, from) {
17415
17560
  for (const param of list) {
@@ -17418,47 +17563,62 @@ function* argPieces(args, offset, params, macro, label) {
17418
17563
  yield* visit(param.parameters, value ?? {});
17419
17564
  continue;
17420
17565
  }
17421
- if (typeof value !== "string" || !value.trim()) continue;
17422
- const at = locate(args, value, cursor);
17423
- if (at >= cursor) cursor = at + value.length;
17424
- const goal = codeGoal(param) ?? inStrings[param.name];
17566
+ const span = spans.get(param);
17567
+ if (typeof value !== "string" || !value.trim() || !span) continue;
17568
+ const [start, end] = span;
17569
+ const written = args.slice(start, end);
17570
+ /** The name as written (quotes included), at the argument. */
17571
+ const passage = (passageName) => ({
17572
+ kind: "passage",
17573
+ name: passageName,
17574
+ offset: offset + start,
17575
+ length: end - start,
17576
+ macro: name,
17577
+ label
17578
+ });
17579
+ const goal = codeGoal(param);
17580
+ if (param.type === "passage") {
17581
+ const target = passageTarget(value);
17582
+ if (target.kind === "name") {
17583
+ yield passage(target.name);
17584
+ continue;
17585
+ }
17586
+ }
17425
17587
  if (goal) {
17426
17588
  const piece = {
17427
17589
  kind: "code",
17428
17590
  code: value,
17429
- offset: offset + at,
17591
+ offset: offset + start,
17430
17592
  goal,
17431
17593
  label
17432
17594
  };
17433
17595
  if (param.type === "passage") {
17434
17596
  piece.passage = true;
17435
- piece.macro = macro.toLowerCase();
17597
+ piece.macro = name;
17436
17598
  }
17437
- if (!codeGoal(param)) piece.inString = true;
17438
17599
  yield piece;
17600
+ continue;
17439
17601
  }
17440
- const written = param.type === "passage" ? value.trim() : value;
17441
- const target = param.type === "passage" ? passageTarget(value) : passageStrings.includes(param.name) ? {
17442
- kind: "name",
17443
- name: value
17444
- } : null;
17445
- if (target?.kind === "name") yield {
17446
- kind: "passage",
17447
- name: target.name,
17448
- offset: offset + at + value.indexOf(written),
17449
- length: written.length,
17450
- macro: macro.toLowerCase(),
17451
- label
17602
+ const at = offset + start + (value !== written && /^["']/.test(written) ? 1 : 0);
17603
+ const holds = stringHolds(param);
17604
+ if (holds === "expression" || holds === "statements") yield {
17605
+ kind: "code",
17606
+ code: value,
17607
+ offset: at,
17608
+ goal: holds,
17609
+ label,
17610
+ inString: true
17452
17611
  };
17453
- if (!goal && (param.type === "text" || param.type === "string") && !literalStrings.includes(param.name)) yield {
17612
+ else if (holds === "passage") yield passage(value);
17613
+ else if (holds === "markup") yield {
17454
17614
  kind: "text",
17455
17615
  text: value,
17456
- offset: offset + at,
17616
+ offset: at,
17457
17617
  where: `In the ${param.name} of {${macro}}: `
17458
17618
  };
17459
17619
  }
17460
17620
  }
17461
- yield* visit(params, values);
17621
+ yield* [...visit(params, values)].sort((a, b) => a.offset - b.offset);
17462
17622
  }
17463
17623
  /**
17464
17624
  * Index of `part` in `text` from `from` on, else anywhere, else `from`: the
@@ -17480,10 +17640,12 @@ function locate(text, part, from) {
17480
17640
  var WIDGET_PARAMETERS = [{
17481
17641
  name: "name",
17482
17642
  type: "text",
17643
+ holds: "text",
17483
17644
  required: true
17484
17645
  }, {
17485
17646
  name: "parameters",
17486
- type: "text"
17647
+ type: "text",
17648
+ holds: "text"
17487
17649
  }];
17488
17650
  /**
17489
17651
  * The widget a {widget} definition's arguments declare: its name and its
@@ -17643,7 +17805,7 @@ function validateMarkup(passages, options) {
17643
17805
  report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
17644
17806
  }
17645
17807
  };
17646
- const parametersOf = (name) => options.parametersOf?.(name) ?? subMacroParameters(name);
17808
+ const parametersOf = options.parametersOf ?? (() => void 0);
17647
17809
  /**
17648
17810
  * Check `tokens`, the tokens of `src`, which starts at `base` in the
17649
17811
  * passage: their macros, the code in them and the markup in their text.
@@ -17711,7 +17873,6 @@ function declarationRegex(sigil) {
17711
17873
  var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
17712
17874
  /** Quoted first argument of an input macro naming a story variable. */
17713
17875
  var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
17714
- var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
17715
17876
  var VALID_VAR_TYPES = new Set([
17716
17877
  "number",
17717
17878
  "string",
@@ -17774,30 +17935,14 @@ function parseStoryVariables(content, sigil = "$") {
17774
17935
  return schema;
17775
17936
  }
17776
17937
  /**
17777
- * Extract for-loop local variable names from passage content.
17778
- * `{for @item of ...}` → "item"
17779
- * `{for @index, @item of ...}` → "index", "item"
17780
- */
17781
- function extractForLocals(content) {
17782
- const locals = /* @__PURE__ */ new Set();
17783
- let match;
17784
- FOR_LOCAL_RE.lastIndex = 0;
17785
- while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
17786
- locals.add(match[1]);
17787
- if (match[2]) locals.add(match[2]);
17788
- }
17789
- return locals;
17790
- }
17791
- /**
17792
17938
  * Validate a single variable reference path (e.g. "player.health") against
17793
17939
  * the schema. Returns an error message or null if valid.
17794
17940
  */
17795
- function validateRef(ref, schema, forLocals) {
17941
+ function validateRef(ref, schema) {
17796
17942
  const parts = ref.split(".");
17797
17943
  const rootName = parts[0];
17798
17944
  const nameError = variableNameError(rootName, "$" + rootName);
17799
17945
  if (nameError) return nameError;
17800
- if (forLocals.has(rootName)) return null;
17801
17946
  const rootSchema = schema.get(rootName);
17802
17947
  if (!rootSchema) return `Undeclared variable: $${ref}`;
17803
17948
  let current = rootSchema;
@@ -17853,8 +17998,9 @@ function scanInterpolations(text, onRef, storeVarMacros = NO_STORE_VAR_MACROS) {
17853
17998
  * Report `$var` references in JavaScript code, lexed as the expression
17854
17999
  * engine lexes it (`lexJs`): references in code, but none inside string or
17855
18000
  * regex literals or comments, nor property names (`a.$b`, `{ $b: 1 }`).
17856
- * The text of string and template literals is scanned for the markup that
17857
- * labels evaluate (`{$…}`); the `${…}` parts of template literals are code.
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.
17858
18004
  */
17859
18005
  function scanCode(code, onRef, goal = "expression") {
17860
18006
  const parsed = parseOrError(code, goal);
@@ -17867,37 +18013,17 @@ function scanCode(code, onRef, goal = "expression") {
17867
18013
  VAR_PATH_RE.lastIndex = ref.start;
17868
18014
  onRef(VAR_PATH_RE.exec(code)[1]);
17869
18015
  }
17870
- for (const text of parsed.strings) scanInterpolations(text, onRef);
17871
18016
  }
17872
18017
  /** `scanCode` for code acorn can't parse: lexed leniently. */
17873
18018
  function scanCodeLeniently(code, onRef, goal) {
17874
- /** Open template literals: their text so far, null in an interpolation. */
17875
- const templates = [];
17876
18019
  lexJs(code, {
17877
18020
  variable(sigil, _name, index) {
17878
18021
  if (sigil !== "$") return;
17879
18022
  VAR_PATH_RE.lastIndex = index;
17880
18023
  onRef(VAR_PATH_RE.exec(code)[1]);
17881
18024
  },
17882
- literal(text, _index, nesting) {
17883
- const template = templates[templates.length - 1];
17884
- if (template?.nesting === nesting) if (template.text === null) template.text = "";
17885
- else if (text === "`" || text === "${") {
17886
- scanInterpolations(template.text, onRef);
17887
- if (text === "`") templates.pop();
17888
- else template.text = null;
17889
- } else template.text += text;
17890
- else if (text === "`") templates.push({
17891
- nesting,
17892
- text: ""
17893
- });
17894
- else if (text[0] === "\"" || text[0] === "'") {
17895
- const { closed } = scanStringLiteral(text, 0);
17896
- scanInterpolations(text.slice(1, closed ? -1 : void 0), onRef);
17897
- }
17898
- }
18025
+ literal() {}
17899
18026
  }, goal);
17900
- for (const t of templates) if (t.text) scanInterpolations(t.text, onRef);
17901
18027
  }
17902
18028
  /**
17903
18029
  * Report the `$var` references a passage evaluates at runtime: `{$var}`
@@ -17909,13 +18035,23 @@ function collectPassageRefs(content, storeVarMacros, onRef, parametersOf) {
17909
18035
  withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef, parametersOf));
17910
18036
  }
17911
18037
  /**
17912
- * Report the references in the code that a macro's quoted arguments hold,
17913
- * the condition and `run` action of `{watch}`: the argument scan sees only
17914
- * the strings.
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.
17915
18043
  */
17916
- function scanStringCode(token, params, onRef) {
17917
- if (!params || !token.rawArgs) return;
17918
- for (const piece of argPieces(token.rawArgs, 0, params, token.name, "")) if (piece.kind === "code" && piece.inString) scanCode(piece.code, onRef, piece.goal);
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);
17919
18055
  }
17920
18056
  /** Report the `$var` references in the tokens of `content`. */
17921
18057
  function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf = () => void 0) {
@@ -17927,12 +18063,12 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf =
17927
18063
  else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) if (isCodeAttribute(name)) scanSigilReferences(value, onRef);
17928
18064
  else scanInterpolations(value, onRef, storeVarMacros);
17929
18065
  else if (token.type === "macro" && !token.isClose) {
17930
- scanCode(token.rawArgs, onRef);
17931
- scanStringCode(token, parametersOf(token.name), onRef);
18066
+ scanArgs(token, parametersOf(token.name), onRef);
17932
18067
  if (storeVarMacros.has(token.name.toLowerCase())) {
17933
18068
  const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
17934
18069
  const quoted = QUOTED_VAR_ARG_RE.exec(first);
17935
18070
  if (quoted) onRef(quoted[1]);
18071
+ else if (!/^["'`]/.test(first)) scanCode(first, onRef);
17936
18072
  }
17937
18073
  if (token.name === "do") {
17938
18074
  let close = t + 1;
@@ -17962,9 +18098,8 @@ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_M
17962
18098
  const storeVarSet = new Set(Array.from(storeVarMacros, (m) => m.toLowerCase()));
17963
18099
  for (const [name, passage] of passages) {
17964
18100
  if (NOT_MARKUP.has(name)) continue;
17965
- const forLocals = extractForLocals(passage.content);
17966
18101
  collectPassageRefs(passage.content, storeVarSet, (ref) => {
17967
- const error = validateRef(ref, schema, forLocals);
18102
+ const error = validateRef(ref, schema);
17968
18103
  if (error) errors.push(`Passage "${name}": ${error}`);
17969
18104
  }, parametersOf);
17970
18105
  }
@@ -18221,12 +18356,16 @@ defineMacro({
18221
18356
  },
18222
18357
  {
18223
18358
  name: "label",
18224
- type: "string"
18359
+ type: "string",
18360
+ holds: "markup"
18225
18361
  }
18226
18362
  ],
18227
18363
  render(_props, ctx) {
18228
18364
  try {
18229
- const { currentExpr, maxExpr, labelMode } = meterArgs(ctx.args);
18365
+ const { currentExpr, maxExpr, labelMode } = meterArgs({
18366
+ ...ctx.args,
18367
+ label: ctx.resolve?.(ctx.args.label)
18368
+ });
18230
18369
  const current = Number(ctx.evaluate(currentExpr));
18231
18370
  const max = Number(ctx.evaluate(maxExpr));
18232
18371
  const pct = max === 0 ? 0 : Math.max(0, Math.min(100, current / max * 100));
@@ -18588,6 +18727,7 @@ defineMacro({
18588
18727
  parameters: [{
18589
18728
  name: "label",
18590
18729
  type: "text",
18730
+ holds: "markup",
18591
18731
  required: true
18592
18732
  }],
18593
18733
  render({ rawArgs, children = [] }, ctx) {
@@ -18619,13 +18759,14 @@ defineMacro({
18619
18759
  parameters: [{
18620
18760
  name: "text",
18621
18761
  type: "string",
18762
+ holds: "markup",
18622
18763
  required: true
18623
18764
  }, {
18624
18765
  name: "passage",
18625
18766
  type: "passage"
18626
18767
  }],
18627
18768
  render({ children = [] }, ctx) {
18628
- const display = ctx.args.text ?? "";
18769
+ const display = ctx.resolve?.(ctx.args.text ?? "") ?? "";
18629
18770
  let passage = null;
18630
18771
  let failure;
18631
18772
  if (ctx.args.passage !== void 0) try {
@@ -18948,6 +19089,7 @@ defineMacro({
18948
19089
  parameters: [{
18949
19090
  name: "label",
18950
19091
  type: "text",
19092
+ holds: "markup",
18951
19093
  required: true
18952
19094
  }, {
18953
19095
  name: "noclose",
@@ -19494,7 +19636,8 @@ function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? St
19494
19636
  storeVar: true,
19495
19637
  parameters: [VARIABLE_PARAMETER, {
19496
19638
  name: "placeholder",
19497
- type: "string"
19639
+ type: "string",
19640
+ holds: "text"
19498
19641
  }],
19499
19642
  render(_props, ctx) {
19500
19643
  const placeholder = ctx.args.placeholder ?? "";
@@ -19530,7 +19673,8 @@ defineMacro({
19530
19673
  storeVar: true,
19531
19674
  parameters: [VARIABLE_PARAMETER, {
19532
19675
  name: "label",
19533
- type: "text"
19676
+ type: "text",
19677
+ holds: "text"
19534
19678
  }],
19535
19679
  render(_props, ctx) {
19536
19680
  const label = ctx.args.label ?? "";
@@ -19561,11 +19705,13 @@ defineMacro({
19561
19705
  {
19562
19706
  name: "value",
19563
19707
  type: "text",
19708
+ holds: "text",
19564
19709
  required: true
19565
19710
  },
19566
19711
  {
19567
19712
  name: "label",
19568
- type: "text"
19713
+ type: "text",
19714
+ holds: "text"
19569
19715
  }
19570
19716
  ],
19571
19717
  render(_props, ctx) {
@@ -19985,6 +20131,7 @@ defineMacro({
19985
20131
  parameters: [{
19986
20132
  name: "condition",
19987
20133
  type: "string",
20134
+ holds: "expression",
19988
20135
  required: true
19989
20136
  }, {
19990
20137
  name: "options",
@@ -19992,19 +20139,23 @@ defineMacro({
19992
20139
  parameters: [
19993
20140
  {
19994
20141
  name: "goto",
19995
- type: "string"
20142
+ type: "string",
20143
+ holds: "passage"
19996
20144
  },
19997
20145
  {
19998
20146
  name: "dialog",
19999
- type: "string"
20147
+ type: "string",
20148
+ holds: "passage"
20000
20149
  },
20001
20150
  {
20002
20151
  name: "run",
20003
- type: "string"
20152
+ type: "string",
20153
+ holds: "statements"
20004
20154
  },
20005
20155
  {
20006
20156
  name: "name",
20007
- type: "string"
20157
+ type: "string",
20158
+ holds: "text"
20008
20159
  },
20009
20160
  {
20010
20161
  name: "priority",
@@ -20033,6 +20184,7 @@ defineMacro({
20033
20184
  parameters: [{
20034
20185
  name: "name",
20035
20186
  type: "text",
20187
+ holds: "text",
20036
20188
  required: true
20037
20189
  }],
20038
20190
  render(_props, ctx) {
@@ -20081,11 +20233,10 @@ function stopWithErrors(errors) {
20081
20233
  */
20082
20234
  function markupErrors(storyData, only) {
20083
20235
  const macros = getMacroRegistry();
20084
- const parameters = new Map(macros.map((m) => [m.name.toLowerCase(), m.parameters]));
20085
20236
  return validateMarkup(storyData.passages.values(), {
20086
20237
  isKnownMacro: (name) => !!getMacro(name) || isSubMacro(name) || !!getWidget(name),
20087
20238
  macroNames: macros.map((m) => m.name),
20088
- parametersOf: (name) => parameters.get(name),
20239
+ parametersOf: parameterLookup(macros),
20089
20240
  only
20090
20241
  }).map(formatDiagnostic);
20091
20242
  }