@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.
@@ -68,24 +68,41 @@ var LINK_SEPARATORS = [
68
68
  ["<-", true]
69
69
  ];
70
70
  var NO_SELECTORS = {};
71
- function parseLink(inner) {
71
+ /**
72
+ * A link's display text and target, from its text `inner` (at `at` in the
73
+ * input), with where the target is written.
74
+ */
75
+ function parseLink(inner, at) {
76
+ /** The trimmed text from `start` to `end` of inner, and where it is. */
77
+ const part = (start, end) => {
78
+ const raw = inner.slice(start, end);
79
+ const value = raw.trim();
80
+ const from = at + start + (value ? raw.indexOf(value) : 0);
81
+ return {
82
+ value,
83
+ start: from,
84
+ end: from + value.length
85
+ };
86
+ };
72
87
  for (const [separator, targetFirst] of LINK_SEPARATORS) {
73
88
  const idx = inner.indexOf(separator);
74
89
  if (idx === -1) continue;
75
- const before = inner.slice(0, idx).trim();
76
- const after = inner.slice(idx + separator.length).trim();
77
- return targetFirst ? {
78
- display: after,
79
- target: before
80
- } : {
81
- display: before,
82
- target: after
90
+ const before = part(0, idx);
91
+ const after = part(idx + separator.length, inner.length);
92
+ const [display, target] = targetFirst ? [after, before] : [before, after];
93
+ return {
94
+ display: display.value,
95
+ target: target.value,
96
+ targetStart: target.start,
97
+ targetEnd: target.end
83
98
  };
84
99
  }
85
- const trimmed = inner.trim();
100
+ const whole = part(0, inner.length);
86
101
  return {
87
- display: trimmed,
88
- target: trimmed
102
+ display: whole.value,
103
+ target: whole.value,
104
+ targetStart: whole.start,
105
+ targetEnd: whole.end
89
106
  };
90
107
  }
91
108
  function quoteArg(value) {
@@ -167,6 +184,8 @@ function tokenOf(x) {
167
184
  type: "link",
168
185
  display: x.display,
169
186
  target: x.target,
187
+ targetStart: x.targetStart,
188
+ targetEnd: x.targetEnd,
170
189
  ...span
171
190
  }, x);
172
191
  case "variable": return withSelectors({
@@ -698,8 +717,8 @@ function peg$parse(input, options) {
698
717
  function peg$f48() {
699
718
  return !textMode;
700
719
  }
701
- function peg$f49(s, inner) {
702
- return withSelectors(lexeme("link", location(), parseLink(inner)), s);
720
+ function peg$f49(s, at, inner) {
721
+ return withSelectors(lexeme("link", location(), parseLink(inner, at)), s);
703
722
  }
704
723
  function peg$f50() {
705
724
  return !textMode;
@@ -1748,7 +1767,7 @@ function peg$parse(input, options) {
1748
1767
  return s0;
1749
1768
  }
1750
1769
  function peg$parseLinkLexeme() {
1751
- let s0, s1, s2, s3, s4, s5;
1770
+ let s0, s1, s2, s3, s4, s5, s6;
1752
1771
  s0 = peg$currPos;
1753
1772
  peg$savedPos = peg$currPos;
1754
1773
  s1 = peg$f48();
@@ -1765,19 +1784,20 @@ function peg$parse(input, options) {
1765
1784
  if (s2 !== peg$FAILED) {
1766
1785
  s3 = peg$parseSelectors();
1767
1786
  if (s3 !== peg$FAILED) {
1768
- s4 = peg$currPos;
1769
- s5 = peg$parseLinkText();
1770
- s4 = input.substring(s4, peg$currPos);
1787
+ s4 = peg$parseHere();
1788
+ s5 = peg$currPos;
1789
+ s6 = peg$parseLinkText();
1790
+ s5 = input.substring(s5, peg$currPos);
1771
1791
  if (input.substr(peg$currPos, 2) === peg$c5) {
1772
- s5 = peg$c5;
1792
+ s6 = peg$c5;
1773
1793
  peg$currPos += 2;
1774
1794
  } else {
1775
- s5 = peg$FAILED;
1795
+ s6 = peg$FAILED;
1776
1796
  if (peg$silentFails === 0) peg$fail(peg$e9);
1777
1797
  }
1778
- if (s5 !== peg$FAILED) {
1798
+ if (s6 !== peg$FAILED) {
1779
1799
  peg$savedPos = s0;
1780
- s0 = peg$f49(s3, s4);
1800
+ s0 = peg$f49(s3, s4, s5);
1781
1801
  } else {
1782
1802
  peg$currPos = s0;
1783
1803
  s0 = peg$FAILED;
@@ -9410,6 +9430,22 @@ function regexEnd(src, start) {
9410
9430
  return -1;
9411
9431
  }
9412
9432
  /**
9433
+ * The value of `src` when it is exactly one `"…"` or `'…'` string literal,
9434
+ * as JavaScript reads it (`"\u0048all"` is `Hall`, `"\1"` is U+0001 in
9435
+ * the non-strict code passages run), otherwise `null`: other code, or a
9436
+ * literal that is not well-formed (`"\u{110000}"`).
9437
+ */
9438
+ function stringLiteralValue(src) {
9439
+ if (src[0] !== "\"" && src[0] !== "'") return null;
9440
+ try {
9441
+ const p = tokenizerAt(src, 0, "expression");
9442
+ p.nextToken();
9443
+ return p.type === types$1.string && p.end === src.length ? p.value : null;
9444
+ } catch {
9445
+ return null;
9446
+ }
9447
+ }
9448
+ /**
9413
9449
  * Scan the `"…"` or `'…'` string literal opening at `start`. `end` is the
9414
9450
  * index just past its closing quote, or `src.length` when it is unterminated
9415
9451
  * (`closed` false). A backslash escapes the character after it, so a quote
@@ -9668,10 +9704,22 @@ function parseSelectors(source, at = 0) {
9668
9704
  function tokenizeMarkupTolerant(source, options = {}) {
9669
9705
  const tokens = [];
9670
9706
  const errors = [];
9707
+ const lastBrace = source.lastIndexOf("}");
9708
+ const { closeBrace } = {
9709
+ ...defaultHooks,
9710
+ ...options.hooks
9711
+ };
9671
9712
  /** `text` shifted by `base`, tokenized: the tokens or the error. */
9672
9713
  const attempt = (text, base) => {
9714
+ const hooks = {
9715
+ ...options.hooks,
9716
+ closeBrace: (input, codeStart, lenientStart) => base + Math.min(codeStart, lenientStart) > lastBrace ? -1 : closeBrace(input, codeStart, lenientStart)
9717
+ };
9673
9718
  try {
9674
- return tokenizeMarkup(text, options).map((token) => shift(token, base));
9719
+ return tokenizeMarkup(text, {
9720
+ ...options,
9721
+ hooks
9722
+ }).map((token) => shift(token, base));
9675
9723
  } catch (error) {
9676
9724
  if (!(error instanceof MarkupError)) throw error;
9677
9725
  return error;
@@ -9713,11 +9761,17 @@ function tokenizeMarkupTolerant(source, options = {}) {
9713
9761
  }
9714
9762
  /** `token` with its offsets moved `by` on. */
9715
9763
  function shift(token, by) {
9716
- return by === 0 ? token : {
9764
+ if (by === 0) return token;
9765
+ const moved = {
9717
9766
  ...token,
9718
9767
  start: token.start + by,
9719
9768
  end: token.end + by
9720
9769
  };
9770
+ if (moved.type === "link") {
9771
+ moved.targetStart += by;
9772
+ moved.targetEnd += by;
9773
+ }
9774
+ return moved;
9721
9775
  }
9722
9776
  //#endregion
9723
9777
  //#region src/utils/parse-delay.ts
@@ -9805,37 +9859,6 @@ function readWholeQuoted(src) {
9805
9859
  const quoted = readQuoted(src, 0);
9806
9860
  return quoted && quoted.end === src.length ? quoted.value : null;
9807
9861
  }
9808
- var JS_SIMPLE_ESCAPES = {
9809
- n: "\n",
9810
- t: " ",
9811
- r: "\r",
9812
- b: "\b",
9813
- f: "\f",
9814
- v: "\v",
9815
- "0": "\0"
9816
- };
9817
- /** Undo every escape of a JavaScript string literal's body. */
9818
- function decodeJsEscapes(body) {
9819
- 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) => {
9820
- if (lineBreak !== void 0) return "";
9821
- const code = braced ?? unicode ?? hex;
9822
- if (code !== void 0) {
9823
- const point = parseInt(code, 16);
9824
- return point <= 1114111 ? String.fromCodePoint(point) : "";
9825
- }
9826
- return JS_SIMPLE_ESCAPES[other] ?? other;
9827
- });
9828
- }
9829
- /**
9830
- * The value of `src` when it is exactly one `"…"` or `'…'` string, read as
9831
- * the JavaScript literal it is (`"\u0048all"` is `Hall`), otherwise `null`.
9832
- * `readWholeQuoted` keeps other escapes as written, for labels and code.
9833
- */
9834
- function readWholeJsString(src) {
9835
- const quoted = readQuoted(src, 0);
9836
- if (!quoted || quoted.end !== src.length) return null;
9837
- return decodeJsEscapes(src.slice(1, -1));
9838
- }
9839
9862
  /**
9840
9863
  * Strip an optional quote from each end of an unquoted-or-loosely-quoted
9841
9864
  * argument (`"Red`, `Red'`), leaving at least one character. This is the
@@ -9995,7 +10018,9 @@ var MacroArgumentError = class extends Error {
9995
10018
  this.name = "MacroArgumentError";
9996
10019
  }
9997
10020
  };
9998
- function readValue(param, src) {
10021
+ /** Read `src`, at `place`, as `param` says. */
10022
+ function readValue(param, src, place) {
10023
+ place.spans?.set(param, [place.at, place.at + src.length]);
9999
10024
  switch (param.type) {
10000
10025
  case "string": {
10001
10026
  const value = readWholeQuoted(src);
@@ -10006,7 +10031,7 @@ function readValue(param, src) {
10006
10031
  case "names": return src.split(",").map((name) => name.trim());
10007
10032
  case "delay": return parseDelay(src);
10008
10033
  case "number": return Number(src);
10009
- case "options": return readOptions(param.parameters ?? [], src);
10034
+ case "options": return readOptions(param.parameters ?? [], src, place);
10010
10035
  default: return src;
10011
10036
  }
10012
10037
  }
@@ -10024,11 +10049,12 @@ function matchAt(re, src, pos) {
10024
10049
  }
10025
10050
  /**
10026
10051
  * Read keyword options (`goto "X" priority 5 once`). A keyword takes the
10027
- * quoted string or digit run after it as its value; keywords that aren't
10028
- * declared, and their values, are skipped. A keyword of a `string` parameter
10029
- * must be followed by a quoted string.
10052
+ * quoted string (with or without whitespace before it) or digit run after
10053
+ * it as its value; keywords that aren't declared, and their values, are
10054
+ * skipped. A keyword of a `string` parameter must be followed by a quoted
10055
+ * string.
10030
10056
  */
10031
- function readOptions(params, src) {
10057
+ function readOptions(params, src, { at, spans }) {
10032
10058
  const options = {};
10033
10059
  let i = 0;
10034
10060
  while (i < src.length) {
@@ -10039,22 +10065,20 @@ function readOptions(params, src) {
10039
10065
  }
10040
10066
  i += key.length;
10041
10067
  let val;
10042
- let quoted = null;
10043
10068
  let j = i;
10044
10069
  while (j < src.length && isWhitespace(src[j])) j++;
10045
- if (j > i) {
10046
- quoted = readQuoted(src, j);
10047
- const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
10048
- if (quoted) {
10049
- val = quoted.value;
10050
- i = quoted.end;
10051
- } else if (digits) {
10052
- val = digits;
10053
- i = j + digits.length;
10054
- }
10070
+ const quoted = readQuoted(src, j);
10071
+ const digits = quoted || j === i ? null : matchAt(DIGITS_RE, src, j);
10072
+ if (quoted) {
10073
+ val = quoted.value;
10074
+ i = quoted.end;
10075
+ } else if (digits) {
10076
+ val = digits;
10077
+ i = j + digits.length;
10055
10078
  }
10056
10079
  const param = params.find((p) => p.name === key);
10057
- if (param?.type === "string" && (!quoted || val === void 0)) throw new MacroArgumentError(param, matchAt(WORD_OR_PUNCT_RE, src, j) ?? "");
10080
+ if (param && param.type !== "flag" && val !== void 0) spans?.set(param, [at + j, at + i]);
10081
+ if (param?.type === "string" && !quoted) throw new MacroArgumentError(param, matchAt(WORD_OR_PUNCT_RE, src, j) ?? "");
10058
10082
  if (param?.type === "flag") options[key] = true;
10059
10083
  else if (param?.type === "number") options[key] = Number(val ?? 0);
10060
10084
  else if (param) options[key] = val;
@@ -10064,21 +10088,21 @@ function readOptions(params, src) {
10064
10088
  /** `inline %name` reads a transient variable; `inline % 2` is modulo. */
10065
10089
  var LEADING_OPERATOR_RE = /^(?:[-+*/&|^=<>?:,.]|%(?![A-Za-z_]))/;
10066
10090
  /**
10067
- * The rest of `src` without the flag `word` as its last or first term, or
10068
- * `null` if it has no such flag. Next to a binary operator the word is an
10069
- * operand (`"a" + inline`), not a flag; the closing `/` of a regex literal
10070
- * is not one.
10091
+ * Where the rest of `src` is without the flag `word` as its last or first
10092
+ * term, or `null` if it has no such flag. Next to a binary operator the
10093
+ * word is an operand (`"a" + inline`), not a flag; the closing `/` of a
10094
+ * regex literal is not one.
10071
10095
  */
10072
10096
  function takeFlag(src, word) {
10073
10097
  const spans = terms(src);
10074
10098
  if (spans.length < 2) return null;
10075
10099
  if (slice(src, spans[spans.length - 1]) === word) {
10076
- const rest = src.slice(0, spans[spans.length - 2][1]);
10077
- if (!endsWithOperator(rest)) return rest;
10100
+ const rest = [0, spans[spans.length - 2][1]];
10101
+ if (!endsWithOperator(slice(src, rest))) return rest;
10078
10102
  }
10079
10103
  if (slice(src, spans[0]) === word) {
10080
- const rest = src.slice(spans[1][0]);
10081
- if (!LEADING_OPERATOR_RE.test(rest)) return rest;
10104
+ const rest = [spans[1][0], src.length];
10105
+ if (!LEADING_OPERATOR_RE.test(slice(src, rest))) return rest;
10082
10106
  }
10083
10107
  return null;
10084
10108
  }
@@ -10106,69 +10130,93 @@ function findSeparator(src, word) {
10106
10130
  return null;
10107
10131
  }
10108
10132
  var takesRest = (param) => param.type === "expression" || param.type === "statements" || param.type === "passage" || param.type === "text";
10109
- /** Read the parameters of one group from `src` (`null`: no text) into `args`. */
10110
- function readGroup(params, src, args) {
10111
- let text = src?.trim() ?? "";
10133
+ /** The text of `src` without the whitespace around it, and where it is. */
10134
+ function trimmed(src, at) {
10135
+ const text = src.trim();
10136
+ return [text, text ? at + src.indexOf(text) : at];
10137
+ }
10138
+ /**
10139
+ * Read the parameters of one group from `src` (`null`: no text), which is
10140
+ * at `at` in the raw arguments, into `args`.
10141
+ */
10142
+ function readGroup(params, src, args, { at: srcAt, spans }) {
10143
+ let [text, at] = trimmed(src ?? "", srcAt);
10112
10144
  for (const flag of params.filter((p) => p.type === "flag")) {
10113
10145
  const rest = src === null ? null : takeFlag(text, flag.name);
10114
10146
  args[flag.name] = rest !== null;
10115
- text = rest ?? text;
10147
+ if (rest) [text, at] = trimmed(slice(text, rest), at + rest[0]);
10116
10148
  }
10117
10149
  const positional = params.filter((p) => p.type !== "flag");
10118
10150
  if (positional.length === 0) return;
10119
10151
  const lastText = positional.findIndex((p, i) => takesRest(p) && !positional.slice(i + 1).some(takesRest));
10120
10152
  const restIndex = lastText < 0 ? positional.length - 1 : lastText;
10121
- const read = (param, value) => value === "" ? unset(param) : readValue(param, value);
10153
+ /** Read `param` from the text from `start` to `end`. */
10154
+ const read = (param, [start, end]) => start === end ? unset(param) : readValue(param, text.slice(start, end), {
10155
+ at: at + start,
10156
+ spans
10157
+ });
10122
10158
  if (positional.length === 1) {
10123
- args[positional[0].name] = read(positional[0], text);
10159
+ args[positional[0].name] = read(positional[0], [0, text.length]);
10124
10160
  return;
10125
10161
  }
10126
- const spans = terms(text);
10162
+ const parts = terms(text);
10127
10163
  let from = 0;
10128
- let to = spans.length;
10129
- for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
10130
- const readTrailing = (param, value) => {
10164
+ let to = parts.length;
10165
+ for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, parts[from++]) : unset(param);
10166
+ const readTrailing = (param, span) => {
10131
10167
  try {
10132
- return read(param, value);
10168
+ return read(param, span);
10133
10169
  } catch (error) {
10134
10170
  if (error instanceof MacroArgumentError) return void 0;
10135
10171
  throw error;
10136
10172
  }
10137
10173
  };
10138
10174
  for (const param of positional.slice(restIndex + 1).reverse()) {
10139
- const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? readTrailing(param, slice(text, spans[to - 1])) : void 0;
10175
+ const value = from < to && !endsWithOperator(text.slice(0, parts[to - 1][0])) ? readTrailing(param, parts[to - 1]) : void 0;
10140
10176
  args[param.name] = value ?? unset(param);
10141
10177
  if (value !== void 0) to--;
10178
+ else spans?.delete(param);
10142
10179
  }
10143
10180
  const rest = positional[restIndex];
10144
- args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
10181
+ args[rest.name] = from < to ? read(rest, [parts[from][0], parts[to - 1][1]]) : unset(rest);
10145
10182
  }
10146
- /** Read `rawArgs` into the declared `parameters` (see above). */
10147
- function parseMacroArgs(rawArgs, parameters) {
10183
+ /**
10184
+ * Read `rawArgs` into the declared `parameters` (see above). With `spans`,
10185
+ * note there where each argument set was read.
10186
+ */
10187
+ function parseMacroArgs(rawArgs, parameters, spans) {
10148
10188
  const args = {};
10149
- let rest = rawArgs.trim();
10189
+ let [rest, restAt] = trimmed(rawArgs, 0);
10150
10190
  let groupStart = 0;
10151
10191
  parameters.forEach((separator, i) => {
10152
10192
  if (separator.type !== "separator") return;
10153
10193
  const at = rest === null ? null : findSeparator(rest, separator.name);
10154
10194
  args[separator.name] = at !== null;
10155
10195
  const before = at ? rest.slice(0, at[0]) : rest;
10156
- readGroup(parameters.slice(groupStart, i), before, args);
10196
+ readGroup(parameters.slice(groupStart, i), before, args, {
10197
+ at: restAt,
10198
+ spans
10199
+ });
10157
10200
  rest = at ? rest.slice(at[1]) : null;
10201
+ if (at) restAt += at[1];
10158
10202
  groupStart = i + 1;
10159
10203
  });
10160
- readGroup(parameters.slice(groupStart), rest, args);
10204
+ readGroup(parameters.slice(groupStart), rest, args, {
10205
+ at: restAt,
10206
+ spans
10207
+ });
10161
10208
  return args;
10162
10209
  }
10163
10210
  /**
10164
10211
  * Read a `passage` argument as written (`{goto "Hall"}`, `{goto $room}`): a
10165
- * quoted string is the name its JavaScript literal has (`"\u0048all"` is
10166
- * `Hall`), anything else an expression, whose value is the name when it
10212
+ * string literal is the name JavaScript reads from it (`"\u0048all"` is
10213
+ * `Hall`), anything else (a literal that is not well-formed too) an
10214
+ * expression, whose value is the name when it
10167
10215
  * runs (see evaluatePassageName).
10168
10216
  */
10169
10217
  function passageTarget(arg) {
10170
10218
  const expression = arg.trim();
10171
- const name = readWholeJsString(expression);
10219
+ const name = stringLiteralValue(expression);
10172
10220
  return name === null ? {
10173
10221
  kind: "expression",
10174
10222
  expression
@@ -10198,10 +10246,12 @@ function evaluatePassageName(expr, evaluate, { storyData, currentPassage }) {
10198
10246
  var WIDGET_PARAMETERS = [{
10199
10247
  name: "name",
10200
10248
  type: "text",
10249
+ holds: "text",
10201
10250
  required: true
10202
10251
  }, {
10203
10252
  name: "parameters",
10204
- type: "text"
10253
+ type: "text",
10254
+ holds: "text"
10205
10255
  }];
10206
10256
  /**
10207
10257
  * The widget a {widget} definition's arguments declare: its name and its
@@ -10233,6 +10283,17 @@ function blockWidgetNames(passages) {
10233
10283
  }
10234
10284
  return names;
10235
10285
  }
10286
+ /** The parameters of the built-in sub-macros that take arguments. */
10287
+ var SUB_MACRO_PARAMETERS = new Map([["option", [{
10288
+ name: "value",
10289
+ type: "string",
10290
+ holds: "text",
10291
+ required: true
10292
+ }]]]);
10293
+ /** The parameters of the built-in sub-macro `name`, if it takes any. */
10294
+ function subMacroParameters(name) {
10295
+ return SUB_MACRO_PARAMETERS.get(name.toLowerCase());
10296
+ }
10236
10297
  //#endregion
10237
10298
  //#region src/code-check.ts
10238
10299
  /** While a pass runs: the code it parsed so far, by goal and source. */
@@ -10270,30 +10331,59 @@ var CONDITION_MACROS = new Set([
10270
10331
  "elseif",
10271
10332
  "case"
10272
10333
  ]);
10273
- /** Code inside the quoted strings of a macro's arguments, by parameter. */
10274
- var CODE_IN_STRINGS = new Map([["watch", {
10275
- condition: "expression",
10276
- run: "statements"
10277
- }]]);
10278
- /** The `string` parameters of a macro that name a passage. */
10279
- var PASSAGE_STRINGS = new Map([["watch", ["goto", "dialog"]]]);
10280
- /**
10281
- * The `text` and `string` parameters of a macro that the runtime keeps as
10282
- * written, not as markup: an option's value, the name of a watcher.
10283
- */
10284
- var LITERAL_STRINGS = new Map([
10285
- ["option", ["value"]],
10286
- ["watch", ["name"]],
10287
- ["unwatch", ["name"]]
10288
- ]);
10289
10334
  /** Block macros whose body is the name of a passage. */
10290
10335
  var PASSAGE_BODIES = new Set(["dialog"]);
10336
+ /**
10337
+ * `params` with each `string` and `text` parameter (options too) saying
10338
+ * what it holds: what it declares, else `holds`.
10339
+ */
10340
+ function withHolds(params, holds) {
10341
+ return params.map((param) => {
10342
+ if (param.parameters) return {
10343
+ ...param,
10344
+ parameters: withHolds(param.parameters, holds)
10345
+ };
10346
+ return (param.type === "string" || param.type === "text") && !param.holds ? {
10347
+ ...param,
10348
+ holds
10349
+ } : param;
10350
+ });
10351
+ }
10352
+ /**
10353
+ * The declared parameters of `macros`, by name in any case, and of the
10354
+ * built-in sub-macros (`{option}`): what the story-start check, reference
10355
+ * collection and tooling read arguments by. Each `string` and `text`
10356
+ * parameter says what it holds: what it declares, else `markup` for a macro
10357
+ * with `interpolate` (which can resolve markup) and `text` for any other.
10358
+ */
10359
+ function parameterLookup(macros) {
10360
+ const parameters = new NameMap();
10361
+ for (const { name, parameters: params, interpolate } of macros) if (params) parameters.set(name, withHolds(params, holdsByDefault(interpolate)));
10362
+ return (name) => {
10363
+ let params = parameters.get(name);
10364
+ if (!params) {
10365
+ const sub = subMacroParameters(name);
10366
+ if (sub) parameters.set(name, params = withHolds(sub, "text"));
10367
+ }
10368
+ return params;
10369
+ };
10370
+ }
10371
+ /** What a `string` or `text` argument holds when its parameter doesn't say. */
10372
+ var holdsByDefault = (interpolate) => interpolate ? "markup" : "text";
10291
10373
  /** The goal of the code an argument of this type holds, if it is code. */
10292
10374
  function codeGoal(param) {
10293
10375
  if (param.type === "expression" || param.type === "passage") return "expression";
10294
10376
  if (param.type === "statements") return "statements";
10295
10377
  }
10296
10378
  /**
10379
+ * What a `string` or `text` argument holds: what its parameter says (see
10380
+ * parameterLookup), else markup, which is checked.
10381
+ */
10382
+ function stringHolds(param) {
10383
+ if (param.type !== "string" && param.type !== "text") return void 0;
10384
+ return param.holds ?? "markup";
10385
+ }
10386
+ /**
10297
10387
  * The code and the markup-holding text in `tokens`, the tokens of `src`.
10298
10388
  * `parametersOf` gives the declared parameters of a macro, if it has any.
10299
10389
  */
@@ -10305,8 +10395,8 @@ function* codeAndText(src, tokens, parametersOf) {
10305
10395
  if (token.type === "link") yield {
10306
10396
  kind: "passage",
10307
10397
  name: token.target,
10308
- offset: locate(src, token.target, token.start),
10309
- length: token.target.length,
10398
+ offset: token.targetStart,
10399
+ length: token.targetEnd - token.targetStart,
10310
10400
  macro: "link",
10311
10401
  label: src.slice(token.start, token.end)
10312
10402
  };
@@ -10317,32 +10407,35 @@ function* codeAndText(src, tokens, parametersOf) {
10317
10407
  goal: "expression",
10318
10408
  label: `{${token.expression}}`
10319
10409
  };
10320
- else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) {
10321
- const at = locate(src, value, token.start);
10322
- if (!isCodeAttribute(name)) {
10323
- yield {
10324
- kind: "text",
10325
- text: value,
10326
- offset: at,
10327
- where: `In the ${name} attribute of <${token.tag}>: `
10328
- };
10329
- continue;
10330
- }
10331
- let from = 0;
10332
- for (const part of splitSigilTemplate(value)) {
10333
- if (!("expr" in part)) continue;
10334
- const k = locate(value, part.expr, from);
10335
- from = k + part.expr.length;
10336
- yield {
10337
- kind: "code",
10338
- code: part.expr,
10339
- offset: at + k,
10340
- goal: "expression",
10341
- label: `{${part.expr}} in the ${name} attribute of <${token.tag}>`
10342
- };
10410
+ else if (token.type === "html") {
10411
+ let cursor = token.start;
10412
+ for (const [name, value] of Object.entries(token.attributes)) {
10413
+ const at = locate(src, value, cursor);
10414
+ cursor = at + value.length;
10415
+ if (!isCodeAttribute(name)) {
10416
+ yield {
10417
+ kind: "text",
10418
+ text: value,
10419
+ offset: at,
10420
+ where: `In the ${name} attribute of <${token.tag}>: `
10421
+ };
10422
+ continue;
10423
+ }
10424
+ let from = 0;
10425
+ for (const part of splitSigilTemplate(value)) {
10426
+ if (!("expr" in part)) continue;
10427
+ const k = locate(value, part.expr, from);
10428
+ from = k + part.expr.length;
10429
+ yield {
10430
+ kind: "code",
10431
+ code: part.expr,
10432
+ offset: at + k,
10433
+ goal: "expression",
10434
+ label: `{${part.expr}} in the ${name} attribute of <${token.tag}>`
10435
+ };
10436
+ }
10343
10437
  }
10344
- }
10345
- else if (token.type === "macro" && !token.isClose) {
10438
+ } else if (token.type === "macro" && !token.isClose) {
10346
10439
  const name = token.name.toLowerCase();
10347
10440
  if (name === "do") {
10348
10441
  const close = closeOf(t, name);
@@ -10362,12 +10455,13 @@ function* codeAndText(src, tokens, parametersOf) {
10362
10455
  const close = closeOf(t, name);
10363
10456
  const body = tokens.slice(t + 1, close);
10364
10457
  if (close > t + 1 && body.every((b) => b.type === "text")) {
10365
- const passage = src.slice(token.end, tokens[close].start).trim().replace(/^["']|["']$/g, "");
10458
+ const text = src.slice(token.end, tokens[close].start);
10459
+ const written = text.trim();
10366
10460
  yield {
10367
10461
  kind: "passage",
10368
- name: passage,
10369
- offset: locate(src, passage, token.end),
10370
- length: passage.length,
10462
+ name: written.replace(/^["']|["']$/g, ""),
10463
+ offset: token.end + text.indexOf(written),
10464
+ length: written.length,
10371
10465
  macro: name,
10372
10466
  label: src.slice(token.start, token.end)
10373
10467
  };
@@ -10375,7 +10469,8 @@ function* codeAndText(src, tokens, parametersOf) {
10375
10469
  }
10376
10470
  if (!args) continue;
10377
10471
  const label = `{${token.name} ${args}}`;
10378
- const argsAt = locate(src, args, token.start + 1);
10472
+ const tag = src.slice(token.start, token.end - 1);
10473
+ const argsAt = token.start + tag.trimEnd().length - args.length;
10379
10474
  if (CONDITION_MACROS.has(name)) {
10380
10475
  yield {
10381
10476
  kind: "code",
@@ -10394,8 +10489,9 @@ function* codeAndText(src, tokens, parametersOf) {
10394
10489
  /** The code and text in the arguments `args` (at `offset`) of `macro`. */
10395
10490
  function* argPieces(args, offset, params, macro, label) {
10396
10491
  let values;
10492
+ const spans = /* @__PURE__ */ new Map();
10397
10493
  try {
10398
- values = parseMacroArgs(args, params);
10494
+ values = parseMacroArgs(args, params, spans);
10399
10495
  } catch (error) {
10400
10496
  if (!(error instanceof MacroArgumentError)) throw error;
10401
10497
  yield {
@@ -10406,11 +10502,7 @@ function* argPieces(args, offset, params, macro, label) {
10406
10502
  };
10407
10503
  return;
10408
10504
  }
10409
- const key = macro.toLowerCase();
10410
- const inStrings = CODE_IN_STRINGS.get(key) ?? {};
10411
- const passageStrings = PASSAGE_STRINGS.get(key) ?? [];
10412
- const literalStrings = LITERAL_STRINGS.get(key) ?? [];
10413
- let cursor = 0;
10505
+ const name = macro.toLowerCase();
10414
10506
  /** The pieces of the parameters `list`, with their values in `from`. */
10415
10507
  function* visit(list, from) {
10416
10508
  for (const param of list) {
@@ -10419,47 +10511,62 @@ function* argPieces(args, offset, params, macro, label) {
10419
10511
  yield* visit(param.parameters, value ?? {});
10420
10512
  continue;
10421
10513
  }
10422
- if (typeof value !== "string" || !value.trim()) continue;
10423
- const at = locate(args, value, cursor);
10424
- if (at >= cursor) cursor = at + value.length;
10425
- const goal = codeGoal(param) ?? inStrings[param.name];
10514
+ const span = spans.get(param);
10515
+ if (typeof value !== "string" || !value.trim() || !span) continue;
10516
+ const [start, end] = span;
10517
+ const written = args.slice(start, end);
10518
+ /** The name as written (quotes included), at the argument. */
10519
+ const passage = (passageName) => ({
10520
+ kind: "passage",
10521
+ name: passageName,
10522
+ offset: offset + start,
10523
+ length: end - start,
10524
+ macro: name,
10525
+ label
10526
+ });
10527
+ const goal = codeGoal(param);
10528
+ if (param.type === "passage") {
10529
+ const target = passageTarget(value);
10530
+ if (target.kind === "name") {
10531
+ yield passage(target.name);
10532
+ continue;
10533
+ }
10534
+ }
10426
10535
  if (goal) {
10427
10536
  const piece = {
10428
10537
  kind: "code",
10429
10538
  code: value,
10430
- offset: offset + at,
10539
+ offset: offset + start,
10431
10540
  goal,
10432
10541
  label
10433
10542
  };
10434
10543
  if (param.type === "passage") {
10435
10544
  piece.passage = true;
10436
- piece.macro = macro.toLowerCase();
10545
+ piece.macro = name;
10437
10546
  }
10438
- if (!codeGoal(param)) piece.inString = true;
10439
10547
  yield piece;
10548
+ continue;
10440
10549
  }
10441
- const written = param.type === "passage" ? value.trim() : value;
10442
- const target = param.type === "passage" ? passageTarget(value) : passageStrings.includes(param.name) ? {
10443
- kind: "name",
10444
- name: value
10445
- } : null;
10446
- if (target?.kind === "name") yield {
10447
- kind: "passage",
10448
- name: target.name,
10449
- offset: offset + at + value.indexOf(written),
10450
- length: written.length,
10451
- macro: macro.toLowerCase(),
10452
- label
10550
+ const at = offset + start + (value !== written && /^["']/.test(written) ? 1 : 0);
10551
+ const holds = stringHolds(param);
10552
+ if (holds === "expression" || holds === "statements") yield {
10553
+ kind: "code",
10554
+ code: value,
10555
+ offset: at,
10556
+ goal: holds,
10557
+ label,
10558
+ inString: true
10453
10559
  };
10454
- if (!goal && (param.type === "text" || param.type === "string") && !literalStrings.includes(param.name)) yield {
10560
+ else if (holds === "passage") yield passage(value);
10561
+ else if (holds === "markup") yield {
10455
10562
  kind: "text",
10456
10563
  text: value,
10457
- offset: offset + at,
10564
+ offset: at,
10458
10565
  where: `In the ${param.name} of {${macro}}: `
10459
10566
  };
10460
10567
  }
10461
10568
  }
10462
- yield* visit(params, values);
10569
+ yield* [...visit(params, values)].sort((a, b) => a.offset - b.offset);
10463
10570
  }
10464
10571
  /**
10465
10572
  * Index of `part` in `text` from `from` on, else anywhere, else `from`: the
@@ -10475,9 +10582,10 @@ function locate(text, part, from) {
10475
10582
  * The passages the markup `src` (with the `tokens` it tokenizes to) names:
10476
10583
  * `[[…]]` links, the passage argument of `{goto}`, `{include}` and `{link}`
10477
10584
  * (and of macros that declare one), the `goto` and `dialog` actions of
10478
- * `{watch}` and the body of `{dialog}`, in source order. A name written out
10479
- * is a literal; the others are expressions, which name a passage when they
10480
- * run.
10585
+ * `{watch}` (and the `string` and `text` arguments of macros that declare
10586
+ * they hold one) and the body of `{dialog}`, in source order. A name written
10587
+ * out is a literal; the others are expressions, which name a passage when
10588
+ * they run.
10481
10589
  */
10482
10590
  function collectPassageReferences(src, tokens, parametersOf) {
10483
10591
  const refs = [];
@@ -10492,21 +10600,14 @@ function collectPassageReferences(src, tokens, parametersOf) {
10492
10600
  });
10493
10601
  else if (piece.kind === "code" && piece.passage) refs.push({
10494
10602
  macro: piece.macro,
10495
- target: passageTarget(piece.code),
10603
+ target: {
10604
+ kind: "expression",
10605
+ expression: piece.code
10606
+ },
10496
10607
  start: piece.offset,
10497
10608
  end: piece.offset + piece.code.length
10498
10609
  });
10499
- return refs.filter((ref, i) => refs.findIndex((r) => r.start === ref.start && r.macro === ref.macro) === i);
10500
- }
10501
- /** The parameters of the built-in sub-macros that take arguments. */
10502
- var SUB_MACRO_PARAMETERS = new Map([["option", [{
10503
- name: "value",
10504
- type: "string",
10505
- required: true
10506
- }]]]);
10507
- /** The parameters of the built-in sub-macro `name`, if it takes any. */
10508
- function subMacroParameters(name) {
10509
- return SUB_MACRO_PARAMETERS.get(name.toLowerCase());
10610
+ return refs;
10510
10611
  }
10511
10612
  //#endregion
10512
10613
  //#region src/markup/validate.ts
@@ -10636,7 +10737,7 @@ function validateMarkup(passages, options) {
10636
10737
  report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
10637
10738
  }
10638
10739
  };
10639
- const parametersOf = (name) => options.parametersOf?.(name) ?? subMacroParameters(name);
10740
+ const parametersOf = options.parametersOf ?? (() => void 0);
10640
10741
  /**
10641
10742
  * Check `tokens`, the tokens of `src`, which starts at `base` in the
10642
10743
  * passage: their macros, the code in them and the markup in their text.
@@ -10719,20 +10820,45 @@ var PARAMETER_TYPES = [
10719
10820
  "options"
10720
10821
  ];
10721
10822
  /**
10823
+ * What the value of a `string` or `text` argument holds, for the check at
10824
+ * story start and for tooling (see code-check.ts):
10825
+ * - `markup`: markup the macro renders (`{button}`'s label): its markup
10826
+ * is checked.
10827
+ * - `text`: plain text the macro uses as written (`{checkbox}`'s label).
10828
+ * - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
10829
+ * - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
10830
+ * is checked as code, and its variable references against the schema.
10831
+ *
10832
+ * Without it, the argument of a macro with `interpolate` holds `markup`,
10833
+ * any other `text`.
10834
+ */
10835
+ var STRING_HOLDS = [
10836
+ "markup",
10837
+ "text",
10838
+ "passage",
10839
+ "expression",
10840
+ "statements"
10841
+ ];
10842
+ /**
10722
10843
  * Throw if a parameter `macro` declares (or an option of one) has no type,
10723
10844
  * or one that isn't a ParameterType: arguments are read by their type, and
10724
- * there is no default.
10845
+ * there is no default. Throw too if it declares what it `holds` wrongly.
10725
10846
  */
10726
10847
  function checkParameterTypes(macro, parameters) {
10848
+ const holdsFix = `Give a \`string\` or \`text\` parameter one of ${STRING_HOLDS.join(", ")} to hold (see docs/custom-macros.md#what-a-string-holds).`;
10727
10849
  for (const param of parameters) {
10728
10850
  const type = param.type;
10729
- if (!PARAMETER_TYPES.includes(type)) {
10730
- const problem = type === void 0 ? "has no type" : `has the unknown type ${JSON.stringify(type)}`;
10731
- 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.`);
10732
- }
10851
+ 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.`);
10852
+ const holds = param.holds;
10853
+ if (holds !== void 0 && type !== "string" && type !== "text") throw parameterError(macro, param, `is of the type ${type}, which holds what it is`, holdsFix);
10854
+ if (holds !== void 0 && !STRING_HOLDS.includes(holds)) throw parameterError(macro, param, `holds the unknown ${JSON.stringify(holds)}`, holdsFix);
10733
10855
  if (param.parameters) checkParameterTypes(macro, param.parameters);
10734
10856
  }
10735
10857
  }
10858
+ /** The error for the parameter `param` of `macro`: what is wrong, and the fix. */
10859
+ function parameterError(macro, param, problem, fix) {
10860
+ return /* @__PURE__ */ new Error(`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ${fix}`);
10861
+ }
10736
10862
  new NameMap();
10737
10863
  //#endregion
10738
10864
  //#region src/utils/namespace.ts
@@ -10918,20 +11044,19 @@ var transformCache = /* @__PURE__ */ new Map();
10918
11044
  */
10919
11045
  function validateStoryMarkup(passages, macros, options = {}) {
10920
11046
  const list = [...passages];
11047
+ const all = [...macros];
10921
11048
  const known = /* @__PURE__ */ new Set();
10922
- const parameters = /* @__PURE__ */ new Map();
10923
11049
  const blocks = new Set(blockWidgetNames(list).map((n) => n.toLowerCase()));
10924
- for (const macro of macros) {
11050
+ for (const macro of all) {
10925
11051
  const name = macro.name.toLowerCase();
10926
11052
  known.add(name);
10927
- parameters.set(name, macro.parameters);
10928
11053
  for (const sub of macro.subMacros) known.add(sub.toLowerCase());
10929
11054
  if (macro.block) blocks.add(name);
10930
11055
  }
10931
11056
  return validateMarkup(list, {
10932
11057
  isKnownMacro: (name) => known.has(name),
10933
11058
  macroNames: known,
10934
- parametersOf: (name) => parameters.get(name),
11059
+ parametersOf: parameterLookup(all),
10935
11060
  isBlockMacro: (name) => blocks.has(name.toLowerCase()) || isBlockMacro(name),
10936
11061
  checkPassageNames: options.checkPassageNames
10937
11062
  });
@@ -10942,10 +11067,8 @@ function validateStoryMarkup(passages, macros, options = {}) {
10942
11067
  * skipped, so it reads half-typed markup.
10943
11068
  */
10944
11069
  function collectStoryPassageReferences(source, macros) {
10945
- const parameters = /* @__PURE__ */ new Map();
10946
- for (const macro of macros) parameters.set(macro.name.toLowerCase(), macro.parameters);
10947
11070
  const { tokens } = tokenizeMarkupTolerant(source);
10948
- return collectPassageReferences(source, tokens, (name) => parameters.get(name.toLowerCase()) ?? subMacroParameters(name));
11071
+ return collectPassageReferences(source, tokens, parameterLookup(macros));
10949
11072
  }
10950
11073
  //#endregion
10951
11074
  export { MarkupError, SIGIL_SCOPES, checkParameterTypes, collectStoryPassageReferences, endsWithOperator, evaluatePassageName, findCodeEnd, formatDiagnostic, isSigil, lexJs, lexTemplate, parseSelectors, parseStoryVariables, passageTarget, readQuoted, scanStringLiteral, splitArgs, splitIncludeFlag, splitTopLevel, stripLooseQuotes, tokenizeMarkup, tokenizeMarkupTolerant, transform, unescapeQuoted, validateStoryMarkup };