@rohal12/spindle 0.57.0 → 0.59.0

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.
@@ -32,15 +32,43 @@ function create() {
32
32
  return (initializer) => bindPreact(createStore()(initializer));
33
33
  }
34
34
  //#endregion
35
- //#region src/markup/spindle.peggy
36
- var LB = String.fromCharCode(123);
37
- var RB = String.fromCharCode(125);
35
+ //#region src/markup/tokens.ts
36
+ /** Variable sigils: story ($), temporary (_), local (@), transient (%). */
38
37
  var SIGIL_SCOPES$1 = {
39
38
  $: "variable",
40
39
  _: "temporary",
41
40
  "@": "local",
42
41
  "%": "transient"
43
42
  };
43
+ /** The sigil of each variable scope. */
44
+ var SCOPE_SIGILS = Object.fromEntries(Object.entries(SIGIL_SCOPES$1).map(([sigil, scope]) => [scope, sigil]));
45
+ var SIGIL_CHARS = new Set(Object.keys(SIGIL_SCOPES$1));
46
+ /** Whether `c` is a variable sigil. */
47
+ function isSigil(c) {
48
+ return c !== void 0 && SIGIL_CHARS.has(c);
49
+ }
50
+ /** Copy the selectors set in `from` onto `target`, and return it. */
51
+ function withSelectors$1(target, from) {
52
+ if (from.className) target.className = from.className;
53
+ if (from.id) target.id = from.id;
54
+ return target;
55
+ }
56
+ //#endregion
57
+ //#region src/markup/grammar-shared.ts
58
+ /**
59
+ * What the grammar (spindle.peggy) imports from the rest of Spindle, so that
60
+ * it holds no second copy: the generated parser imports this module's
61
+ * default export as `shared` (see scripts/peggy.ts).
62
+ */
63
+ var grammar_shared_default = {
64
+ SIGIL_SCOPES: SIGIL_SCOPES$1,
65
+ withSelectors: withSelectors$1
66
+ };
67
+ //#endregion
68
+ //#region src/markup/spindle.peggy
69
+ var LB = String.fromCharCode(123);
70
+ var RB = String.fromCharCode(125);
71
+ var { SIGIL_SCOPES, withSelectors } = grammar_shared_default;
44
72
  var VOID_TAGS = new Set([
45
73
  "area",
46
74
  "base",
@@ -75,34 +103,46 @@ var LINK_SEPARATORS = [
75
103
  ["<-", true]
76
104
  ];
77
105
  var NO_SELECTORS = {};
78
- 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
+ };
79
122
  for (const [separator, targetFirst] of LINK_SEPARATORS) {
80
123
  const idx = inner.indexOf(separator);
81
124
  if (idx === -1) continue;
82
- const before = inner.slice(0, idx).trim();
83
- const after = inner.slice(idx + separator.length).trim();
84
- return targetFirst ? {
85
- display: after,
86
- target: before
87
- } : {
88
- display: before,
89
- 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
90
133
  };
91
134
  }
92
- const trimmed = inner.trim();
135
+ const whole = part(0, inner.length);
93
136
  return {
94
- display: trimmed,
95
- target: trimmed
137
+ display: whole.value,
138
+ target: whole.value,
139
+ targetStart: whole.start,
140
+ targetEnd: whole.end
96
141
  };
97
142
  }
98
143
  function quoteArg(value) {
99
144
  return "\"" + value.replace(/[\\"]/g, "\\$&") + "\"";
100
145
  }
101
- function withSelectors(target, from) {
102
- if (from.className) target.className = from.className;
103
- if (from.id) target.id = from.id;
104
- return target;
105
- }
106
146
  function selectorsOf(parts) {
107
147
  const classes = [];
108
148
  let id = "";
@@ -179,6 +219,8 @@ function tokenOf(x) {
179
219
  type: "link",
180
220
  display: x.display,
181
221
  target: x.target,
222
+ targetStart: x.targetStart,
223
+ targetEnd: x.targetEnd,
182
224
  ...span
183
225
  }, x);
184
226
  case "variable": return withSelectors({
@@ -322,7 +364,8 @@ function peg$parse(input, options) {
322
364
  const peg$source = options.grammarSource;
323
365
  const peg$startRuleFunctions = {
324
366
  Markup: peg$parseMarkup,
325
- Tokens: peg$parseTokens
367
+ Tokens: peg$parseTokens,
368
+ SelectorsPrefix: peg$parseSelectorsPrefix
326
369
  };
327
370
  let peg$startRuleFunction = peg$parseMarkup;
328
371
  const peg$c0 = "\\";
@@ -709,8 +752,8 @@ function peg$parse(input, options) {
709
752
  function peg$f48() {
710
753
  return !textMode;
711
754
  }
712
- function peg$f49(s, inner) {
713
- 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);
714
757
  }
715
758
  function peg$f50() {
716
759
  return !textMode;
@@ -724,40 +767,46 @@ function peg$parse(input, options) {
724
767
  function peg$f53() {
725
768
  return NO_SELECTORS;
726
769
  }
727
- function peg$f54(prefix, name) {
770
+ function peg$f54(s, end) {
771
+ return {
772
+ ...s,
773
+ end
774
+ };
775
+ }
776
+ function peg$f55(prefix, name) {
728
777
  return [prefix, name];
729
778
  }
730
- function peg$f55(s, sigil, name) {
779
+ function peg$f56(s, sigil, name) {
731
780
  return withSelectors(lexeme("variable", location(), {
732
781
  name,
733
- scope: SIGIL_SCOPES$1[sigil]
782
+ scope: SIGIL_SCOPES[sigil]
734
783
  }), s);
735
784
  }
736
- function peg$f56(s, expression) {
785
+ function peg$f57(s, expression) {
737
786
  return withSelectors(lexeme("expression", location(), { expression }), s);
738
787
  }
739
- function peg$f57() {
788
+ function peg$f58() {
740
789
  fail("Unclosed " + LB + input.substr(location().end.offset, 1) + "…: no } ends it", location());
741
790
  }
742
- function peg$f58(start) {
791
+ function peg$f59(start) {
743
792
  return skipTo(hooks.closeBrace(input, start, start));
744
793
  }
745
- function peg$f59(start) {
794
+ function peg$f60(start) {
746
795
  return skipEnd - offset();
747
796
  }
748
- function peg$f60(start, code) {
797
+ function peg$f61(start, code) {
749
798
  return code;
750
799
  }
751
- function peg$f61(s, start, close, name, nameEnd) {
800
+ function peg$f62(s, start, close, name, nameEnd) {
752
801
  return skipTo(hooks.closeBrace(input, nameEnd, start));
753
802
  }
754
- function peg$f62(s, start, close, name, nameEnd) {
803
+ function peg$f63(s, start, close, name, nameEnd) {
755
804
  return skipEnd - offset();
756
805
  }
757
- function peg$f63(s, start, close, name, nameEnd, args) {
806
+ function peg$f64(s, start, close, name, nameEnd, args) {
758
807
  return args;
759
808
  }
760
- function peg$f64(s, start, close, name, nameEnd, tail) {
809
+ function peg$f65(s, start, close, name, nameEnd, tail) {
761
810
  const loc = location();
762
811
  const lower = name.toLowerCase();
763
812
  const tag = lexeme("macro", loc, {
@@ -778,19 +827,19 @@ function peg$parse(input, options) {
778
827
  }
779
828
  return tag;
780
829
  }
781
- function peg$f65(s) {
830
+ function peg$f66(s) {
782
831
  fail("A closing tag starts with a letter after " + LB + "/", location());
783
832
  }
784
- function peg$f66() {
833
+ function peg$f67() {
785
834
  return !textMode;
786
835
  }
787
- function peg$f67(tag) {
836
+ function peg$f68(tag) {
788
837
  return VOID_TAGS.has(tag.lower);
789
838
  }
790
- function peg$f68(tag) {
839
+ function peg$f69(tag) {
791
840
  return null;
792
841
  }
793
- function peg$f69(tag, list, slash) {
842
+ function peg$f70(tag, list, slash) {
794
843
  const lower = tag.toLowerCase();
795
844
  return lexeme("html", location(), {
796
845
  tag,
@@ -800,7 +849,7 @@ function peg$parse(input, options) {
800
849
  isSelfClose: !!slash || VOID_TAGS.has(lower)
801
850
  });
802
851
  }
803
- function peg$f70(tag, bad) {
852
+ function peg$f71(tag, bad) {
804
853
  const c = input.charAt(bad);
805
854
  if (!c) fail("Unclosed tag <" + tag + ": no > ends it (write &lt; for a < that starts no tag)", location());
806
855
  const at = {
@@ -818,7 +867,7 @@ function peg$parse(input, options) {
818
867
  };
819
868
  fail("Unexpected " + JSON.stringify(c) + " in the tag <" + tag + ">: expected an attribute, or > to end the tag", at);
820
869
  }
821
- function peg$f71(tag) {
870
+ function peg$f72(tag) {
822
871
  return lexeme("html", location(), {
823
872
  tag,
824
873
  lower: tag.toLowerCase(),
@@ -827,44 +876,44 @@ function peg$parse(input, options) {
827
876
  isSelfClose: false
828
877
  });
829
878
  }
830
- function peg$f72(name) {
879
+ function peg$f73(name) {
831
880
  attrCode = hooks.isCodeAttribute(name);
832
881
  return true;
833
882
  }
834
- function peg$f73(name, v) {
883
+ function peg$f74(name, v) {
835
884
  return v;
836
885
  }
837
- function peg$f74(name, value) {
886
+ function peg$f75(name, value) {
838
887
  return [name, value ?? ""];
839
888
  }
840
- function peg$f75(v) {
889
+ function peg$f76(v) {
841
890
  return v;
842
891
  }
843
- function peg$f76(v) {
892
+ function peg$f77(v) {
844
893
  return v;
845
894
  }
846
- function peg$f77(q) {
895
+ function peg$f78(q) {
847
896
  fail("Unclosed attribute value: the " + q + " here has no closing " + q, location());
848
897
  }
849
- function peg$f78() {
898
+ function peg$f79() {
850
899
  return !attrCode;
851
900
  }
852
- function peg$f79(bs) {
901
+ function peg$f80(bs) {
853
902
  return bs.length % 2 === 1;
854
903
  }
855
- function peg$f80() {
904
+ function peg$f81() {
856
905
  return !attrCode;
857
906
  }
858
- function peg$f81(start, nameEnd) {
907
+ function peg$f82(start, nameEnd) {
859
908
  return skipTo(hooks.closeBrace(input, nameEnd, start));
860
909
  }
861
- function peg$f82(start, nameEnd) {
910
+ function peg$f83(start, nameEnd) {
862
911
  return skipEnd - offset();
863
912
  }
864
- function peg$f83() {
913
+ function peg$f84() {
865
914
  return !attrCode;
866
915
  }
867
- function peg$f84() {
916
+ function peg$f85() {
868
917
  return attrCode;
869
918
  }
870
919
  let peg$currPos = options.peg$currPos | 0;
@@ -1753,7 +1802,7 @@ function peg$parse(input, options) {
1753
1802
  return s0;
1754
1803
  }
1755
1804
  function peg$parseLinkLexeme() {
1756
- let s0, s1, s2, s3, s4, s5;
1805
+ let s0, s1, s2, s3, s4, s5, s6;
1757
1806
  s0 = peg$currPos;
1758
1807
  peg$savedPos = peg$currPos;
1759
1808
  s1 = peg$f48();
@@ -1770,19 +1819,20 @@ function peg$parse(input, options) {
1770
1819
  if (s2 !== peg$FAILED) {
1771
1820
  s3 = peg$parseSelectors();
1772
1821
  if (s3 !== peg$FAILED) {
1773
- s4 = peg$currPos;
1774
- s5 = peg$parseLinkText();
1775
- 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);
1776
1826
  if (input.substr(peg$currPos, 2) === peg$c5) {
1777
- s5 = peg$c5;
1827
+ s6 = peg$c5;
1778
1828
  peg$currPos += 2;
1779
1829
  } else {
1780
- s5 = peg$FAILED;
1830
+ s6 = peg$FAILED;
1781
1831
  if (peg$silentFails === 0) peg$fail(peg$e9);
1782
1832
  }
1783
- if (s5 !== peg$FAILED) {
1833
+ if (s6 !== peg$FAILED) {
1784
1834
  peg$savedPos = s0;
1785
- s0 = peg$f49(s3, s4);
1835
+ s0 = peg$f49(s3, s4, s5);
1786
1836
  } else {
1787
1837
  peg$currPos = s0;
1788
1838
  s0 = peg$FAILED;
@@ -1953,6 +2003,38 @@ function peg$parse(input, options) {
1953
2003
  }
1954
2004
  return s0;
1955
2005
  }
2006
+ function peg$parseSelectorsPrefix() {
2007
+ let s0, s1, s2, s3, s4;
2008
+ s0 = peg$currPos;
2009
+ s1 = peg$parseSelectors();
2010
+ if (s1 !== peg$FAILED) {
2011
+ s2 = peg$parseHere();
2012
+ s3 = [];
2013
+ if (input.length > peg$currPos) {
2014
+ s4 = input.charAt(peg$currPos);
2015
+ peg$currPos++;
2016
+ } else {
2017
+ s4 = peg$FAILED;
2018
+ if (peg$silentFails === 0) peg$fail(peg$e0);
2019
+ }
2020
+ while (s4 !== peg$FAILED) {
2021
+ s3.push(s4);
2022
+ if (input.length > peg$currPos) {
2023
+ s4 = input.charAt(peg$currPos);
2024
+ peg$currPos++;
2025
+ } else {
2026
+ s4 = peg$FAILED;
2027
+ if (peg$silentFails === 0) peg$fail(peg$e0);
2028
+ }
2029
+ }
2030
+ peg$savedPos = s0;
2031
+ s0 = peg$f54(s1, s2);
2032
+ } else {
2033
+ peg$currPos = s0;
2034
+ s0 = peg$FAILED;
2035
+ }
2036
+ return s0;
2037
+ }
1956
2038
  function peg$parseSelectorPart() {
1957
2039
  let s0, s1, s2, s3, s4;
1958
2040
  s0 = peg$currPos;
@@ -1972,7 +2054,7 @@ function peg$parse(input, options) {
1972
2054
  }
1973
2055
  s2 = input.substring(s2, peg$currPos);
1974
2056
  peg$savedPos = s0;
1975
- s0 = peg$f54(s1, s2);
2057
+ s0 = peg$f55(s1, s2);
1976
2058
  } else {
1977
2059
  peg$currPos = s0;
1978
2060
  s0 = peg$FAILED;
@@ -2097,7 +2179,7 @@ function peg$parse(input, options) {
2097
2179
  }
2098
2180
  if (s5 !== peg$FAILED) {
2099
2181
  peg$savedPos = s0;
2100
- s0 = peg$f55(s2, s3, s4);
2182
+ s0 = peg$f56(s2, s3, s4);
2101
2183
  } else {
2102
2184
  peg$currPos = s0;
2103
2185
  s0 = peg$FAILED;
@@ -2151,7 +2233,7 @@ function peg$parse(input, options) {
2151
2233
  }
2152
2234
  if (s5 !== peg$FAILED) {
2153
2235
  peg$savedPos = s0;
2154
- s0 = peg$f56(s2, s4);
2236
+ s0 = peg$f57(s2, s4);
2155
2237
  } else {
2156
2238
  peg$currPos = s0;
2157
2239
  s0 = peg$FAILED;
@@ -2199,7 +2281,7 @@ function peg$parse(input, options) {
2199
2281
  } else s3 = peg$FAILED;
2200
2282
  if (s3 !== peg$FAILED) {
2201
2283
  peg$savedPos = s0;
2202
- s0 = peg$f57();
2284
+ s0 = peg$f58();
2203
2285
  } else {
2204
2286
  peg$currPos = s0;
2205
2287
  s0 = peg$FAILED;
@@ -2221,12 +2303,12 @@ function peg$parse(input, options) {
2221
2303
  s0 = peg$currPos;
2222
2304
  s1 = peg$parseHere();
2223
2305
  peg$savedPos = peg$currPos;
2224
- s2 = peg$f58(s1);
2306
+ s2 = peg$f59(s1);
2225
2307
  if (s2) s2 = void 0;
2226
2308
  else s2 = peg$FAILED;
2227
2309
  if (s2 !== peg$FAILED) {
2228
2310
  s3 = peg$currPos;
2229
- s4 = peg$f59(s1);
2311
+ s4 = peg$f60(s1);
2230
2312
  s5 = peg$currPos;
2231
2313
  s6 = [];
2232
2314
  if (s6.length >= (s4 | 0)) s7 = peg$FAILED;
@@ -2257,7 +2339,7 @@ function peg$parse(input, options) {
2257
2339
  else s3 = s4;
2258
2340
  if (s3 !== peg$FAILED) {
2259
2341
  peg$savedPos = s0;
2260
- s0 = peg$f60(s1, s3);
2342
+ s0 = peg$f61(s1, s3);
2261
2343
  } else {
2262
2344
  peg$currPos = s0;
2263
2345
  s0 = peg$FAILED;
@@ -2295,12 +2377,12 @@ function peg$parse(input, options) {
2295
2377
  s6 = peg$parseHere();
2296
2378
  s7 = peg$currPos;
2297
2379
  peg$savedPos = peg$currPos;
2298
- s8 = peg$f61(s2, s3, s4, s5, s6);
2380
+ s8 = peg$f62(s2, s3, s4, s5, s6);
2299
2381
  if (s8) s8 = void 0;
2300
2382
  else s8 = peg$FAILED;
2301
2383
  if (s8 !== peg$FAILED) {
2302
2384
  s9 = peg$currPos;
2303
- s10 = peg$f62(s2, s3, s4, s5, s6);
2385
+ s10 = peg$f63(s2, s3, s4, s5, s6);
2304
2386
  s11 = peg$currPos;
2305
2387
  s12 = [];
2306
2388
  if (s12.length >= (s10 | 0)) s13 = peg$FAILED;
@@ -2339,7 +2421,7 @@ function peg$parse(input, options) {
2339
2421
  }
2340
2422
  if (s10 !== peg$FAILED) {
2341
2423
  peg$savedPos = s7;
2342
- s7 = peg$f63(s2, s3, s4, s5, s6, s9);
2424
+ s7 = peg$f64(s2, s3, s4, s5, s6, s9);
2343
2425
  } else {
2344
2426
  peg$currPos = s7;
2345
2427
  s7 = peg$FAILED;
@@ -2354,7 +2436,7 @@ function peg$parse(input, options) {
2354
2436
  }
2355
2437
  if (s7 === peg$FAILED) s7 = null;
2356
2438
  peg$savedPos = s0;
2357
- s0 = peg$f64(s2, s3, s4, s5, s6, s7);
2439
+ s0 = peg$f65(s2, s3, s4, s5, s6, s7);
2358
2440
  } else {
2359
2441
  peg$currPos = s0;
2360
2442
  s0 = peg$FAILED;
@@ -2388,7 +2470,7 @@ function peg$parse(input, options) {
2388
2470
  }
2389
2471
  if (s3 !== peg$FAILED) {
2390
2472
  peg$savedPos = s0;
2391
- s0 = peg$f65(s2);
2473
+ s0 = peg$f66(s2);
2392
2474
  } else {
2393
2475
  peg$currPos = s0;
2394
2476
  s0 = peg$FAILED;
@@ -2445,19 +2527,19 @@ function peg$parse(input, options) {
2445
2527
  let s0, s1, s2, s3;
2446
2528
  s0 = peg$currPos;
2447
2529
  peg$savedPos = peg$currPos;
2448
- s1 = peg$f66();
2530
+ s1 = peg$f67();
2449
2531
  if (s1) s1 = void 0;
2450
2532
  else s1 = peg$FAILED;
2451
2533
  if (s1 !== peg$FAILED) {
2452
2534
  s2 = peg$parseHtmlClose();
2453
2535
  if (s2 !== peg$FAILED) {
2454
2536
  peg$savedPos = peg$currPos;
2455
- s3 = peg$f67(s2);
2537
+ s3 = peg$f68(s2);
2456
2538
  if (s3) s3 = void 0;
2457
2539
  else s3 = peg$FAILED;
2458
2540
  if (s3 !== peg$FAILED) {
2459
2541
  peg$savedPos = s0;
2460
- s0 = peg$f68(s2);
2542
+ s0 = peg$f69(s2);
2461
2543
  } else {
2462
2544
  peg$currPos = s0;
2463
2545
  s0 = peg$FAILED;
@@ -2509,7 +2591,7 @@ function peg$parse(input, options) {
2509
2591
  }
2510
2592
  if (s6 !== peg$FAILED) {
2511
2593
  peg$savedPos = s0;
2512
- s0 = peg$f69(s2, s3, s5);
2594
+ s0 = peg$f70(s2, s3, s5);
2513
2595
  } else {
2514
2596
  peg$currPos = s0;
2515
2597
  s0 = peg$FAILED;
@@ -2574,7 +2656,7 @@ function peg$parse(input, options) {
2574
2656
  s5 = peg$parse_();
2575
2657
  s6 = peg$parseHere();
2576
2658
  peg$savedPos = s0;
2577
- s0 = peg$f70(s2, s6);
2659
+ s0 = peg$f71(s2, s6);
2578
2660
  } else {
2579
2661
  peg$currPos = s0;
2580
2662
  s0 = peg$FAILED;
@@ -2613,7 +2695,7 @@ function peg$parse(input, options) {
2613
2695
  }
2614
2696
  if (s4 !== peg$FAILED) {
2615
2697
  peg$savedPos = s0;
2616
- s0 = peg$f71(s2);
2698
+ s0 = peg$f72(s2);
2617
2699
  } else {
2618
2700
  peg$currPos = s0;
2619
2701
  s0 = peg$FAILED;
@@ -2747,7 +2829,7 @@ function peg$parse(input, options) {
2747
2829
  else s2 = s3;
2748
2830
  if (s2 !== peg$FAILED) {
2749
2831
  peg$savedPos = peg$currPos;
2750
- s3 = peg$f72(s2);
2832
+ s3 = peg$f73(s2);
2751
2833
  if (s3) s3 = void 0;
2752
2834
  else s3 = peg$FAILED;
2753
2835
  if (s3 !== peg$FAILED) {
@@ -2765,7 +2847,7 @@ function peg$parse(input, options) {
2765
2847
  s8 = peg$parseAttrValue();
2766
2848
  if (s8 !== peg$FAILED) {
2767
2849
  peg$savedPos = s4;
2768
- s4 = peg$f73(s2, s8);
2850
+ s4 = peg$f74(s2, s8);
2769
2851
  } else {
2770
2852
  peg$currPos = s4;
2771
2853
  s4 = peg$FAILED;
@@ -2776,7 +2858,7 @@ function peg$parse(input, options) {
2776
2858
  }
2777
2859
  if (s4 === peg$FAILED) s4 = null;
2778
2860
  peg$savedPos = s0;
2779
- s0 = peg$f74(s2, s4);
2861
+ s0 = peg$f75(s2, s4);
2780
2862
  } else {
2781
2863
  peg$currPos = s0;
2782
2864
  s0 = peg$FAILED;
@@ -2831,7 +2913,7 @@ function peg$parse(input, options) {
2831
2913
  }
2832
2914
  if (s3 !== peg$FAILED) {
2833
2915
  peg$savedPos = s0;
2834
- s0 = peg$f75(s2);
2916
+ s0 = peg$f76(s2);
2835
2917
  } else {
2836
2918
  peg$currPos = s0;
2837
2919
  s0 = peg$FAILED;
@@ -2883,7 +2965,7 @@ function peg$parse(input, options) {
2883
2965
  }
2884
2966
  if (s3 !== peg$FAILED) {
2885
2967
  peg$savedPos = s0;
2886
- s0 = peg$f76(s2);
2968
+ s0 = peg$f77(s2);
2887
2969
  } else {
2888
2970
  peg$currPos = s0;
2889
2971
  s0 = peg$FAILED;
@@ -2902,7 +2984,7 @@ function peg$parse(input, options) {
2902
2984
  }
2903
2985
  if (s1 !== peg$FAILED) {
2904
2986
  peg$savedPos = s0;
2905
- s1 = peg$f77(s1);
2987
+ s1 = peg$f78(s1);
2906
2988
  }
2907
2989
  s0 = s1;
2908
2990
  if (s0 === peg$FAILED) {
@@ -2933,7 +3015,7 @@ function peg$parse(input, options) {
2933
3015
  let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13;
2934
3016
  s0 = peg$currPos;
2935
3017
  peg$savedPos = peg$currPos;
2936
- s1 = peg$f78();
3018
+ s1 = peg$f79();
2937
3019
  if (s1) s1 = void 0;
2938
3020
  else s1 = peg$FAILED;
2939
3021
  if (s1 !== peg$FAILED) {
@@ -2962,7 +3044,7 @@ function peg$parse(input, options) {
2962
3044
  if (s2 !== peg$FAILED) {
2963
3045
  s3 = peg$currPos;
2964
3046
  peg$savedPos = peg$currPos;
2965
- s4 = peg$f79(s2);
3047
+ s4 = peg$f80(s2);
2966
3048
  if (s4) s4 = void 0;
2967
3049
  else s4 = peg$FAILED;
2968
3050
  if (s4 !== peg$FAILED) {
@@ -3001,7 +3083,7 @@ function peg$parse(input, options) {
3001
3083
  if (s0 === peg$FAILED) {
3002
3084
  s0 = peg$currPos;
3003
3085
  peg$savedPos = peg$currPos;
3004
- s1 = peg$f80();
3086
+ s1 = peg$f81();
3005
3087
  if (s1) s1 = void 0;
3006
3088
  else s1 = peg$FAILED;
3007
3089
  if (s1 !== peg$FAILED) {
@@ -3028,12 +3110,12 @@ function peg$parse(input, options) {
3028
3110
  if (s6 !== peg$FAILED) {
3029
3111
  s7 = peg$parseHere();
3030
3112
  peg$savedPos = peg$currPos;
3031
- s8 = peg$f81(s4, s7);
3113
+ s8 = peg$f82(s4, s7);
3032
3114
  if (s8) s8 = void 0;
3033
3115
  else s8 = peg$FAILED;
3034
3116
  if (s8 !== peg$FAILED) {
3035
3117
  s9 = peg$currPos;
3036
- s10 = peg$f82(s4, s7);
3118
+ s10 = peg$f83(s4, s7);
3037
3119
  s11 = peg$currPos;
3038
3120
  s12 = [];
3039
3121
  if (s12.length >= (s10 | 0)) s13 = peg$FAILED;
@@ -3115,7 +3197,7 @@ function peg$parse(input, options) {
3115
3197
  if (s0 === peg$FAILED) {
3116
3198
  s0 = peg$currPos;
3117
3199
  peg$savedPos = peg$currPos;
3118
- s1 = peg$f83();
3200
+ s1 = peg$f84();
3119
3201
  if (s1) s1 = void 0;
3120
3202
  else s1 = peg$FAILED;
3121
3203
  if (s1 !== peg$FAILED) {
@@ -3206,7 +3288,7 @@ function peg$parse(input, options) {
3206
3288
  if (s0 === peg$FAILED) {
3207
3289
  s0 = peg$currPos;
3208
3290
  peg$savedPos = peg$currPos;
3209
- s1 = peg$f84();
3291
+ s1 = peg$f85();
3210
3292
  if (s1) s1 = void 0;
3211
3293
  else s1 = peg$FAILED;
3212
3294
  if (s1 !== peg$FAILED) {
@@ -3441,22 +3523,6 @@ function registerBlockMacro(name) {
3441
3523
  BLOCK_MACROS.add(name);
3442
3524
  }
3443
3525
  //#endregion
3444
- //#region src/markup/tokens.ts
3445
- /** Variable sigils: story ($), temporary (_), local (@), transient (%). */
3446
- var SIGIL_SCOPES = {
3447
- $: "variable",
3448
- _: "temporary",
3449
- "@": "local",
3450
- "%": "transient"
3451
- };
3452
- /** The sigil of each variable scope. */
3453
- var SCOPE_SIGILS = Object.fromEntries(Object.entries(SIGIL_SCOPES).map(([sigil, scope]) => [scope, sigil]));
3454
- var SIGIL_CHARS = new Set(Object.keys(SIGIL_SCOPES));
3455
- /** Whether `c` is a variable sigil. */
3456
- function isSigil(c) {
3457
- return c !== void 0 && SIGIL_CHARS.has(c);
3458
- }
3459
- //#endregion
3460
3526
  //#region node_modules/acorn/dist/acorn.mjs
3461
3527
  var astralIdentifierCodes = [
3462
3528
  509,
@@ -9395,6 +9461,22 @@ function regexEnd(src, start) {
9395
9461
  return -1;
9396
9462
  }
9397
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
+ /**
9398
9480
  * Scan the `"…"` or `'…'` string literal opening at `start`. `end` is the
9399
9481
  * index just past its closing quote, or `src.length` when it is unterminated
9400
9482
  * (`closed` false). A backslash escapes the character after it, so a quote
@@ -9876,9 +9958,14 @@ function createCounts(source, name) {
9876
9958
  return counts;
9877
9959
  }
9878
9960
  //#endregion
9879
- //#region src/expression.ts
9880
- var FN_CACHE_MAX = 500;
9881
- var fnCache = /* @__PURE__ */ new Map();
9961
+ //#region src/transform.ts
9962
+ /**
9963
+ * The sigil transform: the story's code with its variable references
9964
+ * (`$var`, `_var`, `@var`, `%var`) turned into the namespace lookups the
9965
+ * expression engine runs. Pure string-to-string, so tooling can use it
9966
+ * (see src/tooling.ts).
9967
+ */
9968
+ var TRANSFORM_CACHE_MAX = 500;
9882
9969
  var NAMESPACES$1 = {
9883
9970
  $: "variables",
9884
9971
  _: "temporary",
@@ -9898,8 +9985,7 @@ var IDENT_END_RE = /[\p{ID_Continue}$\u200c\u200d]$/u;
9898
9985
  * statements, as that decides whether a leading `{` opens an object literal
9899
9986
  * or a block. A reference to a variable named `__proto__` throws a
9900
9987
  * SyntaxError: no namespace can hold one (see utils/namespace.ts).
9901
- * Exported for tests.
9902
- */
9988
+ * */
9903
9989
  function transform(expr, goal = "expression") {
9904
9990
  const key = goal === "statements" ? "s" + expr : "e" + expr;
9905
9991
  const cached = transformCache.get(key);
@@ -9917,12 +10003,16 @@ function transform(expr, goal = "expression") {
9917
10003
  at = ref.end;
9918
10004
  }
9919
10005
  result += expr.slice(at);
9920
- if (transformCache.size >= FN_CACHE_MAX) transformCache.delete(transformCache.keys().next().value);
10006
+ if (transformCache.size >= TRANSFORM_CACHE_MAX) transformCache.delete(transformCache.keys().next().value);
9921
10007
  transformCache.set(key, result);
9922
10008
  return result;
9923
10009
  }
9924
10010
  /** Transformed code by goal and source. */
9925
10011
  var transformCache = /* @__PURE__ */ new Map();
10012
+ //#endregion
10013
+ //#region src/expression.ts
10014
+ var FN_CACHE_MAX = 500;
10015
+ var fnCache = /* @__PURE__ */ new Map();
9926
10016
  var preamble = "const {currentPassage,previousPassage,visited,hasVisited,hasVisitedAny,hasVisitedAll,rendered,hasRendered,hasRenderedAny,hasRenderedAll,random,randomInt}=__fns;";
9927
10017
  function getOrCompile(key, body) {
9928
10018
  const cached = fnCache.get(key);
@@ -10092,24 +10182,53 @@ var PARAMETER_TYPES = [
10092
10182
  "options"
10093
10183
  ];
10094
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
+ /**
10095
10205
  * Throw if a parameter `macro` declares (or an option of one) has no type,
10096
10206
  * or one that isn't a ParameterType: arguments are read by their type, and
10097
- * there is no default.
10207
+ * there is no default. Throw too if it declares what it `holds` wrongly.
10098
10208
  */
10099
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).`;
10100
10211
  for (const param of parameters) {
10101
10212
  const type = param.type;
10102
- if (!PARAMETER_TYPES.includes(type)) {
10103
- const problem = type === void 0 ? "has no type" : `has the unknown type ${JSON.stringify(type)}`;
10104
- 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.`);
10105
- }
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);
10106
10217
  if (param.parameters) checkParameterTypes(macro, param.parameters);
10107
10218
  }
10108
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
+ }
10109
10224
  var metadataRegistry = new NameMap();
10110
10225
  function registerMacroMetadata(name, metadata) {
10111
10226
  metadataRegistry.set(name, metadata);
10112
10227
  }
10228
+ /** The declared parameters of the macro `name`, if it declares any. */
10229
+ function getMacroParameters(name) {
10230
+ return metadataRegistry.get(name)?.parameters;
10231
+ }
10113
10232
  function getMacroRegistry() {
10114
10233
  return Array.from(metadataRegistry.values());
10115
10234
  }
@@ -10563,7 +10682,7 @@ function interpolateCode(template, scope, decode) {
10563
10682
  for (const part of splitSigilTemplate(template)) if ("text" in part) text += decode ? decode(part.text) : part.text;
10564
10683
  else if ("verbatim" in part) text += part.verbatim;
10565
10684
  else if (/^[$_@%][\w.]+$/.test(part.expr)) {
10566
- const kind = SIGIL_SCOPES[part.expr[0]];
10685
+ const kind = SIGIL_SCOPES$1[part.expr[0]];
10567
10686
  try {
10568
10687
  text += display(resolveVariable(scope, kind, part.expr.slice(1)));
10569
10688
  } catch (error) {
@@ -16474,11 +16593,13 @@ function readText(src) {
16474
16593
  */
16475
16594
  var MacroArgumentError = class extends Error {
16476
16595
  constructor(param, src) {
16477
- super(`The ${param.name} must be a quoted string ("…" or '…'), not ${src}`);
16596
+ super(`The ${param.name} must be a quoted string ("…" or '…'), ${src ? `not ${src}` : "but it has no value"}`);
16478
16597
  this.name = "MacroArgumentError";
16479
16598
  }
16480
16599
  };
16481
- function readValue(param, src) {
16600
+ /** Read `src`, at `place`, as `param` says. */
16601
+ function readValue(param, src, place) {
16602
+ place.spans?.set(param, [place.at, place.at + src.length]);
16482
16603
  switch (param.type) {
16483
16604
  case "string": {
16484
16605
  const value = readWholeQuoted(src);
@@ -16489,7 +16610,7 @@ function readValue(param, src) {
16489
16610
  case "names": return src.split(",").map((name) => name.trim());
16490
16611
  case "delay": return parseDelay(src);
16491
16612
  case "number": return Number(src);
16492
- case "options": return readOptions(param.parameters ?? [], src);
16613
+ case "options": return readOptions(param.parameters ?? [], src, place);
16493
16614
  default: return src;
16494
16615
  }
16495
16616
  }
@@ -16499,6 +16620,7 @@ function unset(param) {
16499
16620
  }
16500
16621
  var WORD_RE = /\w+/y;
16501
16622
  var DIGITS_RE = /\d+/y;
16623
+ var WORD_OR_PUNCT_RE = /\S+/y;
16502
16624
  /** Match the sticky regex `re` at `pos`, returning the matched text. */
16503
16625
  function matchAt(re, src, pos) {
16504
16626
  re.lastIndex = pos;
@@ -16506,10 +16628,12 @@ function matchAt(re, src, pos) {
16506
16628
  }
16507
16629
  /**
16508
16630
  * Read keyword options (`goto "X" priority 5 once`). A keyword takes the
16509
- * quoted string or digit run after it as its value; keywords that aren't
16510
- * declared, and their values, are skipped.
16631
+ * quoted string (with or without whitespace before it) or digit run after
16632
+ * it as its value; keywords that aren't declared, and their values, are
16633
+ * skipped. A keyword of a `string` parameter must be followed by a quoted
16634
+ * string.
16511
16635
  */
16512
- function readOptions(params, src) {
16636
+ function readOptions(params, src, { at, spans }) {
16513
16637
  const options = {};
16514
16638
  let i = 0;
16515
16639
  while (i < src.length) {
@@ -16522,18 +16646,18 @@ function readOptions(params, src) {
16522
16646
  let val;
16523
16647
  let j = i;
16524
16648
  while (j < src.length && isWhitespace(src[j])) j++;
16525
- if (j > i) {
16526
- const quoted = readQuoted(src, j);
16527
- const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
16528
- if (quoted) {
16529
- val = quoted.value;
16530
- i = quoted.end;
16531
- } else if (digits) {
16532
- val = digits;
16533
- i = j + digits.length;
16534
- }
16649
+ const quoted = readQuoted(src, j);
16650
+ const digits = quoted || j === i ? null : matchAt(DIGITS_RE, src, j);
16651
+ if (quoted) {
16652
+ val = quoted.value;
16653
+ i = quoted.end;
16654
+ } else if (digits) {
16655
+ val = digits;
16656
+ i = j + digits.length;
16535
16657
  }
16536
16658
  const param = params.find((p) => p.name === key);
16659
+ if (param && param.type !== "flag" && val !== void 0) spans?.set(param, [at + j, at + i]);
16660
+ if (param?.type === "string" && !quoted) throw new MacroArgumentError(param, matchAt(WORD_OR_PUNCT_RE, src, j) ?? "");
16537
16661
  if (param?.type === "flag") options[key] = true;
16538
16662
  else if (param?.type === "number") options[key] = Number(val ?? 0);
16539
16663
  else if (param) options[key] = val;
@@ -16543,21 +16667,21 @@ function readOptions(params, src) {
16543
16667
  /** `inline %name` reads a transient variable; `inline % 2` is modulo. */
16544
16668
  var LEADING_OPERATOR_RE = /^(?:[-+*/&|^=<>?:,.]|%(?![A-Za-z_]))/;
16545
16669
  /**
16546
- * The rest of `src` without the flag `word` as its last or first term, or
16547
- * `null` if it has no such flag. Next to a binary operator the word is an
16548
- * operand (`"a" + inline`), not a flag; the closing `/` of a regex literal
16549
- * is not one.
16670
+ * Where the rest of `src` is without the flag `word` as its last or first
16671
+ * term, or `null` if it has no such flag. Next to a binary operator the
16672
+ * word is an operand (`"a" + inline`), not a flag; the closing `/` of a
16673
+ * regex literal is not one.
16550
16674
  */
16551
16675
  function takeFlag(src, word) {
16552
16676
  const spans = terms(src);
16553
16677
  if (spans.length < 2) return null;
16554
16678
  if (slice(src, spans[spans.length - 1]) === word) {
16555
- const rest = src.slice(0, spans[spans.length - 2][1]);
16556
- if (!endsWithOperator(rest)) return rest;
16679
+ const rest = [0, spans[spans.length - 2][1]];
16680
+ if (!endsWithOperator(slice(src, rest))) return rest;
16557
16681
  }
16558
16682
  if (slice(src, spans[0]) === word) {
16559
- const rest = src.slice(spans[1][0]);
16560
- if (!LEADING_OPERATOR_RE.test(rest)) return rest;
16683
+ const rest = [spans[1][0], src.length];
16684
+ if (!LEADING_OPERATOR_RE.test(slice(src, rest))) return rest;
16561
16685
  }
16562
16686
  return null;
16563
16687
  }
@@ -16585,42 +16709,55 @@ function findSeparator(src, word) {
16585
16709
  return null;
16586
16710
  }
16587
16711
  var takesRest = (param) => param.type === "expression" || param.type === "statements" || param.type === "passage" || param.type === "text";
16588
- /** Read the parameters of one group from `src` (`null`: no text) into `args`. */
16589
- function readGroup(params, src, args) {
16590
- let text = src?.trim() ?? "";
16712
+ /** The text of `src` without the whitespace around it, and where it is. */
16713
+ function trimmed(src, at) {
16714
+ const text = src.trim();
16715
+ return [text, text ? at + src.indexOf(text) : at];
16716
+ }
16717
+ /**
16718
+ * Read the parameters of one group from `src` (`null`: no text), which is
16719
+ * at `at` in the raw arguments, into `args`.
16720
+ */
16721
+ function readGroup(params, src, args, { at: srcAt, spans }) {
16722
+ let [text, at] = trimmed(src ?? "", srcAt);
16591
16723
  for (const flag of params.filter((p) => p.type === "flag")) {
16592
16724
  const rest = src === null ? null : takeFlag(text, flag.name);
16593
16725
  args[flag.name] = rest !== null;
16594
- text = rest ?? text;
16726
+ if (rest) [text, at] = trimmed(slice(text, rest), at + rest[0]);
16595
16727
  }
16596
16728
  const positional = params.filter((p) => p.type !== "flag");
16597
16729
  if (positional.length === 0) return;
16598
16730
  const lastText = positional.findIndex((p, i) => takesRest(p) && !positional.slice(i + 1).some(takesRest));
16599
16731
  const restIndex = lastText < 0 ? positional.length - 1 : lastText;
16600
- const read = (param, value) => value === "" ? unset(param) : readValue(param, value);
16732
+ /** Read `param` from the text from `start` to `end`. */
16733
+ const read = (param, [start, end]) => start === end ? unset(param) : readValue(param, text.slice(start, end), {
16734
+ at: at + start,
16735
+ spans
16736
+ });
16601
16737
  if (positional.length === 1) {
16602
- args[positional[0].name] = read(positional[0], text);
16738
+ args[positional[0].name] = read(positional[0], [0, text.length]);
16603
16739
  return;
16604
16740
  }
16605
- const spans = terms(text);
16741
+ const parts = terms(text);
16606
16742
  let from = 0;
16607
- let to = spans.length;
16608
- for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
16609
- const readTrailing = (param, value) => {
16743
+ let to = parts.length;
16744
+ for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, parts[from++]) : unset(param);
16745
+ const readTrailing = (param, span) => {
16610
16746
  try {
16611
- return read(param, value);
16747
+ return read(param, span);
16612
16748
  } catch (error) {
16613
16749
  if (error instanceof MacroArgumentError) return void 0;
16614
16750
  throw error;
16615
16751
  }
16616
16752
  };
16617
16753
  for (const param of positional.slice(restIndex + 1).reverse()) {
16618
- const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? readTrailing(param, slice(text, spans[to - 1])) : void 0;
16754
+ const value = from < to && !endsWithOperator(text.slice(0, parts[to - 1][0])) ? readTrailing(param, parts[to - 1]) : void 0;
16619
16755
  args[param.name] = value ?? unset(param);
16620
16756
  if (value !== void 0) to--;
16757
+ else spans?.delete(param);
16621
16758
  }
16622
16759
  const rest = positional[restIndex];
16623
- args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
16760
+ args[rest.name] = from < to ? read(rest, [parts[from][0], parts[to - 1][1]]) : unset(rest);
16624
16761
  }
16625
16762
  /** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
16626
16763
  var DELAY_PARAMETER = {
@@ -16628,24 +16765,52 @@ var DELAY_PARAMETER = {
16628
16765
  type: "delay",
16629
16766
  required: true
16630
16767
  };
16631
- /** Read `rawArgs` into the declared `parameters` (see above). */
16632
- function parseMacroArgs(rawArgs, parameters) {
16768
+ /**
16769
+ * Read `rawArgs` into the declared `parameters` (see above). With `spans`,
16770
+ * note there where each argument set was read.
16771
+ */
16772
+ function parseMacroArgs(rawArgs, parameters, spans) {
16633
16773
  const args = {};
16634
- let rest = rawArgs.trim();
16774
+ let [rest, restAt] = trimmed(rawArgs, 0);
16635
16775
  let groupStart = 0;
16636
16776
  parameters.forEach((separator, i) => {
16637
16777
  if (separator.type !== "separator") return;
16638
16778
  const at = rest === null ? null : findSeparator(rest, separator.name);
16639
16779
  args[separator.name] = at !== null;
16640
16780
  const before = at ? rest.slice(0, at[0]) : rest;
16641
- readGroup(parameters.slice(groupStart, i), before, args);
16781
+ readGroup(parameters.slice(groupStart, i), before, args, {
16782
+ at: restAt,
16783
+ spans
16784
+ });
16642
16785
  rest = at ? rest.slice(at[1]) : null;
16786
+ if (at) restAt += at[1];
16643
16787
  groupStart = i + 1;
16644
16788
  });
16645
- readGroup(parameters.slice(groupStart), rest, args);
16789
+ readGroup(parameters.slice(groupStart), rest, args, {
16790
+ at: restAt,
16791
+ spans
16792
+ });
16646
16793
  return args;
16647
16794
  }
16648
16795
  /**
16796
+ * Read a `passage` argument as written (`{goto "Hall"}`, `{goto $room}`): a
16797
+ * string literal is the name JavaScript reads from it (`"\u0048all"` is
16798
+ * `Hall`), anything else (a literal that is not well-formed too) an
16799
+ * expression, whose value is the name when it
16800
+ * runs (see evaluatePassageName).
16801
+ */
16802
+ function passageTarget(arg) {
16803
+ const expression = arg.trim();
16804
+ const name = stringLiteralValue(expression);
16805
+ return name === null ? {
16806
+ kind: "expression",
16807
+ expression
16808
+ } : {
16809
+ kind: "name",
16810
+ name
16811
+ };
16812
+ }
16813
+ /**
16649
16814
  * The passage a `passage` argument names (`{goto "Hall"}`, `{goto $room}`):
16650
16815
  * the value of its expression. Throws what evaluating it throws, and if no
16651
16816
  * passage of the story `state` holds has that name, an error naming it and
@@ -16685,10 +16850,15 @@ function parseVarArgs(rawArgs) {
16685
16850
  var OPTION_PARAMETERS = [{
16686
16851
  name: "value",
16687
16852
  type: "string",
16853
+ holds: "text",
16688
16854
  required: true
16689
16855
  }];
16690
16856
  /** The parameters of the built-in sub-macros that take arguments. */
16691
- var SUB_MACRO_PARAMETERS = { option: OPTION_PARAMETERS };
16857
+ var SUB_MACRO_PARAMETERS = new Map([["option", OPTION_PARAMETERS]]);
16858
+ /** The parameters of the built-in sub-macro `name`, if it takes any. */
16859
+ function subMacroParameters(name) {
16860
+ return SUB_MACRO_PARAMETERS.get(name.toLowerCase());
16861
+ }
16692
16862
  /**
16693
16863
  * Walk AST children to find {option} macro nodes, returning their values.
16694
16864
  */
@@ -17155,21 +17325,63 @@ var CONDITION_MACROS = new Set([
17155
17325
  "elseif",
17156
17326
  "case"
17157
17327
  ]);
17158
- /** Code inside the quoted strings of a macro's arguments, by parameter. */
17159
- var CODE_IN_STRINGS = { watch: {
17160
- condition: "expression",
17161
- run: "statements"
17162
- } };
17163
- /** The `string` parameters of a macro that name a passage. */
17164
- var PASSAGE_STRINGS = { watch: ["goto", "dialog"] };
17165
17328
  /** Block macros whose body is the name of a passage. */
17166
17329
  var PASSAGE_BODIES = new Set(["dialog"]);
17330
+ /**
17331
+ * `params` with each `string` and `text` parameter (options too) saying
17332
+ * what it holds: what it declares, else `holds`.
17333
+ */
17334
+ function withHolds(params, holds) {
17335
+ return params.map((param) => {
17336
+ if (param.parameters) return {
17337
+ ...param,
17338
+ parameters: withHolds(param.parameters, holds)
17339
+ };
17340
+ return (param.type === "string" || param.type === "text") && !param.holds ? {
17341
+ ...param,
17342
+ holds
17343
+ } : param;
17344
+ });
17345
+ }
17346
+ /**
17347
+ * The declared parameters of `macros`, by name in any case, and of the
17348
+ * built-in sub-macros (`{option}`): what the story-start check, reference
17349
+ * collection and tooling read arguments by. Each `string` and `text`
17350
+ * parameter says what it holds: what it declares, else `markup` for a macro
17351
+ * with `interpolate` (which can resolve markup) and `text` for any other.
17352
+ */
17353
+ function parameterLookup(macros) {
17354
+ const parameters = new NameMap();
17355
+ for (const { name, parameters: params, interpolate } of macros) if (params) parameters.set(name, withHolds(params, holdsByDefault(interpolate)));
17356
+ return (name) => {
17357
+ let params = parameters.get(name);
17358
+ if (!params) {
17359
+ const sub = subMacroParameters(name);
17360
+ if (sub) parameters.set(name, params = withHolds(sub, "text"));
17361
+ }
17362
+ return params;
17363
+ };
17364
+ }
17365
+ /** What a `string` or `text` argument holds when its parameter doesn't say. */
17366
+ var holdsByDefault = (interpolate) => interpolate ? "markup" : "text";
17367
+ /** Whether a parameter in `params` (options too) holds code in a string. */
17368
+ function holdsCode(params) {
17369
+ return params.some((p) => p.holds === "expression" || p.holds === "statements" || !!p.parameters && holdsCode(p.parameters));
17370
+ }
17167
17371
  /** The goal of the code an argument of this type holds, if it is code. */
17168
17372
  function codeGoal(param) {
17169
17373
  if (param.type === "expression" || param.type === "passage") return "expression";
17170
17374
  if (param.type === "statements") return "statements";
17171
17375
  }
17172
17376
  /**
17377
+ * What a `string` or `text` argument holds: what its parameter says (see
17378
+ * parameterLookup), else markup, which is checked.
17379
+ */
17380
+ function stringHolds(param) {
17381
+ if (param.type !== "string" && param.type !== "text") return void 0;
17382
+ return param.holds ?? "markup";
17383
+ }
17384
+ /**
17173
17385
  * The code and the markup-holding text in `tokens`, the tokens of `src`.
17174
17386
  * `parametersOf` gives the declared parameters of a macro, if it has any.
17175
17387
  */
@@ -17181,7 +17393,9 @@ function* codeAndText(src, tokens, parametersOf) {
17181
17393
  if (token.type === "link") yield {
17182
17394
  kind: "passage",
17183
17395
  name: token.target,
17184
- offset: locate(src, token.target, token.start),
17396
+ offset: token.targetStart,
17397
+ length: token.targetEnd - token.targetStart,
17398
+ macro: "link",
17185
17399
  label: src.slice(token.start, token.end)
17186
17400
  };
17187
17401
  else if (token.type === "expression") yield {
@@ -17191,32 +17405,35 @@ function* codeAndText(src, tokens, parametersOf) {
17191
17405
  goal: "expression",
17192
17406
  label: `{${token.expression}}`
17193
17407
  };
17194
- else if (token.type === "html") for (const [name, value] of Object.entries(token.attributes)) {
17195
- const at = locate(src, value, token.start);
17196
- if (!isCodeAttribute(name)) {
17197
- yield {
17198
- kind: "text",
17199
- text: value,
17200
- offset: at,
17201
- where: `In the ${name} attribute of <${token.tag}>: `
17202
- };
17203
- continue;
17204
- }
17205
- let from = 0;
17206
- for (const part of splitSigilTemplate(value)) {
17207
- if (!("expr" in part)) continue;
17208
- const k = locate(value, part.expr, from);
17209
- from = k + part.expr.length;
17210
- yield {
17211
- kind: "code",
17212
- code: part.expr,
17213
- offset: at + k,
17214
- goal: "expression",
17215
- label: `{${part.expr}} in the ${name} attribute of <${token.tag}>`
17216
- };
17408
+ else if (token.type === "html") {
17409
+ let cursor = token.start;
17410
+ for (const [name, value] of Object.entries(token.attributes)) {
17411
+ const at = locate(src, value, cursor);
17412
+ cursor = at + value.length;
17413
+ if (!isCodeAttribute(name)) {
17414
+ yield {
17415
+ kind: "text",
17416
+ text: value,
17417
+ offset: at,
17418
+ where: `In the ${name} attribute of <${token.tag}>: `
17419
+ };
17420
+ continue;
17421
+ }
17422
+ let from = 0;
17423
+ for (const part of splitSigilTemplate(value)) {
17424
+ if (!("expr" in part)) continue;
17425
+ const k = locate(value, part.expr, from);
17426
+ from = k + part.expr.length;
17427
+ yield {
17428
+ kind: "code",
17429
+ code: part.expr,
17430
+ offset: at + k,
17431
+ goal: "expression",
17432
+ label: `{${part.expr}} in the ${name} attribute of <${token.tag}>`
17433
+ };
17434
+ }
17217
17435
  }
17218
- }
17219
- else if (token.type === "macro" && !token.isClose) {
17436
+ } else if (token.type === "macro" && !token.isClose) {
17220
17437
  const name = token.name.toLowerCase();
17221
17438
  if (name === "do") {
17222
17439
  const close = closeOf(t, name);
@@ -17236,18 +17453,22 @@ function* codeAndText(src, tokens, parametersOf) {
17236
17453
  const close = closeOf(t, name);
17237
17454
  const body = tokens.slice(t + 1, close);
17238
17455
  if (close > t + 1 && body.every((b) => b.type === "text")) {
17239
- const passage = src.slice(token.end, tokens[close].start).trim().replace(/^["']|["']$/g, "");
17456
+ const text = src.slice(token.end, tokens[close].start);
17457
+ const written = text.trim();
17240
17458
  yield {
17241
17459
  kind: "passage",
17242
- name: passage,
17243
- offset: locate(src, passage, token.end),
17460
+ name: written.replace(/^["']|["']$/g, ""),
17461
+ offset: token.end + text.indexOf(written),
17462
+ length: written.length,
17463
+ macro: name,
17244
17464
  label: src.slice(token.start, token.end)
17245
17465
  };
17246
17466
  }
17247
17467
  }
17248
17468
  if (!args) continue;
17249
17469
  const label = `{${token.name} ${args}}`;
17250
- const argsAt = locate(src, args, token.start + 1);
17470
+ const tag = src.slice(token.start, token.end - 1);
17471
+ const argsAt = token.start + tag.trimEnd().length - args.length;
17251
17472
  if (CONDITION_MACROS.has(name)) {
17252
17473
  yield {
17253
17474
  kind: "code",
@@ -17266,8 +17487,9 @@ function* codeAndText(src, tokens, parametersOf) {
17266
17487
  /** The code and text in the arguments `args` (at `offset`) of `macro`. */
17267
17488
  function* argPieces(args, offset, params, macro, label) {
17268
17489
  let values;
17490
+ const spans = /* @__PURE__ */ new Map();
17269
17491
  try {
17270
- values = parseMacroArgs(args, params);
17492
+ values = parseMacroArgs(args, params, spans);
17271
17493
  } catch (error) {
17272
17494
  if (!(error instanceof MacroArgumentError)) throw error;
17273
17495
  yield {
@@ -17278,9 +17500,7 @@ function* argPieces(args, offset, params, macro, label) {
17278
17500
  };
17279
17501
  return;
17280
17502
  }
17281
- const inStrings = CODE_IN_STRINGS[macro.toLowerCase()] ?? {};
17282
- const passageStrings = PASSAGE_STRINGS[macro.toLowerCase()] ?? [];
17283
- let cursor = 0;
17503
+ const name = macro.toLowerCase();
17284
17504
  /** The pieces of the parameters `list`, with their values in `from`. */
17285
17505
  function* visit(list, from) {
17286
17506
  for (const param of list) {
@@ -17289,32 +17509,57 @@ function* argPieces(args, offset, params, macro, label) {
17289
17509
  yield* visit(param.parameters, value ?? {});
17290
17510
  continue;
17291
17511
  }
17292
- if (typeof value !== "string" || !value.trim()) continue;
17293
- const at = locate(args, value, cursor);
17294
- if (at >= cursor) cursor = at + value.length;
17295
- const goal = codeGoal(param) ?? inStrings[param.name];
17512
+ const span = spans.get(param);
17513
+ if (typeof value !== "string" || !value.trim() || !span) continue;
17514
+ const [start, end] = span;
17515
+ const written = args.slice(start, end);
17516
+ /** The name as written (quotes included), at the argument. */
17517
+ const passage = (passageName) => ({
17518
+ kind: "passage",
17519
+ name: passageName,
17520
+ offset: offset + start,
17521
+ length: end - start,
17522
+ macro: name,
17523
+ label
17524
+ });
17525
+ const goal = codeGoal(param);
17526
+ if (param.type === "passage") {
17527
+ const target = passageTarget(value);
17528
+ if (target.kind === "name") {
17529
+ yield passage(target.name);
17530
+ continue;
17531
+ }
17532
+ }
17296
17533
  if (goal) {
17297
17534
  const piece = {
17298
17535
  kind: "code",
17299
17536
  code: value,
17300
- offset: offset + at,
17537
+ offset: offset + start,
17301
17538
  goal,
17302
17539
  label
17303
17540
  };
17304
- if (param.type === "passage") piece.passage = true;
17541
+ if (param.type === "passage") {
17542
+ piece.passage = true;
17543
+ piece.macro = name;
17544
+ }
17305
17545
  yield piece;
17546
+ continue;
17306
17547
  }
17307
- const name = param.type === "passage" ? readWholeQuoted(value.trim()) : passageStrings.includes(param.name) ? value : null;
17308
- if (name !== null) yield {
17309
- kind: "passage",
17310
- name,
17311
- offset: offset + at,
17312
- label
17548
+ const at = offset + start + (value !== written && /^["']/.test(written) ? 1 : 0);
17549
+ const holds = stringHolds(param);
17550
+ if (holds === "expression" || holds === "statements") yield {
17551
+ kind: "code",
17552
+ code: value,
17553
+ offset: at,
17554
+ goal: holds,
17555
+ label,
17556
+ inString: true
17313
17557
  };
17314
- if (!goal && (param.type === "text" || param.type === "string")) yield {
17558
+ else if (holds === "passage") yield passage(value);
17559
+ else if (holds === "markup") yield {
17315
17560
  kind: "text",
17316
17561
  text: value,
17317
- offset: offset + at,
17562
+ offset: at,
17318
17563
  where: `In the ${param.name} of {${macro}}: `
17319
17564
  };
17320
17565
  }
@@ -17332,56 +17577,277 @@ function locate(text, part, from) {
17332
17577
  return anywhere >= 0 ? anywhere : from;
17333
17578
  }
17334
17579
  //#endregion
17335
- //#region src/story-variables.ts
17580
+ //#region src/widgets/widget-def.ts
17336
17581
  /**
17337
- * The tokens of markup, or none if it is malformed: validateMarkup reports
17338
- * that, with its position.
17582
+ * Reading widget definitions without rendering anything: the `{widget}`
17583
+ * macro, startup and markup validation (also in tooling) share these.
17339
17584
  */
17340
- function tokensOf(text, textMode) {
17341
- try {
17342
- return tokenizeMarkup(text, { text: textMode });
17343
- } catch (err) {
17344
- if (err instanceof MarkupError) return [];
17345
- throw err;
17346
- }
17347
- }
17348
- function declarationRegex(sigil) {
17349
- const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17350
- return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
17585
+ /** A {widget} definition's name, then its `@` parameters. */
17586
+ var WIDGET_PARAMETERS = [{
17587
+ name: "name",
17588
+ type: "text",
17589
+ holds: "text",
17590
+ required: true
17591
+ }, {
17592
+ name: "parameters",
17593
+ type: "text",
17594
+ holds: "text"
17595
+ }];
17596
+ /**
17597
+ * The widget a {widget} definition's arguments declare: its name and its
17598
+ * parameters, the words after it that start with `@` (docs/widgets.md).
17599
+ * Other words, such as `$name`, are not parameters.
17600
+ */
17601
+ function widgetDef({ name = "", parameters = "" }) {
17602
+ return {
17603
+ name,
17604
+ params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
17605
+ };
17351
17606
  }
17352
- /** A `$name` reference with its dot path, at a `$` the lexer found. */
17353
- var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
17354
- /** Quoted first argument of an input macro naming a story variable. */
17355
- var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
17356
- var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
17357
- var VALID_VAR_TYPES = new Set([
17358
- "number",
17359
- "string",
17360
- "boolean"
17361
- ]);
17362
- /** Boxed sample values whose members a primitive of each type can access. */
17363
- var PRIMITIVE_SAMPLES = {
17364
- string: Object(""),
17365
- number: Object(0),
17366
- boolean: Object(false)
17367
- };
17368
- function inferSchema(value) {
17369
- if (Array.isArray(value)) return { type: "array" };
17370
- if (value !== null && typeof value === "object") {
17371
- const fields = /* @__PURE__ */ new Map();
17372
- for (const [key, val] of Object.entries(value)) fields.set(key, inferSchema(val));
17373
- return {
17374
- type: "object",
17375
- fields
17376
- };
17377
- }
17378
- const jsType = typeof value;
17379
- if (!VALID_VAR_TYPES.has(jsType)) throw new Error(`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`);
17380
- return { type: jsType };
17607
+ /** Read the arguments of a {widget} definition (see widgetDef). */
17608
+ function parseWidgetDef(rawArgs) {
17609
+ return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
17381
17610
  }
17611
+ var BLOCK_WIDGET = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
17382
17612
  /**
17383
- * Parse a StoryVariables or StoryTransients passage content into a schema map.
17384
- * Each line: `$varName = expression` (or `%varName = expression` for transients)
17613
+ * The names of the block widgets (those whose body renders `{@children}`)
17614
+ * that StoryInit and the passages tagged `widget` define. They must be
17615
+ * known as block macros before any passage is parsed, so that passages
17616
+ * invoking them nest their content whatever the passage order.
17617
+ */
17618
+ function blockWidgetNames(passages) {
17619
+ const names = [];
17620
+ for (const passage of passages) {
17621
+ if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
17622
+ for (const match of passage.content.matchAll(BLOCK_WIDGET)) if (/\{@children\}/.test(match[2])) names.push(match[1]);
17623
+ }
17624
+ return names;
17625
+ }
17626
+ //#endregion
17627
+ //#region src/markup/validate.ts
17628
+ /**
17629
+ * Markup validation: parse every passage when the story starts and report
17630
+ * malformed markup, unknown macros, syntax errors in the code it runs and
17631
+ * passage names that name no passage (see code-check.ts) with the passage,
17632
+ * line and column they are at, instead of failing when the passage renders.
17633
+ * Tooling runs it too (see src/tooling.ts).
17634
+ */
17635
+ /**
17636
+ * Passages that hold no markup: declarations, and the JavaScript function
17637
+ * body of SaveTitle.
17638
+ */
17639
+ var NOT_MARKUP = new Set([
17640
+ "StoryVariables",
17641
+ "StoryTransients",
17642
+ "SaveTitle"
17643
+ ]);
17644
+ var NOT_MARKUP_TAGS = ["script", "stylesheet"];
17645
+ /** Branch macros, which their parent macro renders. */
17646
+ var BRANCH_MACROS = new Set([
17647
+ "elseif",
17648
+ "else",
17649
+ "case",
17650
+ "default",
17651
+ "next"
17652
+ ]);
17653
+ /** A diagnostic as one line of text. */
17654
+ function formatDiagnostic(d) {
17655
+ const source = d.file ? ` (${d.file}${d.fileLine ? `:${d.fileLine}` : ""})` : "";
17656
+ return `Passage "${d.passage}", line ${d.line}, column ${d.column}${source}: ${d.message}`;
17657
+ }
17658
+ /** Edit distance, for "did you mean" suggestions. */
17659
+ function distance(a, b) {
17660
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
17661
+ for (let i = 1; i <= a.length; i++) {
17662
+ const row = [i];
17663
+ for (let j = 1; j <= b.length; j++) row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
17664
+ prev = row;
17665
+ }
17666
+ return prev[b.length];
17667
+ }
17668
+ /**
17669
+ * " Did you mean …?" with the known name closest to `name`, written by
17670
+ * `quote`, if one is close.
17671
+ */
17672
+ function didYouMean(name, names, quote) {
17673
+ const lower = name.toLowerCase();
17674
+ let best = "";
17675
+ let bestDistance = Math.max(2, Math.floor(lower.length / 3)) + 1;
17676
+ for (const candidate of names) {
17677
+ const d = distance(lower, candidate.toLowerCase());
17678
+ if (d < bestDistance) {
17679
+ best = candidate;
17680
+ bestDistance = d;
17681
+ }
17682
+ }
17683
+ return best ? ` Did you mean ${quote(best)}?` : "";
17684
+ }
17685
+ /** What a passage argument may be, after an error in one. */
17686
+ var PASSAGE_ARGUMENT_HINT = " (a passage name is a quoted string or an expression)";
17687
+ /** A passage argument that is one bare word (`{goto Kitchen}`). */
17688
+ var BARE_WORD_RE = /^[A-Za-z][\w$]*$/;
17689
+ /** The name of the widget a `{widget}` with these arguments defines. */
17690
+ function widgetName(rawArgs) {
17691
+ try {
17692
+ return parseWidgetDef(rawArgs).name.toLowerCase() || void 0;
17693
+ } catch {
17694
+ return;
17695
+ }
17696
+ }
17697
+ /**
17698
+ * Validate the markup of every passage. Passage names written out (links,
17699
+ * quoted `passage` arguments) must name one of `passages`.
17700
+ */
17701
+ function validateMarkup(passages, options) {
17702
+ const diagnostics = [];
17703
+ const hooks = options.isBlockMacro ? { isBlock: options.isBlockMacro } : void 0;
17704
+ const report = (passage, offset, message) => {
17705
+ const { line, column } = lineColumn(passage.content, offset);
17706
+ const d = {
17707
+ passage: passage.name,
17708
+ line,
17709
+ column,
17710
+ message
17711
+ };
17712
+ const file = passage.metadata?.["data-source-file"];
17713
+ if (file) {
17714
+ d.file = file;
17715
+ const header = Number(passage.metadata?.["data-source-line"]);
17716
+ if (header) d.fileLine = header + line;
17717
+ }
17718
+ diagnostics.push(d);
17719
+ };
17720
+ const parsed = [];
17721
+ const widgets = /* @__PURE__ */ new Set();
17722
+ const passageNames = /* @__PURE__ */ new Set();
17723
+ for (const passage of passages) {
17724
+ passageNames.add(passage.name);
17725
+ if (NOT_MARKUP.has(passage.name)) continue;
17726
+ if (passage.tags?.some((tag) => NOT_MARKUP_TAGS.includes(tag))) continue;
17727
+ const reported = !options.only || options.only(passage);
17728
+ try {
17729
+ const tokens = tokenizeMarkup(passage.content);
17730
+ for (const token of tokens) {
17731
+ if (token.type !== "macro" || token.isClose) continue;
17732
+ if (token.name.toLowerCase() !== "widget") continue;
17733
+ const name = widgetName(token.rawArgs);
17734
+ if (name) widgets.add(name);
17735
+ }
17736
+ if (!reported) continue;
17737
+ parseMarkup(passage.content, { hooks });
17738
+ parsed.push([passage, tokens]);
17739
+ } catch (err) {
17740
+ if (!(err instanceof MarkupError)) throw err;
17741
+ if (reported) report(passage, err.offset, err.reason);
17742
+ }
17743
+ }
17744
+ const names = [...options.macroNames ?? [], ...widgets];
17745
+ /** Report the unknown macros among `tokens`, which start at `base`. */
17746
+ const checkMacros = (passage, tokens, base, where) => {
17747
+ for (const token of tokens) {
17748
+ if (token.type !== "macro" || token.isClose) continue;
17749
+ const lower = token.name.toLowerCase();
17750
+ if (BRANCH_MACROS.has(lower) || widgets.has(lower) || options.isKnownMacro(lower)) continue;
17751
+ report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
17752
+ }
17753
+ };
17754
+ const parametersOf = options.parametersOf ?? (() => void 0);
17755
+ /**
17756
+ * Check `tokens`, the tokens of `src`, which starts at `base` in the
17757
+ * passage: their macros, the code in them and the markup in their text.
17758
+ */
17759
+ const checkTokens = (passage, src, tokens, base, where) => {
17760
+ checkMacros(passage, tokens, base, where);
17761
+ for (const piece of codeAndText(src, tokens, parametersOf)) {
17762
+ const at = base + piece.offset;
17763
+ if (piece.kind === "argument-error") {
17764
+ report(passage, at, `${where}${piece.message} in ${piece.label}`);
17765
+ continue;
17766
+ }
17767
+ if (piece.kind === "passage") {
17768
+ if (options.checkPassageNames !== false && !passageNames.has(piece.name)) {
17769
+ const hint = didYouMean(piece.name, passageNames, JSON.stringify);
17770
+ report(passage, at, `${where}No passage named ${JSON.stringify(piece.name)} in ${piece.label}.${hint}`);
17771
+ }
17772
+ continue;
17773
+ }
17774
+ if (piece.kind === "code") {
17775
+ const result = parseOrError(piece.code, piece.goal);
17776
+ const code = piece.code.trim();
17777
+ if (result instanceof CodeSyntaxError) {
17778
+ const reason = result.reasonIn(passage.content, at);
17779
+ report(passage, at + result.pos, `${where}${reason} in ${piece.label}` + (piece.passage ? PASSAGE_ARGUMENT_HINT : ""));
17780
+ } else if (piece.passage && BARE_WORD_RE.test(code)) report(passage, at, `${where}Unquoted passage name in ${piece.label}: write ${JSON.stringify(code)}${PASSAGE_ARGUMENT_HINT}`);
17781
+ continue;
17782
+ }
17783
+ if (!piece.text.includes("{")) continue;
17784
+ try {
17785
+ parseMarkup(piece.text, {
17786
+ text: true,
17787
+ hooks
17788
+ });
17789
+ const inner = tokenizeMarkup(piece.text, { text: true });
17790
+ checkTokens(passage, piece.text, inner, at, piece.where);
17791
+ } catch (err) {
17792
+ if (!(err instanceof MarkupError)) throw err;
17793
+ report(passage, at + err.offset, piece.where + err.reason);
17794
+ }
17795
+ }
17796
+ };
17797
+ for (const [passage, tokens] of parsed) withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ""));
17798
+ return diagnostics;
17799
+ }
17800
+ //#endregion
17801
+ //#region src/story-variables.ts
17802
+ /**
17803
+ * The tokens of markup, or none if it is malformed: validateMarkup reports
17804
+ * that, with its position.
17805
+ */
17806
+ function tokensOf(text, textMode) {
17807
+ try {
17808
+ return tokenizeMarkup(text, { text: textMode });
17809
+ } catch (err) {
17810
+ if (err instanceof MarkupError) return [];
17811
+ throw err;
17812
+ }
17813
+ }
17814
+ function declarationRegex(sigil) {
17815
+ const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17816
+ return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
17817
+ }
17818
+ /** A `$name` reference with its dot path, at a `$` the lexer found. */
17819
+ var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
17820
+ /** Quoted first argument of an input macro naming a story variable. */
17821
+ var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
17822
+ var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
17823
+ var VALID_VAR_TYPES = new Set([
17824
+ "number",
17825
+ "string",
17826
+ "boolean"
17827
+ ]);
17828
+ /** Boxed sample values whose members a primitive of each type can access. */
17829
+ var PRIMITIVE_SAMPLES = {
17830
+ string: Object(""),
17831
+ number: Object(0),
17832
+ boolean: Object(false)
17833
+ };
17834
+ function inferSchema(value) {
17835
+ if (Array.isArray(value)) return { type: "array" };
17836
+ if (value !== null && typeof value === "object") {
17837
+ const fields = /* @__PURE__ */ new Map();
17838
+ for (const [key, val] of Object.entries(value)) fields.set(key, inferSchema(val));
17839
+ return {
17840
+ type: "object",
17841
+ fields
17842
+ };
17843
+ }
17844
+ const jsType = typeof value;
17845
+ if (!VALID_VAR_TYPES.has(jsType)) throw new Error(`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`);
17846
+ return { type: jsType };
17847
+ }
17848
+ /**
17849
+ * Parse a StoryVariables or StoryTransients passage content into a schema map.
17850
+ * Each line: `$varName = expression` (or `%varName = expression` for transients)
17385
17851
  */
17386
17852
  function parseStoryVariables(content, sigil = "$") {
17387
17853
  const schema = /* @__PURE__ */ new Map();
@@ -17547,11 +18013,21 @@ function scanCodeLeniently(code, onRef, goal) {
17547
18013
  * code), quoted variable names bound by input macros, and `{$…}`
17548
18014
  * interpolations in HTML attributes. Prose is literal text and not scanned.
17549
18015
  */
17550
- function collectPassageRefs(content, storeVarMacros, onRef) {
17551
- withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef));
18016
+ function collectPassageRefs(content, storeVarMacros, onRef, parametersOf) {
18017
+ withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef, parametersOf));
18018
+ }
18019
+ /**
18020
+ * Report the references in the code that a macro's quoted arguments hold
18021
+ * (see ParameterDef.holds), the condition and `run` action of `{watch}`:
18022
+ * the argument scan sees only the strings. Only the macros that declare
18023
+ * code in a string have their arguments read.
18024
+ */
18025
+ function scanStringCode(token, params, onRef) {
18026
+ if (!params || !token.rawArgs || !holdsCode(params)) return;
18027
+ for (const piece of argPieces(token.rawArgs, 0, params, token.name, "")) if (piece.kind === "code" && piece.inString) scanCode(piece.code, onRef, piece.goal);
17552
18028
  }
17553
18029
  /** Report the `$var` references in the tokens of `content`. */
17554
- function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
18030
+ function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf = () => void 0) {
17555
18031
  for (let t = 0; t < tokens.length; t++) {
17556
18032
  const token = tokens[t];
17557
18033
  if (token.type === "variable") {
@@ -17561,6 +18037,7 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
17561
18037
  else scanInterpolations(value, onRef, storeVarMacros);
17562
18038
  else if (token.type === "macro" && !token.isClose) {
17563
18039
  scanCode(token.rawArgs, onRef);
18040
+ scanStringCode(token, parametersOf(token.name), onRef);
17564
18041
  if (storeVarMacros.has(token.name.toLowerCase())) {
17565
18042
  const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
17566
18043
  const quoted = QUOTED_VAR_ARG_RE.exec(first);
@@ -17589,16 +18066,16 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
17589
18066
  * `storeVarMacros` lists the input macros whose first argument names a bound
17590
18067
  * variable; it defaults to the built-in ones.
17591
18068
  */
17592
- function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_MACROS) {
18069
+ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_MACROS, parametersOf = getMacroParameters) {
17593
18070
  const errors = [];
17594
18071
  const storeVarSet = new Set(Array.from(storeVarMacros, (m) => m.toLowerCase()));
17595
18072
  for (const [name, passage] of passages) {
17596
- if (name === "StoryVariables" || name === "StoryTransients") continue;
18073
+ if (NOT_MARKUP.has(name)) continue;
17597
18074
  const forLocals = extractForLocals(passage.content);
17598
18075
  collectPassageRefs(passage.content, storeVarSet, (ref) => {
17599
18076
  const error = validateRef(ref, schema, forLocals);
17600
18077
  if (error) errors.push(`Passage "${name}": ${error}`);
17601
- });
18078
+ }, parametersOf);
17602
18079
  }
17603
18080
  return errors;
17604
18081
  }
@@ -17612,218 +18089,6 @@ function extractDefaults(schema) {
17612
18089
  return defaults;
17613
18090
  }
17614
18091
  //#endregion
17615
- //#region src/widgets/widget-def.ts
17616
- /**
17617
- * Reading widget definitions without rendering anything: the `{widget}`
17618
- * macro, startup and markup validation (also in tooling) share these.
17619
- */
17620
- /** A {widget} definition's name, then its `@` parameters. */
17621
- var WIDGET_PARAMETERS = [{
17622
- name: "name",
17623
- type: "text",
17624
- required: true
17625
- }, {
17626
- name: "parameters",
17627
- type: "text"
17628
- }];
17629
- /**
17630
- * The widget a {widget} definition's arguments declare: its name and its
17631
- * parameters, the words after it that start with `@` (docs/widgets.md).
17632
- * Other words, such as `$name`, are not parameters.
17633
- */
17634
- function widgetDef({ name = "", parameters = "" }) {
17635
- return {
17636
- name,
17637
- params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
17638
- };
17639
- }
17640
- /** Read the arguments of a {widget} definition (see widgetDef). */
17641
- function parseWidgetDef(rawArgs) {
17642
- return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
17643
- }
17644
- var BLOCK_WIDGET = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
17645
- /**
17646
- * The names of the block widgets (those whose body renders `{@children}`)
17647
- * that StoryInit and the passages tagged `widget` define. They must be
17648
- * known as block macros before any passage is parsed, so that passages
17649
- * invoking them nest their content whatever the passage order.
17650
- */
17651
- function blockWidgetNames(passages) {
17652
- const names = [];
17653
- for (const passage of passages) {
17654
- if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
17655
- for (const match of passage.content.matchAll(BLOCK_WIDGET)) if (/\{@children\}/.test(match[2])) names.push(match[1]);
17656
- }
17657
- return names;
17658
- }
17659
- //#endregion
17660
- //#region src/markup/validate.ts
17661
- /**
17662
- * Markup validation: parse every passage when the story starts and report
17663
- * malformed markup, unknown macros, syntax errors in the code it runs and
17664
- * passage names that name no passage (see code-check.ts) with the passage,
17665
- * line and column they are at, instead of failing when the passage renders.
17666
- * Tooling runs it too (see src/tooling.ts).
17667
- */
17668
- /** Passages that hold no markup. */
17669
- var NOT_MARKUP = new Set(["StoryVariables", "StoryTransients"]);
17670
- var NOT_MARKUP_TAGS = ["script", "stylesheet"];
17671
- /** Branch macros, which their parent macro renders. */
17672
- var BRANCH_MACROS = new Set([
17673
- "elseif",
17674
- "else",
17675
- "case",
17676
- "default",
17677
- "next"
17678
- ]);
17679
- /** A diagnostic as one line of text. */
17680
- function formatDiagnostic(d) {
17681
- const source = d.file ? ` (${d.file}${d.fileLine ? `:${d.fileLine}` : ""})` : "";
17682
- return `Passage "${d.passage}", line ${d.line}, column ${d.column}${source}: ${d.message}`;
17683
- }
17684
- /** Edit distance, for "did you mean" suggestions. */
17685
- function distance(a, b) {
17686
- let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
17687
- for (let i = 1; i <= a.length; i++) {
17688
- const row = [i];
17689
- for (let j = 1; j <= b.length; j++) row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
17690
- prev = row;
17691
- }
17692
- return prev[b.length];
17693
- }
17694
- /**
17695
- * " Did you mean …?" with the known name closest to `name`, written by
17696
- * `quote`, if one is close.
17697
- */
17698
- function didYouMean(name, names, quote) {
17699
- const lower = name.toLowerCase();
17700
- let best = "";
17701
- let bestDistance = Math.max(2, Math.floor(lower.length / 3)) + 1;
17702
- for (const candidate of names) {
17703
- const d = distance(lower, candidate.toLowerCase());
17704
- if (d < bestDistance) {
17705
- best = candidate;
17706
- bestDistance = d;
17707
- }
17708
- }
17709
- return best ? ` Did you mean ${quote(best)}?` : "";
17710
- }
17711
- /** What a passage argument may be, after an error in one. */
17712
- var PASSAGE_ARGUMENT_HINT = " (a passage name is a quoted string or an expression)";
17713
- /** A passage argument that is one bare word (`{goto Kitchen}`). */
17714
- var BARE_WORD_RE = /^[A-Za-z][\w$]*$/;
17715
- /** The name of the widget a `{widget}` with these arguments defines. */
17716
- function widgetName(rawArgs) {
17717
- try {
17718
- return parseWidgetDef(rawArgs).name.toLowerCase() || void 0;
17719
- } catch {
17720
- return;
17721
- }
17722
- }
17723
- /**
17724
- * Validate the markup of every passage. Passage names written out (links,
17725
- * quoted `passage` arguments) must name one of `passages`.
17726
- */
17727
- function validateMarkup(passages, options) {
17728
- const diagnostics = [];
17729
- const hooks = options.isBlockMacro ? { isBlock: options.isBlockMacro } : void 0;
17730
- const report = (passage, offset, message) => {
17731
- const { line, column } = lineColumn(passage.content, offset);
17732
- const d = {
17733
- passage: passage.name,
17734
- line,
17735
- column,
17736
- message
17737
- };
17738
- const file = passage.metadata?.["data-source-file"];
17739
- if (file) {
17740
- d.file = file;
17741
- const header = Number(passage.metadata?.["data-source-line"]);
17742
- if (header) d.fileLine = header + line;
17743
- }
17744
- diagnostics.push(d);
17745
- };
17746
- const parsed = [];
17747
- const widgets = /* @__PURE__ */ new Set();
17748
- const passageNames = /* @__PURE__ */ new Set();
17749
- for (const passage of passages) {
17750
- passageNames.add(passage.name);
17751
- if (NOT_MARKUP.has(passage.name)) continue;
17752
- if (passage.tags?.some((tag) => NOT_MARKUP_TAGS.includes(tag))) continue;
17753
- const reported = !options.only || options.only(passage);
17754
- try {
17755
- const tokens = tokenizeMarkup(passage.content);
17756
- for (const token of tokens) {
17757
- if (token.type !== "macro" || token.isClose) continue;
17758
- if (token.name.toLowerCase() !== "widget") continue;
17759
- const name = widgetName(token.rawArgs);
17760
- if (name) widgets.add(name);
17761
- }
17762
- if (!reported) continue;
17763
- parseMarkup(passage.content, { hooks });
17764
- parsed.push([passage, tokens]);
17765
- } catch (err) {
17766
- if (!(err instanceof MarkupError)) throw err;
17767
- if (reported) report(passage, err.offset, err.reason);
17768
- }
17769
- }
17770
- const names = [...options.macroNames ?? [], ...widgets];
17771
- /** Report the unknown macros among `tokens`, which start at `base`. */
17772
- const checkMacros = (passage, tokens, base, where) => {
17773
- for (const token of tokens) {
17774
- if (token.type !== "macro" || token.isClose) continue;
17775
- const lower = token.name.toLowerCase();
17776
- if (BRANCH_MACROS.has(lower) || widgets.has(lower) || options.isKnownMacro(lower)) continue;
17777
- report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
17778
- }
17779
- };
17780
- const parametersOf = (name) => options.parametersOf?.(name) ?? SUB_MACRO_PARAMETERS[name.toLowerCase()];
17781
- /**
17782
- * Check `tokens`, the tokens of `src`, which starts at `base` in the
17783
- * passage: their macros, the code in them and the markup in their text.
17784
- */
17785
- const checkTokens = (passage, src, tokens, base, where) => {
17786
- checkMacros(passage, tokens, base, where);
17787
- for (const piece of codeAndText(src, tokens, parametersOf)) {
17788
- const at = base + piece.offset;
17789
- if (piece.kind === "argument-error") {
17790
- report(passage, at, `${where}${piece.message} in ${piece.label}`);
17791
- continue;
17792
- }
17793
- if (piece.kind === "passage") {
17794
- if (options.checkPassageNames !== false && !passageNames.has(piece.name)) {
17795
- const hint = didYouMean(piece.name, passageNames, JSON.stringify);
17796
- report(passage, at, `${where}No passage named ${JSON.stringify(piece.name)} in ${piece.label}.${hint}`);
17797
- }
17798
- continue;
17799
- }
17800
- if (piece.kind === "code") {
17801
- const result = parseOrError(piece.code, piece.goal);
17802
- const code = piece.code.trim();
17803
- if (result instanceof CodeSyntaxError) {
17804
- const reason = result.reasonIn(passage.content, at);
17805
- report(passage, at + result.pos, `${where}${reason} in ${piece.label}` + (piece.passage ? PASSAGE_ARGUMENT_HINT : ""));
17806
- } else if (piece.passage && BARE_WORD_RE.test(code)) report(passage, at, `${where}Unquoted passage name in ${piece.label}: write ${JSON.stringify(code)}${PASSAGE_ARGUMENT_HINT}`);
17807
- continue;
17808
- }
17809
- if (!piece.text.includes("{")) continue;
17810
- try {
17811
- parseMarkup(piece.text, {
17812
- text: true,
17813
- hooks
17814
- });
17815
- const inner = tokenizeMarkup(piece.text, { text: true });
17816
- checkTokens(passage, piece.text, inner, at, piece.where);
17817
- } catch (err) {
17818
- if (!(err instanceof MarkupError)) throw err;
17819
- report(passage, at + err.offset, piece.where + err.reason);
17820
- }
17821
- }
17822
- };
17823
- for (const [passage, tokens] of parsed) withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ""));
17824
- return diagnostics;
17825
- }
17826
- //#endregion
17827
18092
  //#region src/widgets/ast-scanner.ts
17828
18093
  /**
17829
18094
  * Recursively scan an AST node array for a VariableNode
@@ -18065,7 +18330,8 @@ defineMacro({
18065
18330
  },
18066
18331
  {
18067
18332
  name: "label",
18068
- type: "string"
18333
+ type: "string",
18334
+ holds: "markup"
18069
18335
  }
18070
18336
  ],
18071
18337
  render(_props, ctx) {
@@ -18432,6 +18698,7 @@ defineMacro({
18432
18698
  parameters: [{
18433
18699
  name: "label",
18434
18700
  type: "text",
18701
+ holds: "markup",
18435
18702
  required: true
18436
18703
  }],
18437
18704
  render({ rawArgs, children = [] }, ctx) {
@@ -18463,6 +18730,7 @@ defineMacro({
18463
18730
  parameters: [{
18464
18731
  name: "text",
18465
18732
  type: "string",
18733
+ holds: "markup",
18466
18734
  required: true
18467
18735
  }, {
18468
18736
  name: "passage",
@@ -18479,6 +18747,7 @@ defineMacro({
18479
18747
  }
18480
18748
  const runBody = useDetachedBody();
18481
18749
  const perform = () => {
18750
+ if (failure) return;
18482
18751
  runBody(children);
18483
18752
  if (passage) useStoryStore.getState().navigate(passage);
18484
18753
  };
@@ -18492,6 +18761,7 @@ defineMacro({
18492
18761
  authorId: ctx.id,
18493
18762
  label: display,
18494
18763
  target: passage ?? void 0,
18764
+ disabled: failure ? true : void 0,
18495
18765
  perform
18496
18766
  });
18497
18767
  if (failure) return /* @__PURE__ */ jsx(MacroError, {
@@ -18790,6 +19060,7 @@ defineMacro({
18790
19060
  parameters: [{
18791
19061
  name: "label",
18792
19062
  type: "text",
19063
+ holds: "markup",
18793
19064
  required: true
18794
19065
  }, {
18795
19066
  name: "noclose",
@@ -19336,7 +19607,8 @@ function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? St
19336
19607
  storeVar: true,
19337
19608
  parameters: [VARIABLE_PARAMETER, {
19338
19609
  name: "placeholder",
19339
- type: "string"
19610
+ type: "string",
19611
+ holds: "text"
19340
19612
  }],
19341
19613
  render(_props, ctx) {
19342
19614
  const placeholder = ctx.args.placeholder ?? "";
@@ -19372,7 +19644,8 @@ defineMacro({
19372
19644
  storeVar: true,
19373
19645
  parameters: [VARIABLE_PARAMETER, {
19374
19646
  name: "label",
19375
- type: "text"
19647
+ type: "text",
19648
+ holds: "text"
19376
19649
  }],
19377
19650
  render(_props, ctx) {
19378
19651
  const label = ctx.args.label ?? "";
@@ -19403,11 +19676,13 @@ defineMacro({
19403
19676
  {
19404
19677
  name: "value",
19405
19678
  type: "text",
19679
+ holds: "text",
19406
19680
  required: true
19407
19681
  },
19408
19682
  {
19409
19683
  name: "label",
19410
- type: "text"
19684
+ type: "text",
19685
+ holds: "text"
19411
19686
  }
19412
19687
  ],
19413
19688
  render(_props, ctx) {
@@ -19827,6 +20102,7 @@ defineMacro({
19827
20102
  parameters: [{
19828
20103
  name: "condition",
19829
20104
  type: "string",
20105
+ holds: "expression",
19830
20106
  required: true
19831
20107
  }, {
19832
20108
  name: "options",
@@ -19834,19 +20110,23 @@ defineMacro({
19834
20110
  parameters: [
19835
20111
  {
19836
20112
  name: "goto",
19837
- type: "string"
20113
+ type: "string",
20114
+ holds: "passage"
19838
20115
  },
19839
20116
  {
19840
20117
  name: "dialog",
19841
- type: "string"
20118
+ type: "string",
20119
+ holds: "passage"
19842
20120
  },
19843
20121
  {
19844
20122
  name: "run",
19845
- type: "string"
20123
+ type: "string",
20124
+ holds: "statements"
19846
20125
  },
19847
20126
  {
19848
20127
  name: "name",
19849
- type: "string"
20128
+ type: "string",
20129
+ holds: "text"
19850
20130
  },
19851
20131
  {
19852
20132
  name: "priority",
@@ -19875,6 +20155,7 @@ defineMacro({
19875
20155
  parameters: [{
19876
20156
  name: "name",
19877
20157
  type: "text",
20158
+ holds: "text",
19878
20159
  required: true
19879
20160
  }],
19880
20161
  render(_props, ctx) {
@@ -19923,11 +20204,10 @@ function stopWithErrors(errors) {
19923
20204
  */
19924
20205
  function markupErrors(storyData, only) {
19925
20206
  const macros = getMacroRegistry();
19926
- const parameters = new Map(macros.map((m) => [m.name.toLowerCase(), m.parameters]));
19927
20207
  return validateMarkup(storyData.passages.values(), {
19928
20208
  isKnownMacro: (name) => !!getMacro(name) || isSubMacro(name) || !!getWidget(name),
19929
20209
  macroNames: macros.map((m) => m.name),
19930
- parametersOf: (name) => parameters.get(name),
20210
+ parametersOf: parameterLookup(macros),
19931
20211
  only
19932
20212
  }).map(formatDiagnostic);
19933
20213
  }