@rohal12/spindle 0.57.0 → 0.58.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",
@@ -98,11 +126,6 @@ function parseLink(inner) {
98
126
  function quoteArg(value) {
99
127
  return "\"" + value.replace(/[\\"]/g, "\\$&") + "\"";
100
128
  }
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
129
  function selectorsOf(parts) {
107
130
  const classes = [];
108
131
  let id = "";
@@ -322,7 +345,8 @@ function peg$parse(input, options) {
322
345
  const peg$source = options.grammarSource;
323
346
  const peg$startRuleFunctions = {
324
347
  Markup: peg$parseMarkup,
325
- Tokens: peg$parseTokens
348
+ Tokens: peg$parseTokens,
349
+ SelectorsPrefix: peg$parseSelectorsPrefix
326
350
  };
327
351
  let peg$startRuleFunction = peg$parseMarkup;
328
352
  const peg$c0 = "\\";
@@ -724,40 +748,46 @@ function peg$parse(input, options) {
724
748
  function peg$f53() {
725
749
  return NO_SELECTORS;
726
750
  }
727
- function peg$f54(prefix, name) {
751
+ function peg$f54(s, end) {
752
+ return {
753
+ ...s,
754
+ end
755
+ };
756
+ }
757
+ function peg$f55(prefix, name) {
728
758
  return [prefix, name];
729
759
  }
730
- function peg$f55(s, sigil, name) {
760
+ function peg$f56(s, sigil, name) {
731
761
  return withSelectors(lexeme("variable", location(), {
732
762
  name,
733
- scope: SIGIL_SCOPES$1[sigil]
763
+ scope: SIGIL_SCOPES[sigil]
734
764
  }), s);
735
765
  }
736
- function peg$f56(s, expression) {
766
+ function peg$f57(s, expression) {
737
767
  return withSelectors(lexeme("expression", location(), { expression }), s);
738
768
  }
739
- function peg$f57() {
769
+ function peg$f58() {
740
770
  fail("Unclosed " + LB + input.substr(location().end.offset, 1) + "…: no } ends it", location());
741
771
  }
742
- function peg$f58(start) {
772
+ function peg$f59(start) {
743
773
  return skipTo(hooks.closeBrace(input, start, start));
744
774
  }
745
- function peg$f59(start) {
775
+ function peg$f60(start) {
746
776
  return skipEnd - offset();
747
777
  }
748
- function peg$f60(start, code) {
778
+ function peg$f61(start, code) {
749
779
  return code;
750
780
  }
751
- function peg$f61(s, start, close, name, nameEnd) {
781
+ function peg$f62(s, start, close, name, nameEnd) {
752
782
  return skipTo(hooks.closeBrace(input, nameEnd, start));
753
783
  }
754
- function peg$f62(s, start, close, name, nameEnd) {
784
+ function peg$f63(s, start, close, name, nameEnd) {
755
785
  return skipEnd - offset();
756
786
  }
757
- function peg$f63(s, start, close, name, nameEnd, args) {
787
+ function peg$f64(s, start, close, name, nameEnd, args) {
758
788
  return args;
759
789
  }
760
- function peg$f64(s, start, close, name, nameEnd, tail) {
790
+ function peg$f65(s, start, close, name, nameEnd, tail) {
761
791
  const loc = location();
762
792
  const lower = name.toLowerCase();
763
793
  const tag = lexeme("macro", loc, {
@@ -778,19 +808,19 @@ function peg$parse(input, options) {
778
808
  }
779
809
  return tag;
780
810
  }
781
- function peg$f65(s) {
811
+ function peg$f66(s) {
782
812
  fail("A closing tag starts with a letter after " + LB + "/", location());
783
813
  }
784
- function peg$f66() {
814
+ function peg$f67() {
785
815
  return !textMode;
786
816
  }
787
- function peg$f67(tag) {
817
+ function peg$f68(tag) {
788
818
  return VOID_TAGS.has(tag.lower);
789
819
  }
790
- function peg$f68(tag) {
820
+ function peg$f69(tag) {
791
821
  return null;
792
822
  }
793
- function peg$f69(tag, list, slash) {
823
+ function peg$f70(tag, list, slash) {
794
824
  const lower = tag.toLowerCase();
795
825
  return lexeme("html", location(), {
796
826
  tag,
@@ -800,7 +830,7 @@ function peg$parse(input, options) {
800
830
  isSelfClose: !!slash || VOID_TAGS.has(lower)
801
831
  });
802
832
  }
803
- function peg$f70(tag, bad) {
833
+ function peg$f71(tag, bad) {
804
834
  const c = input.charAt(bad);
805
835
  if (!c) fail("Unclosed tag <" + tag + ": no > ends it (write &lt; for a < that starts no tag)", location());
806
836
  const at = {
@@ -818,7 +848,7 @@ function peg$parse(input, options) {
818
848
  };
819
849
  fail("Unexpected " + JSON.stringify(c) + " in the tag <" + tag + ">: expected an attribute, or > to end the tag", at);
820
850
  }
821
- function peg$f71(tag) {
851
+ function peg$f72(tag) {
822
852
  return lexeme("html", location(), {
823
853
  tag,
824
854
  lower: tag.toLowerCase(),
@@ -827,44 +857,44 @@ function peg$parse(input, options) {
827
857
  isSelfClose: false
828
858
  });
829
859
  }
830
- function peg$f72(name) {
860
+ function peg$f73(name) {
831
861
  attrCode = hooks.isCodeAttribute(name);
832
862
  return true;
833
863
  }
834
- function peg$f73(name, v) {
864
+ function peg$f74(name, v) {
835
865
  return v;
836
866
  }
837
- function peg$f74(name, value) {
867
+ function peg$f75(name, value) {
838
868
  return [name, value ?? ""];
839
869
  }
840
- function peg$f75(v) {
870
+ function peg$f76(v) {
841
871
  return v;
842
872
  }
843
- function peg$f76(v) {
873
+ function peg$f77(v) {
844
874
  return v;
845
875
  }
846
- function peg$f77(q) {
876
+ function peg$f78(q) {
847
877
  fail("Unclosed attribute value: the " + q + " here has no closing " + q, location());
848
878
  }
849
- function peg$f78() {
879
+ function peg$f79() {
850
880
  return !attrCode;
851
881
  }
852
- function peg$f79(bs) {
882
+ function peg$f80(bs) {
853
883
  return bs.length % 2 === 1;
854
884
  }
855
- function peg$f80() {
885
+ function peg$f81() {
856
886
  return !attrCode;
857
887
  }
858
- function peg$f81(start, nameEnd) {
888
+ function peg$f82(start, nameEnd) {
859
889
  return skipTo(hooks.closeBrace(input, nameEnd, start));
860
890
  }
861
- function peg$f82(start, nameEnd) {
891
+ function peg$f83(start, nameEnd) {
862
892
  return skipEnd - offset();
863
893
  }
864
- function peg$f83() {
894
+ function peg$f84() {
865
895
  return !attrCode;
866
896
  }
867
- function peg$f84() {
897
+ function peg$f85() {
868
898
  return attrCode;
869
899
  }
870
900
  let peg$currPos = options.peg$currPos | 0;
@@ -1953,6 +1983,38 @@ function peg$parse(input, options) {
1953
1983
  }
1954
1984
  return s0;
1955
1985
  }
1986
+ function peg$parseSelectorsPrefix() {
1987
+ let s0, s1, s2, s3, s4;
1988
+ s0 = peg$currPos;
1989
+ s1 = peg$parseSelectors();
1990
+ if (s1 !== peg$FAILED) {
1991
+ s2 = peg$parseHere();
1992
+ s3 = [];
1993
+ if (input.length > peg$currPos) {
1994
+ s4 = input.charAt(peg$currPos);
1995
+ peg$currPos++;
1996
+ } else {
1997
+ s4 = peg$FAILED;
1998
+ if (peg$silentFails === 0) peg$fail(peg$e0);
1999
+ }
2000
+ while (s4 !== peg$FAILED) {
2001
+ s3.push(s4);
2002
+ if (input.length > peg$currPos) {
2003
+ s4 = input.charAt(peg$currPos);
2004
+ peg$currPos++;
2005
+ } else {
2006
+ s4 = peg$FAILED;
2007
+ if (peg$silentFails === 0) peg$fail(peg$e0);
2008
+ }
2009
+ }
2010
+ peg$savedPos = s0;
2011
+ s0 = peg$f54(s1, s2);
2012
+ } else {
2013
+ peg$currPos = s0;
2014
+ s0 = peg$FAILED;
2015
+ }
2016
+ return s0;
2017
+ }
1956
2018
  function peg$parseSelectorPart() {
1957
2019
  let s0, s1, s2, s3, s4;
1958
2020
  s0 = peg$currPos;
@@ -1972,7 +2034,7 @@ function peg$parse(input, options) {
1972
2034
  }
1973
2035
  s2 = input.substring(s2, peg$currPos);
1974
2036
  peg$savedPos = s0;
1975
- s0 = peg$f54(s1, s2);
2037
+ s0 = peg$f55(s1, s2);
1976
2038
  } else {
1977
2039
  peg$currPos = s0;
1978
2040
  s0 = peg$FAILED;
@@ -2097,7 +2159,7 @@ function peg$parse(input, options) {
2097
2159
  }
2098
2160
  if (s5 !== peg$FAILED) {
2099
2161
  peg$savedPos = s0;
2100
- s0 = peg$f55(s2, s3, s4);
2162
+ s0 = peg$f56(s2, s3, s4);
2101
2163
  } else {
2102
2164
  peg$currPos = s0;
2103
2165
  s0 = peg$FAILED;
@@ -2151,7 +2213,7 @@ function peg$parse(input, options) {
2151
2213
  }
2152
2214
  if (s5 !== peg$FAILED) {
2153
2215
  peg$savedPos = s0;
2154
- s0 = peg$f56(s2, s4);
2216
+ s0 = peg$f57(s2, s4);
2155
2217
  } else {
2156
2218
  peg$currPos = s0;
2157
2219
  s0 = peg$FAILED;
@@ -2199,7 +2261,7 @@ function peg$parse(input, options) {
2199
2261
  } else s3 = peg$FAILED;
2200
2262
  if (s3 !== peg$FAILED) {
2201
2263
  peg$savedPos = s0;
2202
- s0 = peg$f57();
2264
+ s0 = peg$f58();
2203
2265
  } else {
2204
2266
  peg$currPos = s0;
2205
2267
  s0 = peg$FAILED;
@@ -2221,12 +2283,12 @@ function peg$parse(input, options) {
2221
2283
  s0 = peg$currPos;
2222
2284
  s1 = peg$parseHere();
2223
2285
  peg$savedPos = peg$currPos;
2224
- s2 = peg$f58(s1);
2286
+ s2 = peg$f59(s1);
2225
2287
  if (s2) s2 = void 0;
2226
2288
  else s2 = peg$FAILED;
2227
2289
  if (s2 !== peg$FAILED) {
2228
2290
  s3 = peg$currPos;
2229
- s4 = peg$f59(s1);
2291
+ s4 = peg$f60(s1);
2230
2292
  s5 = peg$currPos;
2231
2293
  s6 = [];
2232
2294
  if (s6.length >= (s4 | 0)) s7 = peg$FAILED;
@@ -2257,7 +2319,7 @@ function peg$parse(input, options) {
2257
2319
  else s3 = s4;
2258
2320
  if (s3 !== peg$FAILED) {
2259
2321
  peg$savedPos = s0;
2260
- s0 = peg$f60(s1, s3);
2322
+ s0 = peg$f61(s1, s3);
2261
2323
  } else {
2262
2324
  peg$currPos = s0;
2263
2325
  s0 = peg$FAILED;
@@ -2295,12 +2357,12 @@ function peg$parse(input, options) {
2295
2357
  s6 = peg$parseHere();
2296
2358
  s7 = peg$currPos;
2297
2359
  peg$savedPos = peg$currPos;
2298
- s8 = peg$f61(s2, s3, s4, s5, s6);
2360
+ s8 = peg$f62(s2, s3, s4, s5, s6);
2299
2361
  if (s8) s8 = void 0;
2300
2362
  else s8 = peg$FAILED;
2301
2363
  if (s8 !== peg$FAILED) {
2302
2364
  s9 = peg$currPos;
2303
- s10 = peg$f62(s2, s3, s4, s5, s6);
2365
+ s10 = peg$f63(s2, s3, s4, s5, s6);
2304
2366
  s11 = peg$currPos;
2305
2367
  s12 = [];
2306
2368
  if (s12.length >= (s10 | 0)) s13 = peg$FAILED;
@@ -2339,7 +2401,7 @@ function peg$parse(input, options) {
2339
2401
  }
2340
2402
  if (s10 !== peg$FAILED) {
2341
2403
  peg$savedPos = s7;
2342
- s7 = peg$f63(s2, s3, s4, s5, s6, s9);
2404
+ s7 = peg$f64(s2, s3, s4, s5, s6, s9);
2343
2405
  } else {
2344
2406
  peg$currPos = s7;
2345
2407
  s7 = peg$FAILED;
@@ -2354,7 +2416,7 @@ function peg$parse(input, options) {
2354
2416
  }
2355
2417
  if (s7 === peg$FAILED) s7 = null;
2356
2418
  peg$savedPos = s0;
2357
- s0 = peg$f64(s2, s3, s4, s5, s6, s7);
2419
+ s0 = peg$f65(s2, s3, s4, s5, s6, s7);
2358
2420
  } else {
2359
2421
  peg$currPos = s0;
2360
2422
  s0 = peg$FAILED;
@@ -2388,7 +2450,7 @@ function peg$parse(input, options) {
2388
2450
  }
2389
2451
  if (s3 !== peg$FAILED) {
2390
2452
  peg$savedPos = s0;
2391
- s0 = peg$f65(s2);
2453
+ s0 = peg$f66(s2);
2392
2454
  } else {
2393
2455
  peg$currPos = s0;
2394
2456
  s0 = peg$FAILED;
@@ -2445,19 +2507,19 @@ function peg$parse(input, options) {
2445
2507
  let s0, s1, s2, s3;
2446
2508
  s0 = peg$currPos;
2447
2509
  peg$savedPos = peg$currPos;
2448
- s1 = peg$f66();
2510
+ s1 = peg$f67();
2449
2511
  if (s1) s1 = void 0;
2450
2512
  else s1 = peg$FAILED;
2451
2513
  if (s1 !== peg$FAILED) {
2452
2514
  s2 = peg$parseHtmlClose();
2453
2515
  if (s2 !== peg$FAILED) {
2454
2516
  peg$savedPos = peg$currPos;
2455
- s3 = peg$f67(s2);
2517
+ s3 = peg$f68(s2);
2456
2518
  if (s3) s3 = void 0;
2457
2519
  else s3 = peg$FAILED;
2458
2520
  if (s3 !== peg$FAILED) {
2459
2521
  peg$savedPos = s0;
2460
- s0 = peg$f68(s2);
2522
+ s0 = peg$f69(s2);
2461
2523
  } else {
2462
2524
  peg$currPos = s0;
2463
2525
  s0 = peg$FAILED;
@@ -2509,7 +2571,7 @@ function peg$parse(input, options) {
2509
2571
  }
2510
2572
  if (s6 !== peg$FAILED) {
2511
2573
  peg$savedPos = s0;
2512
- s0 = peg$f69(s2, s3, s5);
2574
+ s0 = peg$f70(s2, s3, s5);
2513
2575
  } else {
2514
2576
  peg$currPos = s0;
2515
2577
  s0 = peg$FAILED;
@@ -2574,7 +2636,7 @@ function peg$parse(input, options) {
2574
2636
  s5 = peg$parse_();
2575
2637
  s6 = peg$parseHere();
2576
2638
  peg$savedPos = s0;
2577
- s0 = peg$f70(s2, s6);
2639
+ s0 = peg$f71(s2, s6);
2578
2640
  } else {
2579
2641
  peg$currPos = s0;
2580
2642
  s0 = peg$FAILED;
@@ -2613,7 +2675,7 @@ function peg$parse(input, options) {
2613
2675
  }
2614
2676
  if (s4 !== peg$FAILED) {
2615
2677
  peg$savedPos = s0;
2616
- s0 = peg$f71(s2);
2678
+ s0 = peg$f72(s2);
2617
2679
  } else {
2618
2680
  peg$currPos = s0;
2619
2681
  s0 = peg$FAILED;
@@ -2747,7 +2809,7 @@ function peg$parse(input, options) {
2747
2809
  else s2 = s3;
2748
2810
  if (s2 !== peg$FAILED) {
2749
2811
  peg$savedPos = peg$currPos;
2750
- s3 = peg$f72(s2);
2812
+ s3 = peg$f73(s2);
2751
2813
  if (s3) s3 = void 0;
2752
2814
  else s3 = peg$FAILED;
2753
2815
  if (s3 !== peg$FAILED) {
@@ -2765,7 +2827,7 @@ function peg$parse(input, options) {
2765
2827
  s8 = peg$parseAttrValue();
2766
2828
  if (s8 !== peg$FAILED) {
2767
2829
  peg$savedPos = s4;
2768
- s4 = peg$f73(s2, s8);
2830
+ s4 = peg$f74(s2, s8);
2769
2831
  } else {
2770
2832
  peg$currPos = s4;
2771
2833
  s4 = peg$FAILED;
@@ -2776,7 +2838,7 @@ function peg$parse(input, options) {
2776
2838
  }
2777
2839
  if (s4 === peg$FAILED) s4 = null;
2778
2840
  peg$savedPos = s0;
2779
- s0 = peg$f74(s2, s4);
2841
+ s0 = peg$f75(s2, s4);
2780
2842
  } else {
2781
2843
  peg$currPos = s0;
2782
2844
  s0 = peg$FAILED;
@@ -2831,7 +2893,7 @@ function peg$parse(input, options) {
2831
2893
  }
2832
2894
  if (s3 !== peg$FAILED) {
2833
2895
  peg$savedPos = s0;
2834
- s0 = peg$f75(s2);
2896
+ s0 = peg$f76(s2);
2835
2897
  } else {
2836
2898
  peg$currPos = s0;
2837
2899
  s0 = peg$FAILED;
@@ -2883,7 +2945,7 @@ function peg$parse(input, options) {
2883
2945
  }
2884
2946
  if (s3 !== peg$FAILED) {
2885
2947
  peg$savedPos = s0;
2886
- s0 = peg$f76(s2);
2948
+ s0 = peg$f77(s2);
2887
2949
  } else {
2888
2950
  peg$currPos = s0;
2889
2951
  s0 = peg$FAILED;
@@ -2902,7 +2964,7 @@ function peg$parse(input, options) {
2902
2964
  }
2903
2965
  if (s1 !== peg$FAILED) {
2904
2966
  peg$savedPos = s0;
2905
- s1 = peg$f77(s1);
2967
+ s1 = peg$f78(s1);
2906
2968
  }
2907
2969
  s0 = s1;
2908
2970
  if (s0 === peg$FAILED) {
@@ -2933,7 +2995,7 @@ function peg$parse(input, options) {
2933
2995
  let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13;
2934
2996
  s0 = peg$currPos;
2935
2997
  peg$savedPos = peg$currPos;
2936
- s1 = peg$f78();
2998
+ s1 = peg$f79();
2937
2999
  if (s1) s1 = void 0;
2938
3000
  else s1 = peg$FAILED;
2939
3001
  if (s1 !== peg$FAILED) {
@@ -2962,7 +3024,7 @@ function peg$parse(input, options) {
2962
3024
  if (s2 !== peg$FAILED) {
2963
3025
  s3 = peg$currPos;
2964
3026
  peg$savedPos = peg$currPos;
2965
- s4 = peg$f79(s2);
3027
+ s4 = peg$f80(s2);
2966
3028
  if (s4) s4 = void 0;
2967
3029
  else s4 = peg$FAILED;
2968
3030
  if (s4 !== peg$FAILED) {
@@ -3001,7 +3063,7 @@ function peg$parse(input, options) {
3001
3063
  if (s0 === peg$FAILED) {
3002
3064
  s0 = peg$currPos;
3003
3065
  peg$savedPos = peg$currPos;
3004
- s1 = peg$f80();
3066
+ s1 = peg$f81();
3005
3067
  if (s1) s1 = void 0;
3006
3068
  else s1 = peg$FAILED;
3007
3069
  if (s1 !== peg$FAILED) {
@@ -3028,12 +3090,12 @@ function peg$parse(input, options) {
3028
3090
  if (s6 !== peg$FAILED) {
3029
3091
  s7 = peg$parseHere();
3030
3092
  peg$savedPos = peg$currPos;
3031
- s8 = peg$f81(s4, s7);
3093
+ s8 = peg$f82(s4, s7);
3032
3094
  if (s8) s8 = void 0;
3033
3095
  else s8 = peg$FAILED;
3034
3096
  if (s8 !== peg$FAILED) {
3035
3097
  s9 = peg$currPos;
3036
- s10 = peg$f82(s4, s7);
3098
+ s10 = peg$f83(s4, s7);
3037
3099
  s11 = peg$currPos;
3038
3100
  s12 = [];
3039
3101
  if (s12.length >= (s10 | 0)) s13 = peg$FAILED;
@@ -3115,7 +3177,7 @@ function peg$parse(input, options) {
3115
3177
  if (s0 === peg$FAILED) {
3116
3178
  s0 = peg$currPos;
3117
3179
  peg$savedPos = peg$currPos;
3118
- s1 = peg$f83();
3180
+ s1 = peg$f84();
3119
3181
  if (s1) s1 = void 0;
3120
3182
  else s1 = peg$FAILED;
3121
3183
  if (s1 !== peg$FAILED) {
@@ -3206,7 +3268,7 @@ function peg$parse(input, options) {
3206
3268
  if (s0 === peg$FAILED) {
3207
3269
  s0 = peg$currPos;
3208
3270
  peg$savedPos = peg$currPos;
3209
- s1 = peg$f84();
3271
+ s1 = peg$f85();
3210
3272
  if (s1) s1 = void 0;
3211
3273
  else s1 = peg$FAILED;
3212
3274
  if (s1 !== peg$FAILED) {
@@ -3441,22 +3503,6 @@ function registerBlockMacro(name) {
3441
3503
  BLOCK_MACROS.add(name);
3442
3504
  }
3443
3505
  //#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
3506
  //#region node_modules/acorn/dist/acorn.mjs
3461
3507
  var astralIdentifierCodes = [
3462
3508
  509,
@@ -9876,9 +9922,14 @@ function createCounts(source, name) {
9876
9922
  return counts;
9877
9923
  }
9878
9924
  //#endregion
9879
- //#region src/expression.ts
9880
- var FN_CACHE_MAX = 500;
9881
- var fnCache = /* @__PURE__ */ new Map();
9925
+ //#region src/transform.ts
9926
+ /**
9927
+ * The sigil transform: the story's code with its variable references
9928
+ * (`$var`, `_var`, `@var`, `%var`) turned into the namespace lookups the
9929
+ * expression engine runs. Pure string-to-string, so tooling can use it
9930
+ * (see src/tooling.ts).
9931
+ */
9932
+ var TRANSFORM_CACHE_MAX = 500;
9882
9933
  var NAMESPACES$1 = {
9883
9934
  $: "variables",
9884
9935
  _: "temporary",
@@ -9898,8 +9949,7 @@ var IDENT_END_RE = /[\p{ID_Continue}$\u200c\u200d]$/u;
9898
9949
  * statements, as that decides whether a leading `{` opens an object literal
9899
9950
  * or a block. A reference to a variable named `__proto__` throws a
9900
9951
  * SyntaxError: no namespace can hold one (see utils/namespace.ts).
9901
- * Exported for tests.
9902
- */
9952
+ * */
9903
9953
  function transform(expr, goal = "expression") {
9904
9954
  const key = goal === "statements" ? "s" + expr : "e" + expr;
9905
9955
  const cached = transformCache.get(key);
@@ -9917,12 +9967,16 @@ function transform(expr, goal = "expression") {
9917
9967
  at = ref.end;
9918
9968
  }
9919
9969
  result += expr.slice(at);
9920
- if (transformCache.size >= FN_CACHE_MAX) transformCache.delete(transformCache.keys().next().value);
9970
+ if (transformCache.size >= TRANSFORM_CACHE_MAX) transformCache.delete(transformCache.keys().next().value);
9921
9971
  transformCache.set(key, result);
9922
9972
  return result;
9923
9973
  }
9924
9974
  /** Transformed code by goal and source. */
9925
9975
  var transformCache = /* @__PURE__ */ new Map();
9976
+ //#endregion
9977
+ //#region src/expression.ts
9978
+ var FN_CACHE_MAX = 500;
9979
+ var fnCache = /* @__PURE__ */ new Map();
9926
9980
  var preamble = "const {currentPassage,previousPassage,visited,hasVisited,hasVisitedAny,hasVisitedAll,rendered,hasRendered,hasRenderedAny,hasRenderedAll,random,randomInt}=__fns;";
9927
9981
  function getOrCompile(key, body) {
9928
9982
  const cached = fnCache.get(key);
@@ -10110,6 +10164,10 @@ var metadataRegistry = new NameMap();
10110
10164
  function registerMacroMetadata(name, metadata) {
10111
10165
  metadataRegistry.set(name, metadata);
10112
10166
  }
10167
+ /** The declared parameters of the macro `name`, if it declares any. */
10168
+ function getMacroParameters(name) {
10169
+ return metadataRegistry.get(name)?.parameters;
10170
+ }
10113
10171
  function getMacroRegistry() {
10114
10172
  return Array.from(metadataRegistry.values());
10115
10173
  }
@@ -10184,6 +10242,37 @@ function readWholeQuoted(src) {
10184
10242
  const quoted = readQuoted(src, 0);
10185
10243
  return quoted && quoted.end === src.length ? quoted.value : null;
10186
10244
  }
10245
+ var JS_SIMPLE_ESCAPES = {
10246
+ n: "\n",
10247
+ t: " ",
10248
+ r: "\r",
10249
+ b: "\b",
10250
+ f: "\f",
10251
+ v: "\v",
10252
+ "0": "\0"
10253
+ };
10254
+ /** Undo every escape of a JavaScript string literal's body. */
10255
+ function decodeJsEscapes(body) {
10256
+ return body.replace(/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\n\r\u2028\u2029])|([\s\S]))/g, (_, braced, unicode, hex, lineBreak, other) => {
10257
+ if (lineBreak !== void 0) return "";
10258
+ const code = braced ?? unicode ?? hex;
10259
+ if (code !== void 0) {
10260
+ const point = parseInt(code, 16);
10261
+ return point <= 1114111 ? String.fromCodePoint(point) : "";
10262
+ }
10263
+ return JS_SIMPLE_ESCAPES[other] ?? other;
10264
+ });
10265
+ }
10266
+ /**
10267
+ * The value of `src` when it is exactly one `"…"` or `'…'` string, read as
10268
+ * the JavaScript literal it is (`"\u0048all"` is `Hall`), otherwise `null`.
10269
+ * `readWholeQuoted` keeps other escapes as written, for labels and code.
10270
+ */
10271
+ function readWholeJsString(src) {
10272
+ const quoted = readQuoted(src, 0);
10273
+ if (!quoted || quoted.end !== src.length) return null;
10274
+ return decodeJsEscapes(src.slice(1, -1));
10275
+ }
10187
10276
  /**
10188
10277
  * Strip an optional quote from each end of an unquoted-or-loosely-quoted
10189
10278
  * argument (`"Red`, `Red'`), leaving at least one character. This is the
@@ -10563,7 +10652,7 @@ function interpolateCode(template, scope, decode) {
10563
10652
  for (const part of splitSigilTemplate(template)) if ("text" in part) text += decode ? decode(part.text) : part.text;
10564
10653
  else if ("verbatim" in part) text += part.verbatim;
10565
10654
  else if (/^[$_@%][\w.]+$/.test(part.expr)) {
10566
- const kind = SIGIL_SCOPES[part.expr[0]];
10655
+ const kind = SIGIL_SCOPES$1[part.expr[0]];
10567
10656
  try {
10568
10657
  text += display(resolveVariable(scope, kind, part.expr.slice(1)));
10569
10658
  } catch (error) {
@@ -16474,7 +16563,7 @@ function readText(src) {
16474
16563
  */
16475
16564
  var MacroArgumentError = class extends Error {
16476
16565
  constructor(param, src) {
16477
- super(`The ${param.name} must be a quoted string ("…" or '…'), not ${src}`);
16566
+ super(`The ${param.name} must be a quoted string ("…" or '…'), ${src ? `not ${src}` : "but it has no value"}`);
16478
16567
  this.name = "MacroArgumentError";
16479
16568
  }
16480
16569
  };
@@ -16499,6 +16588,7 @@ function unset(param) {
16499
16588
  }
16500
16589
  var WORD_RE = /\w+/y;
16501
16590
  var DIGITS_RE = /\d+/y;
16591
+ var WORD_OR_PUNCT_RE = /\S+/y;
16502
16592
  /** Match the sticky regex `re` at `pos`, returning the matched text. */
16503
16593
  function matchAt(re, src, pos) {
16504
16594
  re.lastIndex = pos;
@@ -16507,7 +16597,8 @@ function matchAt(re, src, pos) {
16507
16597
  /**
16508
16598
  * Read keyword options (`goto "X" priority 5 once`). A keyword takes the
16509
16599
  * quoted string or digit run after it as its value; keywords that aren't
16510
- * declared, and their values, are skipped.
16600
+ * declared, and their values, are skipped. A keyword of a `string` parameter
16601
+ * must be followed by a quoted string.
16511
16602
  */
16512
16603
  function readOptions(params, src) {
16513
16604
  const options = {};
@@ -16520,10 +16611,11 @@ function readOptions(params, src) {
16520
16611
  }
16521
16612
  i += key.length;
16522
16613
  let val;
16614
+ let quoted = null;
16523
16615
  let j = i;
16524
16616
  while (j < src.length && isWhitespace(src[j])) j++;
16525
16617
  if (j > i) {
16526
- const quoted = readQuoted(src, j);
16618
+ quoted = readQuoted(src, j);
16527
16619
  const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
16528
16620
  if (quoted) {
16529
16621
  val = quoted.value;
@@ -16534,6 +16626,7 @@ function readOptions(params, src) {
16534
16626
  }
16535
16627
  }
16536
16628
  const param = params.find((p) => p.name === key);
16629
+ if (param?.type === "string" && (!quoted || val === void 0)) throw new MacroArgumentError(param, matchAt(WORD_OR_PUNCT_RE, src, j) ?? "");
16537
16630
  if (param?.type === "flag") options[key] = true;
16538
16631
  else if (param?.type === "number") options[key] = Number(val ?? 0);
16539
16632
  else if (param) options[key] = val;
@@ -16646,6 +16739,23 @@ function parseMacroArgs(rawArgs, parameters) {
16646
16739
  return args;
16647
16740
  }
16648
16741
  /**
16742
+ * Read a `passage` argument as written (`{goto "Hall"}`, `{goto $room}`): a
16743
+ * quoted string is the name its JavaScript literal has (`"\u0048all"` is
16744
+ * `Hall`), anything else an expression, whose value is the name when it
16745
+ * runs (see evaluatePassageName).
16746
+ */
16747
+ function passageTarget(arg) {
16748
+ const expression = arg.trim();
16749
+ const name = readWholeJsString(expression);
16750
+ return name === null ? {
16751
+ kind: "expression",
16752
+ expression
16753
+ } : {
16754
+ kind: "name",
16755
+ name
16756
+ };
16757
+ }
16758
+ /**
16649
16759
  * The passage a `passage` argument names (`{goto "Hall"}`, `{goto $room}`):
16650
16760
  * the value of its expression. Throws what evaluating it throws, and if no
16651
16761
  * passage of the story `state` holds has that name, an error naming it and
@@ -16688,7 +16798,11 @@ var OPTION_PARAMETERS = [{
16688
16798
  required: true
16689
16799
  }];
16690
16800
  /** The parameters of the built-in sub-macros that take arguments. */
16691
- var SUB_MACRO_PARAMETERS = { option: OPTION_PARAMETERS };
16801
+ var SUB_MACRO_PARAMETERS = new Map([["option", OPTION_PARAMETERS]]);
16802
+ /** The parameters of the built-in sub-macro `name`, if it takes any. */
16803
+ function subMacroParameters(name) {
16804
+ return SUB_MACRO_PARAMETERS.get(name.toLowerCase());
16805
+ }
16692
16806
  /**
16693
16807
  * Walk AST children to find {option} macro nodes, returning their values.
16694
16808
  */
@@ -17156,12 +17270,21 @@ var CONDITION_MACROS = new Set([
17156
17270
  "case"
17157
17271
  ]);
17158
17272
  /** Code inside the quoted strings of a macro's arguments, by parameter. */
17159
- var CODE_IN_STRINGS = { watch: {
17273
+ var CODE_IN_STRINGS = new Map([["watch", {
17160
17274
  condition: "expression",
17161
17275
  run: "statements"
17162
- } };
17276
+ }]]);
17163
17277
  /** The `string` parameters of a macro that name a passage. */
17164
- var PASSAGE_STRINGS = { watch: ["goto", "dialog"] };
17278
+ var PASSAGE_STRINGS = new Map([["watch", ["goto", "dialog"]]]);
17279
+ /**
17280
+ * The `text` and `string` parameters of a macro that the runtime keeps as
17281
+ * written, not as markup: an option's value, the name of a watcher.
17282
+ */
17283
+ var LITERAL_STRINGS = new Map([
17284
+ ["option", ["value"]],
17285
+ ["watch", ["name"]],
17286
+ ["unwatch", ["name"]]
17287
+ ]);
17165
17288
  /** Block macros whose body is the name of a passage. */
17166
17289
  var PASSAGE_BODIES = new Set(["dialog"]);
17167
17290
  /** The goal of the code an argument of this type holds, if it is code. */
@@ -17182,6 +17305,8 @@ function* codeAndText(src, tokens, parametersOf) {
17182
17305
  kind: "passage",
17183
17306
  name: token.target,
17184
17307
  offset: locate(src, token.target, token.start),
17308
+ length: token.target.length,
17309
+ macro: "link",
17185
17310
  label: src.slice(token.start, token.end)
17186
17311
  };
17187
17312
  else if (token.type === "expression") yield {
@@ -17241,6 +17366,8 @@ function* codeAndText(src, tokens, parametersOf) {
17241
17366
  kind: "passage",
17242
17367
  name: passage,
17243
17368
  offset: locate(src, passage, token.end),
17369
+ length: passage.length,
17370
+ macro: name,
17244
17371
  label: src.slice(token.start, token.end)
17245
17372
  };
17246
17373
  }
@@ -17278,8 +17405,10 @@ function* argPieces(args, offset, params, macro, label) {
17278
17405
  };
17279
17406
  return;
17280
17407
  }
17281
- const inStrings = CODE_IN_STRINGS[macro.toLowerCase()] ?? {};
17282
- const passageStrings = PASSAGE_STRINGS[macro.toLowerCase()] ?? [];
17408
+ const key = macro.toLowerCase();
17409
+ const inStrings = CODE_IN_STRINGS.get(key) ?? {};
17410
+ const passageStrings = PASSAGE_STRINGS.get(key) ?? [];
17411
+ const literalStrings = LITERAL_STRINGS.get(key) ?? [];
17283
17412
  let cursor = 0;
17284
17413
  /** The pieces of the parameters `list`, with their values in `from`. */
17285
17414
  function* visit(list, from) {
@@ -17301,17 +17430,27 @@ function* argPieces(args, offset, params, macro, label) {
17301
17430
  goal,
17302
17431
  label
17303
17432
  };
17304
- if (param.type === "passage") piece.passage = true;
17433
+ if (param.type === "passage") {
17434
+ piece.passage = true;
17435
+ piece.macro = macro.toLowerCase();
17436
+ }
17437
+ if (!codeGoal(param)) piece.inString = true;
17305
17438
  yield piece;
17306
17439
  }
17307
- const name = param.type === "passage" ? readWholeQuoted(value.trim()) : passageStrings.includes(param.name) ? value : null;
17308
- if (name !== null) yield {
17440
+ const written = param.type === "passage" ? value.trim() : value;
17441
+ const target = param.type === "passage" ? passageTarget(value) : passageStrings.includes(param.name) ? {
17442
+ kind: "name",
17443
+ name: value
17444
+ } : null;
17445
+ if (target?.kind === "name") yield {
17309
17446
  kind: "passage",
17310
- name,
17311
- offset: offset + at,
17447
+ name: target.name,
17448
+ offset: offset + at + value.indexOf(written),
17449
+ length: written.length,
17450
+ macro: macro.toLowerCase(),
17312
17451
  label
17313
17452
  };
17314
- if (!goal && (param.type === "text" || param.type === "string")) yield {
17453
+ if (!goal && (param.type === "text" || param.type === "string") && !literalStrings.includes(param.name)) yield {
17315
17454
  kind: "text",
17316
17455
  text: value,
17317
17456
  offset: offset + at,
@@ -17332,116 +17471,335 @@ function locate(text, part, from) {
17332
17471
  return anywhere >= 0 ? anywhere : from;
17333
17472
  }
17334
17473
  //#endregion
17335
- //#region src/story-variables.ts
17474
+ //#region src/widgets/widget-def.ts
17336
17475
  /**
17337
- * The tokens of markup, or none if it is malformed: validateMarkup reports
17338
- * that, with its position.
17476
+ * Reading widget definitions without rendering anything: the `{widget}`
17477
+ * macro, startup and markup validation (also in tooling) share these.
17339
17478
  */
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*(.+)$`);
17479
+ /** A {widget} definition's name, then its `@` parameters. */
17480
+ var WIDGET_PARAMETERS = [{
17481
+ name: "name",
17482
+ type: "text",
17483
+ required: true
17484
+ }, {
17485
+ name: "parameters",
17486
+ type: "text"
17487
+ }];
17488
+ /**
17489
+ * The widget a {widget} definition's arguments declare: its name and its
17490
+ * parameters, the words after it that start with `@` (docs/widgets.md).
17491
+ * Other words, such as `$name`, are not parameters.
17492
+ */
17493
+ function widgetDef({ name = "", parameters = "" }) {
17494
+ return {
17495
+ name,
17496
+ params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
17497
+ };
17351
17498
  }
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 };
17499
+ /** Read the arguments of a {widget} definition (see widgetDef). */
17500
+ function parseWidgetDef(rawArgs) {
17501
+ return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
17381
17502
  }
17503
+ var BLOCK_WIDGET = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
17382
17504
  /**
17383
- * Parse a StoryVariables or StoryTransients passage content into a schema map.
17384
- * Each line: `$varName = expression` (or `%varName = expression` for transients)
17505
+ * The names of the block widgets (those whose body renders `{@children}`)
17506
+ * that StoryInit and the passages tagged `widget` define. They must be
17507
+ * known as block macros before any passage is parsed, so that passages
17508
+ * invoking them nest their content whatever the passage order.
17385
17509
  */
17386
- function parseStoryVariables(content, sigil = "$") {
17387
- const schema = /* @__PURE__ */ new Map();
17388
- const DECLARATION_RE = declarationRegex(sigil);
17389
- const passageName = sigil === "%" ? "StoryTransients" : "StoryVariables";
17390
- for (const rawLine of content.split("\n")) {
17391
- const line = rawLine.trim();
17392
- if (!line) continue;
17393
- const match = line.match(DECLARATION_RE);
17394
- if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
17395
- const [, name, expr] = match;
17396
- const nameError = variableNameError(name, sigil + name);
17397
- if (nameError) throw new Error(`${passageName}: ${nameError}`);
17398
- let value;
17399
- try {
17400
- value = new Function("return (" + expr + ")")();
17401
- } catch (err) {
17402
- throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${errorMessage(err)}`);
17403
- }
17404
- let fieldSchema;
17405
- try {
17406
- fieldSchema = inferSchema(value);
17407
- } catch (err) {
17408
- throw new Error(`${passageName}: ${errorMessage(err)}`);
17409
- }
17410
- schema.set(name, {
17411
- ...fieldSchema,
17412
- name,
17413
- default: value
17414
- });
17510
+ function blockWidgetNames(passages) {
17511
+ const names = [];
17512
+ for (const passage of passages) {
17513
+ if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
17514
+ for (const match of passage.content.matchAll(BLOCK_WIDGET)) if (/\{@children\}/.test(match[2])) names.push(match[1]);
17415
17515
  }
17416
- return schema;
17516
+ return names;
17417
17517
  }
17518
+ //#endregion
17519
+ //#region src/markup/validate.ts
17418
17520
  /**
17419
- * Extract for-loop local variable names from passage content.
17420
- * `{for @item of ...}` → "item"
17421
- * `{for @index, @item of ...}` → "index", "item"
17521
+ * Markup validation: parse every passage when the story starts and report
17522
+ * malformed markup, unknown macros, syntax errors in the code it runs and
17523
+ * passage names that name no passage (see code-check.ts) with the passage,
17524
+ * line and column they are at, instead of failing when the passage renders.
17525
+ * Tooling runs it too (see src/tooling.ts).
17422
17526
  */
17423
- function extractForLocals(content) {
17424
- const locals = /* @__PURE__ */ new Set();
17425
- let match;
17426
- FOR_LOCAL_RE.lastIndex = 0;
17427
- while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
17428
- locals.add(match[1]);
17429
- if (match[2]) locals.add(match[2]);
17430
- }
17431
- return locals;
17432
- }
17433
17527
  /**
17434
- * Validate a single variable reference path (e.g. "player.health") against
17435
- * the schema. Returns an error message or null if valid.
17528
+ * Passages that hold no markup: declarations, and the JavaScript function
17529
+ * body of SaveTitle.
17436
17530
  */
17437
- function validateRef(ref, schema, forLocals) {
17438
- const parts = ref.split(".");
17439
- const rootName = parts[0];
17440
- const nameError = variableNameError(rootName, "$" + rootName);
17441
- if (nameError) return nameError;
17442
- if (forLocals.has(rootName)) return null;
17443
- const rootSchema = schema.get(rootName);
17444
- if (!rootSchema) return `Undeclared variable: $${ref}`;
17531
+ var NOT_MARKUP = new Set([
17532
+ "StoryVariables",
17533
+ "StoryTransients",
17534
+ "SaveTitle"
17535
+ ]);
17536
+ var NOT_MARKUP_TAGS = ["script", "stylesheet"];
17537
+ /** Branch macros, which their parent macro renders. */
17538
+ var BRANCH_MACROS = new Set([
17539
+ "elseif",
17540
+ "else",
17541
+ "case",
17542
+ "default",
17543
+ "next"
17544
+ ]);
17545
+ /** A diagnostic as one line of text. */
17546
+ function formatDiagnostic(d) {
17547
+ const source = d.file ? ` (${d.file}${d.fileLine ? `:${d.fileLine}` : ""})` : "";
17548
+ return `Passage "${d.passage}", line ${d.line}, column ${d.column}${source}: ${d.message}`;
17549
+ }
17550
+ /** Edit distance, for "did you mean" suggestions. */
17551
+ function distance(a, b) {
17552
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
17553
+ for (let i = 1; i <= a.length; i++) {
17554
+ const row = [i];
17555
+ 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));
17556
+ prev = row;
17557
+ }
17558
+ return prev[b.length];
17559
+ }
17560
+ /**
17561
+ * " Did you mean …?" with the known name closest to `name`, written by
17562
+ * `quote`, if one is close.
17563
+ */
17564
+ function didYouMean(name, names, quote) {
17565
+ const lower = name.toLowerCase();
17566
+ let best = "";
17567
+ let bestDistance = Math.max(2, Math.floor(lower.length / 3)) + 1;
17568
+ for (const candidate of names) {
17569
+ const d = distance(lower, candidate.toLowerCase());
17570
+ if (d < bestDistance) {
17571
+ best = candidate;
17572
+ bestDistance = d;
17573
+ }
17574
+ }
17575
+ return best ? ` Did you mean ${quote(best)}?` : "";
17576
+ }
17577
+ /** What a passage argument may be, after an error in one. */
17578
+ var PASSAGE_ARGUMENT_HINT = " (a passage name is a quoted string or an expression)";
17579
+ /** A passage argument that is one bare word (`{goto Kitchen}`). */
17580
+ var BARE_WORD_RE = /^[A-Za-z][\w$]*$/;
17581
+ /** The name of the widget a `{widget}` with these arguments defines. */
17582
+ function widgetName(rawArgs) {
17583
+ try {
17584
+ return parseWidgetDef(rawArgs).name.toLowerCase() || void 0;
17585
+ } catch {
17586
+ return;
17587
+ }
17588
+ }
17589
+ /**
17590
+ * Validate the markup of every passage. Passage names written out (links,
17591
+ * quoted `passage` arguments) must name one of `passages`.
17592
+ */
17593
+ function validateMarkup(passages, options) {
17594
+ const diagnostics = [];
17595
+ const hooks = options.isBlockMacro ? { isBlock: options.isBlockMacro } : void 0;
17596
+ const report = (passage, offset, message) => {
17597
+ const { line, column } = lineColumn(passage.content, offset);
17598
+ const d = {
17599
+ passage: passage.name,
17600
+ line,
17601
+ column,
17602
+ message
17603
+ };
17604
+ const file = passage.metadata?.["data-source-file"];
17605
+ if (file) {
17606
+ d.file = file;
17607
+ const header = Number(passage.metadata?.["data-source-line"]);
17608
+ if (header) d.fileLine = header + line;
17609
+ }
17610
+ diagnostics.push(d);
17611
+ };
17612
+ const parsed = [];
17613
+ const widgets = /* @__PURE__ */ new Set();
17614
+ const passageNames = /* @__PURE__ */ new Set();
17615
+ for (const passage of passages) {
17616
+ passageNames.add(passage.name);
17617
+ if (NOT_MARKUP.has(passage.name)) continue;
17618
+ if (passage.tags?.some((tag) => NOT_MARKUP_TAGS.includes(tag))) continue;
17619
+ const reported = !options.only || options.only(passage);
17620
+ try {
17621
+ const tokens = tokenizeMarkup(passage.content);
17622
+ for (const token of tokens) {
17623
+ if (token.type !== "macro" || token.isClose) continue;
17624
+ if (token.name.toLowerCase() !== "widget") continue;
17625
+ const name = widgetName(token.rawArgs);
17626
+ if (name) widgets.add(name);
17627
+ }
17628
+ if (!reported) continue;
17629
+ parseMarkup(passage.content, { hooks });
17630
+ parsed.push([passage, tokens]);
17631
+ } catch (err) {
17632
+ if (!(err instanceof MarkupError)) throw err;
17633
+ if (reported) report(passage, err.offset, err.reason);
17634
+ }
17635
+ }
17636
+ const names = [...options.macroNames ?? [], ...widgets];
17637
+ /** Report the unknown macros among `tokens`, which start at `base`. */
17638
+ const checkMacros = (passage, tokens, base, where) => {
17639
+ for (const token of tokens) {
17640
+ if (token.type !== "macro" || token.isClose) continue;
17641
+ const lower = token.name.toLowerCase();
17642
+ if (BRANCH_MACROS.has(lower) || widgets.has(lower) || options.isKnownMacro(lower)) continue;
17643
+ report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
17644
+ }
17645
+ };
17646
+ const parametersOf = (name) => options.parametersOf?.(name) ?? subMacroParameters(name);
17647
+ /**
17648
+ * Check `tokens`, the tokens of `src`, which starts at `base` in the
17649
+ * passage: their macros, the code in them and the markup in their text.
17650
+ */
17651
+ const checkTokens = (passage, src, tokens, base, where) => {
17652
+ checkMacros(passage, tokens, base, where);
17653
+ for (const piece of codeAndText(src, tokens, parametersOf)) {
17654
+ const at = base + piece.offset;
17655
+ if (piece.kind === "argument-error") {
17656
+ report(passage, at, `${where}${piece.message} in ${piece.label}`);
17657
+ continue;
17658
+ }
17659
+ if (piece.kind === "passage") {
17660
+ if (options.checkPassageNames !== false && !passageNames.has(piece.name)) {
17661
+ const hint = didYouMean(piece.name, passageNames, JSON.stringify);
17662
+ report(passage, at, `${where}No passage named ${JSON.stringify(piece.name)} in ${piece.label}.${hint}`);
17663
+ }
17664
+ continue;
17665
+ }
17666
+ if (piece.kind === "code") {
17667
+ const result = parseOrError(piece.code, piece.goal);
17668
+ const code = piece.code.trim();
17669
+ if (result instanceof CodeSyntaxError) {
17670
+ const reason = result.reasonIn(passage.content, at);
17671
+ report(passage, at + result.pos, `${where}${reason} in ${piece.label}` + (piece.passage ? PASSAGE_ARGUMENT_HINT : ""));
17672
+ } 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}`);
17673
+ continue;
17674
+ }
17675
+ if (!piece.text.includes("{")) continue;
17676
+ try {
17677
+ parseMarkup(piece.text, {
17678
+ text: true,
17679
+ hooks
17680
+ });
17681
+ const inner = tokenizeMarkup(piece.text, { text: true });
17682
+ checkTokens(passage, piece.text, inner, at, piece.where);
17683
+ } catch (err) {
17684
+ if (!(err instanceof MarkupError)) throw err;
17685
+ report(passage, at + err.offset, piece.where + err.reason);
17686
+ }
17687
+ }
17688
+ };
17689
+ for (const [passage, tokens] of parsed) withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ""));
17690
+ return diagnostics;
17691
+ }
17692
+ //#endregion
17693
+ //#region src/story-variables.ts
17694
+ /**
17695
+ * The tokens of markup, or none if it is malformed: validateMarkup reports
17696
+ * that, with its position.
17697
+ */
17698
+ function tokensOf(text, textMode) {
17699
+ try {
17700
+ return tokenizeMarkup(text, { text: textMode });
17701
+ } catch (err) {
17702
+ if (err instanceof MarkupError) return [];
17703
+ throw err;
17704
+ }
17705
+ }
17706
+ function declarationRegex(sigil) {
17707
+ const escaped = sigil.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17708
+ return new RegExp(`^${escaped}(\\w+)\\s*=\\s*(.+)$`);
17709
+ }
17710
+ /** A `$name` reference with its dot path, at a `$` the lexer found. */
17711
+ var VAR_PATH_RE = /\$(\w+(?:\.\w+)*)/y;
17712
+ /** Quoted first argument of an input macro naming a story variable. */
17713
+ var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
17714
+ var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
17715
+ var VALID_VAR_TYPES = new Set([
17716
+ "number",
17717
+ "string",
17718
+ "boolean"
17719
+ ]);
17720
+ /** Boxed sample values whose members a primitive of each type can access. */
17721
+ var PRIMITIVE_SAMPLES = {
17722
+ string: Object(""),
17723
+ number: Object(0),
17724
+ boolean: Object(false)
17725
+ };
17726
+ function inferSchema(value) {
17727
+ if (Array.isArray(value)) return { type: "array" };
17728
+ if (value !== null && typeof value === "object") {
17729
+ const fields = /* @__PURE__ */ new Map();
17730
+ for (const [key, val] of Object.entries(value)) fields.set(key, inferSchema(val));
17731
+ return {
17732
+ type: "object",
17733
+ fields
17734
+ };
17735
+ }
17736
+ const jsType = typeof value;
17737
+ if (!VALID_VAR_TYPES.has(jsType)) throw new Error(`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`);
17738
+ return { type: jsType };
17739
+ }
17740
+ /**
17741
+ * Parse a StoryVariables or StoryTransients passage content into a schema map.
17742
+ * Each line: `$varName = expression` (or `%varName = expression` for transients)
17743
+ */
17744
+ function parseStoryVariables(content, sigil = "$") {
17745
+ const schema = /* @__PURE__ */ new Map();
17746
+ const DECLARATION_RE = declarationRegex(sigil);
17747
+ const passageName = sigil === "%" ? "StoryTransients" : "StoryVariables";
17748
+ for (const rawLine of content.split("\n")) {
17749
+ const line = rawLine.trim();
17750
+ if (!line) continue;
17751
+ const match = line.match(DECLARATION_RE);
17752
+ if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
17753
+ const [, name, expr] = match;
17754
+ const nameError = variableNameError(name, sigil + name);
17755
+ if (nameError) throw new Error(`${passageName}: ${nameError}`);
17756
+ let value;
17757
+ try {
17758
+ value = new Function("return (" + expr + ")")();
17759
+ } catch (err) {
17760
+ throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${errorMessage(err)}`);
17761
+ }
17762
+ let fieldSchema;
17763
+ try {
17764
+ fieldSchema = inferSchema(value);
17765
+ } catch (err) {
17766
+ throw new Error(`${passageName}: ${errorMessage(err)}`);
17767
+ }
17768
+ schema.set(name, {
17769
+ ...fieldSchema,
17770
+ name,
17771
+ default: value
17772
+ });
17773
+ }
17774
+ return schema;
17775
+ }
17776
+ /**
17777
+ * Extract for-loop local variable names from passage content.
17778
+ * `{for @item of ...}` → "item"
17779
+ * `{for @index, @item of ...}` → "index", "item"
17780
+ */
17781
+ function extractForLocals(content) {
17782
+ const locals = /* @__PURE__ */ new Set();
17783
+ let match;
17784
+ FOR_LOCAL_RE.lastIndex = 0;
17785
+ while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
17786
+ locals.add(match[1]);
17787
+ if (match[2]) locals.add(match[2]);
17788
+ }
17789
+ return locals;
17790
+ }
17791
+ /**
17792
+ * Validate a single variable reference path (e.g. "player.health") against
17793
+ * the schema. Returns an error message or null if valid.
17794
+ */
17795
+ function validateRef(ref, schema, forLocals) {
17796
+ const parts = ref.split(".");
17797
+ const rootName = parts[0];
17798
+ const nameError = variableNameError(rootName, "$" + rootName);
17799
+ if (nameError) return nameError;
17800
+ if (forLocals.has(rootName)) return null;
17801
+ const rootSchema = schema.get(rootName);
17802
+ if (!rootSchema) return `Undeclared variable: $${ref}`;
17445
17803
  let current = rootSchema;
17446
17804
  for (let i = 1; i < parts.length; i++) {
17447
17805
  const part = parts[i];
@@ -17547,11 +17905,20 @@ function scanCodeLeniently(code, onRef, goal) {
17547
17905
  * code), quoted variable names bound by input macros, and `{$…}`
17548
17906
  * interpolations in HTML attributes. Prose is literal text and not scanned.
17549
17907
  */
17550
- function collectPassageRefs(content, storeVarMacros, onRef) {
17551
- withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef));
17908
+ function collectPassageRefs(content, storeVarMacros, onRef, parametersOf) {
17909
+ withParseCache(() => collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef, parametersOf));
17910
+ }
17911
+ /**
17912
+ * Report the references in the code that a macro's quoted arguments hold,
17913
+ * the condition and `run` action of `{watch}`: the argument scan sees only
17914
+ * the strings.
17915
+ */
17916
+ function scanStringCode(token, params, onRef) {
17917
+ if (!params || !token.rawArgs) return;
17918
+ for (const piece of argPieces(token.rawArgs, 0, params, token.name, "")) if (piece.kind === "code" && piece.inString) scanCode(piece.code, onRef, piece.goal);
17552
17919
  }
17553
17920
  /** Report the `$var` references in the tokens of `content`. */
17554
- function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
17921
+ function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf = () => void 0) {
17555
17922
  for (let t = 0; t < tokens.length; t++) {
17556
17923
  const token = tokens[t];
17557
17924
  if (token.type === "variable") {
@@ -17561,6 +17928,7 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
17561
17928
  else scanInterpolations(value, onRef, storeVarMacros);
17562
17929
  else if (token.type === "macro" && !token.isClose) {
17563
17930
  scanCode(token.rawArgs, onRef);
17931
+ scanStringCode(token, parametersOf(token.name), onRef);
17564
17932
  if (storeVarMacros.has(token.name.toLowerCase())) {
17565
17933
  const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
17566
17934
  const quoted = QUOTED_VAR_ARG_RE.exec(first);
@@ -17589,16 +17957,16 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
17589
17957
  * `storeVarMacros` lists the input macros whose first argument names a bound
17590
17958
  * variable; it defaults to the built-in ones.
17591
17959
  */
17592
- function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_MACROS) {
17960
+ function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_MACROS, parametersOf = getMacroParameters) {
17593
17961
  const errors = [];
17594
17962
  const storeVarSet = new Set(Array.from(storeVarMacros, (m) => m.toLowerCase()));
17595
17963
  for (const [name, passage] of passages) {
17596
- if (name === "StoryVariables" || name === "StoryTransients") continue;
17964
+ if (NOT_MARKUP.has(name)) continue;
17597
17965
  const forLocals = extractForLocals(passage.content);
17598
17966
  collectPassageRefs(passage.content, storeVarSet, (ref) => {
17599
17967
  const error = validateRef(ref, schema, forLocals);
17600
17968
  if (error) errors.push(`Passage "${name}": ${error}`);
17601
- });
17969
+ }, parametersOf);
17602
17970
  }
17603
17971
  return errors;
17604
17972
  }
@@ -17612,218 +17980,6 @@ function extractDefaults(schema) {
17612
17980
  return defaults;
17613
17981
  }
17614
17982
  //#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
17983
  //#region src/widgets/ast-scanner.ts
17828
17984
  /**
17829
17985
  * Recursively scan an AST node array for a VariableNode
@@ -18479,6 +18635,7 @@ defineMacro({
18479
18635
  }
18480
18636
  const runBody = useDetachedBody();
18481
18637
  const perform = () => {
18638
+ if (failure) return;
18482
18639
  runBody(children);
18483
18640
  if (passage) useStoryStore.getState().navigate(passage);
18484
18641
  };
@@ -18492,6 +18649,7 @@ defineMacro({
18492
18649
  authorId: ctx.id,
18493
18650
  label: display,
18494
18651
  target: passage ?? void 0,
18652
+ disabled: failure ? true : void 0,
18495
18653
  perform
18496
18654
  });
18497
18655
  if (failure) return /* @__PURE__ */ jsx(MacroError, {