@rohal12/spindle 0.56.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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +638 -414
- package/dist/pkg/macro-registry.json +2 -1
- package/dist/pkg/story-variables.js +611 -157
- package/dist/pkg/tooling.js +39 -2
- package/package.json +1 -1
- package/src/code-check.ts +129 -21
- package/src/components/macros/Include.tsx +2 -6
- package/src/components/macros/MacroLink.tsx +33 -7
- package/src/components/macros/arg-utils.ts +37 -0
- package/src/components/macros/include-args.ts +23 -0
- package/src/components/macros/macro-args.ts +61 -5
- package/src/components/macros/option-utils.ts +17 -6
- package/src/define-macro.ts +22 -2
- package/src/expression.ts +4 -54
- package/src/markup/grammar-shared.ts +8 -0
- package/src/markup/parse.ts +89 -2
- package/src/markup/spindle.d.peggy.ts +5 -2
- package/src/markup/spindle.peggy +6 -7
- package/src/markup/validate.ts +16 -3
- package/src/registry.ts +7 -0
- package/src/story-variables.ts +48 -10
- package/src/tooling.ts +59 -0
- package/src/transform.ts +65 -0
- package/types/tooling.d.ts +311 -0
package/dist/pkg/headless.js
CHANGED
|
@@ -32,15 +32,43 @@ function create() {
|
|
|
32
32
|
return (initializer) => bindPreact(createStore()(initializer));
|
|
33
33
|
}
|
|
34
34
|
//#endregion
|
|
35
|
-
//#region src/markup/
|
|
36
|
-
|
|
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(
|
|
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$
|
|
760
|
+
function peg$f56(s, sigil, name) {
|
|
731
761
|
return withSelectors(lexeme("variable", location(), {
|
|
732
762
|
name,
|
|
733
|
-
scope: SIGIL_SCOPES
|
|
763
|
+
scope: SIGIL_SCOPES[sigil]
|
|
734
764
|
}), s);
|
|
735
765
|
}
|
|
736
|
-
function peg$
|
|
766
|
+
function peg$f57(s, expression) {
|
|
737
767
|
return withSelectors(lexeme("expression", location(), { expression }), s);
|
|
738
768
|
}
|
|
739
|
-
function peg$
|
|
769
|
+
function peg$f58() {
|
|
740
770
|
fail("Unclosed " + LB + input.substr(location().end.offset, 1) + "…: no } ends it", location());
|
|
741
771
|
}
|
|
742
|
-
function peg$
|
|
772
|
+
function peg$f59(start) {
|
|
743
773
|
return skipTo(hooks.closeBrace(input, start, start));
|
|
744
774
|
}
|
|
745
|
-
function peg$
|
|
775
|
+
function peg$f60(start) {
|
|
746
776
|
return skipEnd - offset();
|
|
747
777
|
}
|
|
748
|
-
function peg$
|
|
778
|
+
function peg$f61(start, code) {
|
|
749
779
|
return code;
|
|
750
780
|
}
|
|
751
|
-
function peg$
|
|
781
|
+
function peg$f62(s, start, close, name, nameEnd) {
|
|
752
782
|
return skipTo(hooks.closeBrace(input, nameEnd, start));
|
|
753
783
|
}
|
|
754
|
-
function peg$
|
|
784
|
+
function peg$f63(s, start, close, name, nameEnd) {
|
|
755
785
|
return skipEnd - offset();
|
|
756
786
|
}
|
|
757
|
-
function peg$
|
|
787
|
+
function peg$f64(s, start, close, name, nameEnd, args) {
|
|
758
788
|
return args;
|
|
759
789
|
}
|
|
760
|
-
function peg$
|
|
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$
|
|
811
|
+
function peg$f66(s) {
|
|
782
812
|
fail("A closing tag starts with a letter after " + LB + "/", location());
|
|
783
813
|
}
|
|
784
|
-
function peg$
|
|
814
|
+
function peg$f67() {
|
|
785
815
|
return !textMode;
|
|
786
816
|
}
|
|
787
|
-
function peg$
|
|
817
|
+
function peg$f68(tag) {
|
|
788
818
|
return VOID_TAGS.has(tag.lower);
|
|
789
819
|
}
|
|
790
|
-
function peg$
|
|
820
|
+
function peg$f69(tag) {
|
|
791
821
|
return null;
|
|
792
822
|
}
|
|
793
|
-
function peg$
|
|
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$
|
|
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 < 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$
|
|
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$
|
|
860
|
+
function peg$f73(name) {
|
|
831
861
|
attrCode = hooks.isCodeAttribute(name);
|
|
832
862
|
return true;
|
|
833
863
|
}
|
|
834
|
-
function peg$
|
|
864
|
+
function peg$f74(name, v) {
|
|
835
865
|
return v;
|
|
836
866
|
}
|
|
837
|
-
function peg$
|
|
867
|
+
function peg$f75(name, value) {
|
|
838
868
|
return [name, value ?? ""];
|
|
839
869
|
}
|
|
840
|
-
function peg$
|
|
870
|
+
function peg$f76(v) {
|
|
841
871
|
return v;
|
|
842
872
|
}
|
|
843
|
-
function peg$
|
|
873
|
+
function peg$f77(v) {
|
|
844
874
|
return v;
|
|
845
875
|
}
|
|
846
|
-
function peg$
|
|
876
|
+
function peg$f78(q) {
|
|
847
877
|
fail("Unclosed attribute value: the " + q + " here has no closing " + q, location());
|
|
848
878
|
}
|
|
849
|
-
function peg$
|
|
879
|
+
function peg$f79() {
|
|
850
880
|
return !attrCode;
|
|
851
881
|
}
|
|
852
|
-
function peg$
|
|
882
|
+
function peg$f80(bs) {
|
|
853
883
|
return bs.length % 2 === 1;
|
|
854
884
|
}
|
|
855
|
-
function peg$
|
|
885
|
+
function peg$f81() {
|
|
856
886
|
return !attrCode;
|
|
857
887
|
}
|
|
858
|
-
function peg$
|
|
888
|
+
function peg$f82(start, nameEnd) {
|
|
859
889
|
return skipTo(hooks.closeBrace(input, nameEnd, start));
|
|
860
890
|
}
|
|
861
|
-
function peg$
|
|
891
|
+
function peg$f83(start, nameEnd) {
|
|
862
892
|
return skipEnd - offset();
|
|
863
893
|
}
|
|
864
|
-
function peg$
|
|
894
|
+
function peg$f84() {
|
|
865
895
|
return !attrCode;
|
|
866
896
|
}
|
|
867
|
-
function peg$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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/
|
|
9880
|
-
|
|
9881
|
-
|
|
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
|
-
*
|
|
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 >=
|
|
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) {
|
|
@@ -16468,9 +16557,23 @@ var slice = (src, [start, end]) => src.slice(start, end);
|
|
|
16468
16557
|
function readText(src) {
|
|
16469
16558
|
return readWholeQuoted(src) ?? stripLooseQuotes(src);
|
|
16470
16559
|
}
|
|
16560
|
+
/**
|
|
16561
|
+
* An argument that doesn't have its parameter's form: a `string` parameter
|
|
16562
|
+
* given anything but one quoted string.
|
|
16563
|
+
*/
|
|
16564
|
+
var MacroArgumentError = class extends Error {
|
|
16565
|
+
constructor(param, src) {
|
|
16566
|
+
super(`The ${param.name} must be a quoted string ("…" or '…'), ${src ? `not ${src}` : "but it has no value"}`);
|
|
16567
|
+
this.name = "MacroArgumentError";
|
|
16568
|
+
}
|
|
16569
|
+
};
|
|
16471
16570
|
function readValue(param, src) {
|
|
16472
16571
|
switch (param.type) {
|
|
16473
|
-
case "string":
|
|
16572
|
+
case "string": {
|
|
16573
|
+
const value = readWholeQuoted(src);
|
|
16574
|
+
if (value === null) throw new MacroArgumentError(param, src);
|
|
16575
|
+
return value;
|
|
16576
|
+
}
|
|
16474
16577
|
case "text": return readText(src);
|
|
16475
16578
|
case "names": return src.split(",").map((name) => name.trim());
|
|
16476
16579
|
case "delay": return parseDelay(src);
|
|
@@ -16485,6 +16588,7 @@ function unset(param) {
|
|
|
16485
16588
|
}
|
|
16486
16589
|
var WORD_RE = /\w+/y;
|
|
16487
16590
|
var DIGITS_RE = /\d+/y;
|
|
16591
|
+
var WORD_OR_PUNCT_RE = /\S+/y;
|
|
16488
16592
|
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
16489
16593
|
function matchAt(re, src, pos) {
|
|
16490
16594
|
re.lastIndex = pos;
|
|
@@ -16493,7 +16597,8 @@ function matchAt(re, src, pos) {
|
|
|
16493
16597
|
/**
|
|
16494
16598
|
* Read keyword options (`goto "X" priority 5 once`). A keyword takes the
|
|
16495
16599
|
* quoted string or digit run after it as its value; keywords that aren't
|
|
16496
|
-
* 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.
|
|
16497
16602
|
*/
|
|
16498
16603
|
function readOptions(params, src) {
|
|
16499
16604
|
const options = {};
|
|
@@ -16506,10 +16611,11 @@ function readOptions(params, src) {
|
|
|
16506
16611
|
}
|
|
16507
16612
|
i += key.length;
|
|
16508
16613
|
let val;
|
|
16614
|
+
let quoted = null;
|
|
16509
16615
|
let j = i;
|
|
16510
16616
|
while (j < src.length && isWhitespace(src[j])) j++;
|
|
16511
16617
|
if (j > i) {
|
|
16512
|
-
|
|
16618
|
+
quoted = readQuoted(src, j);
|
|
16513
16619
|
const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
|
|
16514
16620
|
if (quoted) {
|
|
16515
16621
|
val = quoted.value;
|
|
@@ -16520,6 +16626,7 @@ function readOptions(params, src) {
|
|
|
16520
16626
|
}
|
|
16521
16627
|
}
|
|
16522
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) ?? "");
|
|
16523
16630
|
if (param?.type === "flag") options[key] = true;
|
|
16524
16631
|
else if (param?.type === "number") options[key] = Number(val ?? 0);
|
|
16525
16632
|
else if (param) options[key] = val;
|
|
@@ -16592,8 +16699,16 @@ function readGroup(params, src, args) {
|
|
|
16592
16699
|
let from = 0;
|
|
16593
16700
|
let to = spans.length;
|
|
16594
16701
|
for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
|
|
16702
|
+
const readTrailing = (param, value) => {
|
|
16703
|
+
try {
|
|
16704
|
+
return read(param, value);
|
|
16705
|
+
} catch (error) {
|
|
16706
|
+
if (error instanceof MacroArgumentError) return void 0;
|
|
16707
|
+
throw error;
|
|
16708
|
+
}
|
|
16709
|
+
};
|
|
16595
16710
|
for (const param of positional.slice(restIndex + 1).reverse()) {
|
|
16596
|
-
const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ?
|
|
16711
|
+
const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? readTrailing(param, slice(text, spans[to - 1])) : void 0;
|
|
16597
16712
|
args[param.name] = value ?? unset(param);
|
|
16598
16713
|
if (value !== void 0) to--;
|
|
16599
16714
|
}
|
|
@@ -16624,6 +16739,23 @@ function parseMacroArgs(rawArgs, parameters) {
|
|
|
16624
16739
|
return args;
|
|
16625
16740
|
}
|
|
16626
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
|
+
/**
|
|
16627
16759
|
* The passage a `passage` argument names (`{goto "Hall"}`, `{goto $room}`):
|
|
16628
16760
|
* the value of its expression. Throws what evaluating it throws, and if no
|
|
16629
16761
|
* passage of the story `state` holds has that name, an error naming it and
|
|
@@ -16662,8 +16794,15 @@ function parseVarArgs(rawArgs) {
|
|
|
16662
16794
|
/** The parameters of an `{option}` sub-macro: its value. */
|
|
16663
16795
|
var OPTION_PARAMETERS = [{
|
|
16664
16796
|
name: "value",
|
|
16665
|
-
type: "string"
|
|
16797
|
+
type: "string",
|
|
16798
|
+
required: true
|
|
16666
16799
|
}];
|
|
16800
|
+
/** The parameters of the built-in sub-macros that take arguments. */
|
|
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
|
+
}
|
|
16667
16806
|
/**
|
|
16668
16807
|
* Walk AST children to find {option} macro nodes, returning their values.
|
|
16669
16808
|
*/
|
|
@@ -16671,7 +16810,7 @@ function extractOptions(children) {
|
|
|
16671
16810
|
const options = [];
|
|
16672
16811
|
for (const node of children) if (node.type === "macro" && node.name === "option") {
|
|
16673
16812
|
const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
|
|
16674
|
-
options.push(value ??
|
|
16813
|
+
options.push(value ?? "");
|
|
16675
16814
|
}
|
|
16676
16815
|
return options;
|
|
16677
16816
|
}
|
|
@@ -16708,8 +16847,18 @@ function defineMacro(config, source = "builtin") {
|
|
|
16708
16847
|
...renderOptions,
|
|
16709
16848
|
...options
|
|
16710
16849
|
});
|
|
16850
|
+
let args;
|
|
16851
|
+
try {
|
|
16852
|
+
args = parseMacroArgs(props.rawArgs, parameters);
|
|
16853
|
+
} catch (error) {
|
|
16854
|
+
if (error instanceof MacroArgumentError) return h(MacroError, {
|
|
16855
|
+
macro: config.name,
|
|
16856
|
+
error
|
|
16857
|
+
});
|
|
16858
|
+
throw error;
|
|
16859
|
+
}
|
|
16711
16860
|
const ctx = {
|
|
16712
|
-
args
|
|
16861
|
+
args,
|
|
16713
16862
|
collectText,
|
|
16714
16863
|
sourceLocation: currentSourceLocation,
|
|
16715
16864
|
parseVarArgs,
|
|
@@ -16748,7 +16897,15 @@ function defineMacro(config, source = "builtin") {
|
|
|
16748
16897
|
});
|
|
16749
16898
|
};
|
|
16750
16899
|
}
|
|
16751
|
-
|
|
16900
|
+
try {
|
|
16901
|
+
return config.render(props, ctx);
|
|
16902
|
+
} catch (error) {
|
|
16903
|
+
if (error instanceof MacroArgumentError) return h(MacroError, {
|
|
16904
|
+
macro: config.name,
|
|
16905
|
+
error
|
|
16906
|
+
});
|
|
16907
|
+
throw error;
|
|
16908
|
+
}
|
|
16752
16909
|
}
|
|
16753
16910
|
registerMacro(config.name, Wrapper);
|
|
16754
16911
|
const text = config.text;
|
|
@@ -17113,15 +17270,21 @@ var CONDITION_MACROS = new Set([
|
|
|
17113
17270
|
"case"
|
|
17114
17271
|
]);
|
|
17115
17272
|
/** Code inside the quoted strings of a macro's arguments, by parameter. */
|
|
17116
|
-
var CODE_IN_STRINGS =
|
|
17273
|
+
var CODE_IN_STRINGS = new Map([["watch", {
|
|
17117
17274
|
condition: "expression",
|
|
17118
17275
|
run: "statements"
|
|
17119
|
-
}
|
|
17276
|
+
}]]);
|
|
17120
17277
|
/** The `string` parameters of a macro that name a passage. */
|
|
17121
|
-
var PASSAGE_STRINGS =
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
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
|
+
]);
|
|
17125
17288
|
/** Block macros whose body is the name of a passage. */
|
|
17126
17289
|
var PASSAGE_BODIES = new Set(["dialog"]);
|
|
17127
17290
|
/** The goal of the code an argument of this type holds, if it is code. */
|
|
@@ -17142,6 +17305,8 @@ function* codeAndText(src, tokens, parametersOf) {
|
|
|
17142
17305
|
kind: "passage",
|
|
17143
17306
|
name: token.target,
|
|
17144
17307
|
offset: locate(src, token.target, token.start),
|
|
17308
|
+
length: token.target.length,
|
|
17309
|
+
macro: "link",
|
|
17145
17310
|
label: src.slice(token.start, token.end)
|
|
17146
17311
|
};
|
|
17147
17312
|
else if (token.type === "expression") yield {
|
|
@@ -17201,6 +17366,8 @@ function* codeAndText(src, tokens, parametersOf) {
|
|
|
17201
17366
|
kind: "passage",
|
|
17202
17367
|
name: passage,
|
|
17203
17368
|
offset: locate(src, passage, token.end),
|
|
17369
|
+
length: passage.length,
|
|
17370
|
+
macro: name,
|
|
17204
17371
|
label: src.slice(token.start, token.end)
|
|
17205
17372
|
};
|
|
17206
17373
|
}
|
|
@@ -17225,9 +17392,23 @@ function* codeAndText(src, tokens, parametersOf) {
|
|
|
17225
17392
|
}
|
|
17226
17393
|
/** The code and text in the arguments `args` (at `offset`) of `macro`. */
|
|
17227
17394
|
function* argPieces(args, offset, params, macro, label) {
|
|
17228
|
-
|
|
17229
|
-
|
|
17230
|
-
|
|
17395
|
+
let values;
|
|
17396
|
+
try {
|
|
17397
|
+
values = parseMacroArgs(args, params);
|
|
17398
|
+
} catch (error) {
|
|
17399
|
+
if (!(error instanceof MacroArgumentError)) throw error;
|
|
17400
|
+
yield {
|
|
17401
|
+
kind: "argument-error",
|
|
17402
|
+
message: error.message,
|
|
17403
|
+
offset,
|
|
17404
|
+
label
|
|
17405
|
+
};
|
|
17406
|
+
return;
|
|
17407
|
+
}
|
|
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) ?? [];
|
|
17231
17412
|
let cursor = 0;
|
|
17232
17413
|
/** The pieces of the parameters `list`, with their values in `from`. */
|
|
17233
17414
|
function* visit(list, from) {
|
|
@@ -17249,17 +17430,27 @@ function* argPieces(args, offset, params, macro, label) {
|
|
|
17249
17430
|
goal,
|
|
17250
17431
|
label
|
|
17251
17432
|
};
|
|
17252
|
-
if (param.type === "passage")
|
|
17433
|
+
if (param.type === "passage") {
|
|
17434
|
+
piece.passage = true;
|
|
17435
|
+
piece.macro = macro.toLowerCase();
|
|
17436
|
+
}
|
|
17437
|
+
if (!codeGoal(param)) piece.inString = true;
|
|
17253
17438
|
yield piece;
|
|
17254
17439
|
}
|
|
17255
|
-
const
|
|
17256
|
-
|
|
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 {
|
|
17257
17446
|
kind: "passage",
|
|
17258
|
-
name,
|
|
17259
|
-
offset: offset + at,
|
|
17447
|
+
name: target.name,
|
|
17448
|
+
offset: offset + at + value.indexOf(written),
|
|
17449
|
+
length: written.length,
|
|
17450
|
+
macro: macro.toLowerCase(),
|
|
17260
17451
|
label
|
|
17261
17452
|
};
|
|
17262
|
-
if (!goal && (param.type === "text" || param.type === "string")) yield {
|
|
17453
|
+
if (!goal && (param.type === "text" || param.type === "string") && !literalStrings.includes(param.name)) yield {
|
|
17263
17454
|
kind: "text",
|
|
17264
17455
|
text: value,
|
|
17265
17456
|
offset: offset + at,
|
|
@@ -17280,92 +17471,311 @@ function locate(text, part, from) {
|
|
|
17280
17471
|
return anywhere >= 0 ? anywhere : from;
|
|
17281
17472
|
}
|
|
17282
17473
|
//#endregion
|
|
17283
|
-
//#region src/
|
|
17474
|
+
//#region src/widgets/widget-def.ts
|
|
17284
17475
|
/**
|
|
17285
|
-
*
|
|
17286
|
-
*
|
|
17476
|
+
* Reading widget definitions without rendering anything: the `{widget}`
|
|
17477
|
+
* macro, startup and markup validation (also in tooling) share these.
|
|
17287
17478
|
*/
|
|
17288
|
-
|
|
17289
|
-
|
|
17290
|
-
|
|
17291
|
-
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
|
|
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
|
+
};
|
|
17299
17498
|
}
|
|
17300
|
-
/**
|
|
17301
|
-
|
|
17302
|
-
|
|
17303
|
-
var QUOTED_VAR_ARG_RE = /^["']\$(\w+(?:\.\w+)*)["']?$/;
|
|
17304
|
-
var FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
17305
|
-
var VALID_VAR_TYPES = new Set([
|
|
17306
|
-
"number",
|
|
17307
|
-
"string",
|
|
17308
|
-
"boolean"
|
|
17309
|
-
]);
|
|
17310
|
-
/** Boxed sample values whose members a primitive of each type can access. */
|
|
17311
|
-
var PRIMITIVE_SAMPLES = {
|
|
17312
|
-
string: Object(""),
|
|
17313
|
-
number: Object(0),
|
|
17314
|
-
boolean: Object(false)
|
|
17315
|
-
};
|
|
17316
|
-
function inferSchema(value) {
|
|
17317
|
-
if (Array.isArray(value)) return { type: "array" };
|
|
17318
|
-
if (value !== null && typeof value === "object") {
|
|
17319
|
-
const fields = /* @__PURE__ */ new Map();
|
|
17320
|
-
for (const [key, val] of Object.entries(value)) fields.set(key, inferSchema(val));
|
|
17321
|
-
return {
|
|
17322
|
-
type: "object",
|
|
17323
|
-
fields
|
|
17324
|
-
};
|
|
17325
|
-
}
|
|
17326
|
-
const jsType = typeof value;
|
|
17327
|
-
if (!VALID_VAR_TYPES.has(jsType)) throw new Error(`Unsupported type "${jsType}" for value ${String(value)}. Expected number, string, boolean, array, or object.`);
|
|
17328
|
-
return { type: jsType };
|
|
17499
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
17500
|
+
function parseWidgetDef(rawArgs) {
|
|
17501
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
17329
17502
|
}
|
|
17503
|
+
var BLOCK_WIDGET = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
|
|
17330
17504
|
/**
|
|
17331
|
-
*
|
|
17332
|
-
*
|
|
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.
|
|
17333
17509
|
*/
|
|
17334
|
-
function
|
|
17335
|
-
const
|
|
17336
|
-
const
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
const line = rawLine.trim();
|
|
17340
|
-
if (!line) continue;
|
|
17341
|
-
const match = line.match(DECLARATION_RE);
|
|
17342
|
-
if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
|
|
17343
|
-
const [, name, expr] = match;
|
|
17344
|
-
const nameError = variableNameError(name, sigil + name);
|
|
17345
|
-
if (nameError) throw new Error(`${passageName}: ${nameError}`);
|
|
17346
|
-
let value;
|
|
17347
|
-
try {
|
|
17348
|
-
value = new Function("return (" + expr + ")")();
|
|
17349
|
-
} catch (err) {
|
|
17350
|
-
throw new Error(`${passageName}: Failed to evaluate "${sigil}${name} = ${expr}": ${errorMessage(err)}`);
|
|
17351
|
-
}
|
|
17352
|
-
let fieldSchema;
|
|
17353
|
-
try {
|
|
17354
|
-
fieldSchema = inferSchema(value);
|
|
17355
|
-
} catch (err) {
|
|
17356
|
-
throw new Error(`${passageName}: ${errorMessage(err)}`);
|
|
17357
|
-
}
|
|
17358
|
-
schema.set(name, {
|
|
17359
|
-
...fieldSchema,
|
|
17360
|
-
name,
|
|
17361
|
-
default: value
|
|
17362
|
-
});
|
|
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]);
|
|
17363
17515
|
}
|
|
17364
|
-
return
|
|
17516
|
+
return names;
|
|
17365
17517
|
}
|
|
17518
|
+
//#endregion
|
|
17519
|
+
//#region src/markup/validate.ts
|
|
17366
17520
|
/**
|
|
17367
|
-
*
|
|
17368
|
-
*
|
|
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).
|
|
17526
|
+
*/
|
|
17527
|
+
/**
|
|
17528
|
+
* Passages that hold no markup: declarations, and the JavaScript function
|
|
17529
|
+
* body of SaveTitle.
|
|
17530
|
+
*/
|
|
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"
|
|
17369
17779
|
* `{for @index, @item of ...}` → "index", "item"
|
|
17370
17780
|
*/
|
|
17371
17781
|
function extractForLocals(content) {
|
|
@@ -17495,11 +17905,20 @@ function scanCodeLeniently(code, onRef, goal) {
|
|
|
17495
17905
|
* code), quoted variable names bound by input macros, and `{$…}`
|
|
17496
17906
|
* interpolations in HTML attributes. Prose is literal text and not scanned.
|
|
17497
17907
|
*/
|
|
17498
|
-
function collectPassageRefs(content, storeVarMacros, onRef) {
|
|
17499
|
-
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);
|
|
17500
17919
|
}
|
|
17501
17920
|
/** Report the `$var` references in the tokens of `content`. */
|
|
17502
|
-
function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
|
|
17921
|
+
function collectTokenRefs(content, tokens, storeVarMacros, onRef, parametersOf = () => void 0) {
|
|
17503
17922
|
for (let t = 0; t < tokens.length; t++) {
|
|
17504
17923
|
const token = tokens[t];
|
|
17505
17924
|
if (token.type === "variable") {
|
|
@@ -17509,6 +17928,7 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
|
|
|
17509
17928
|
else scanInterpolations(value, onRef, storeVarMacros);
|
|
17510
17929
|
else if (token.type === "macro" && !token.isClose) {
|
|
17511
17930
|
scanCode(token.rawArgs, onRef);
|
|
17931
|
+
scanStringCode(token, parametersOf(token.name), onRef);
|
|
17512
17932
|
if (storeVarMacros.has(token.name.toLowerCase())) {
|
|
17513
17933
|
const first = token.rawArgs.trim().split(/\s+/)[0] ?? "";
|
|
17514
17934
|
const quoted = QUOTED_VAR_ARG_RE.exec(first);
|
|
@@ -17537,16 +17957,16 @@ function collectTokenRefs(content, tokens, storeVarMacros, onRef) {
|
|
|
17537
17957
|
* `storeVarMacros` lists the input macros whose first argument names a bound
|
|
17538
17958
|
* variable; it defaults to the built-in ones.
|
|
17539
17959
|
*/
|
|
17540
|
-
function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_MACROS) {
|
|
17960
|
+
function validatePassages(passages, schema, storeVarMacros = BUILTIN_STORE_VAR_MACROS, parametersOf = getMacroParameters) {
|
|
17541
17961
|
const errors = [];
|
|
17542
17962
|
const storeVarSet = new Set(Array.from(storeVarMacros, (m) => m.toLowerCase()));
|
|
17543
17963
|
for (const [name, passage] of passages) {
|
|
17544
|
-
if (name
|
|
17964
|
+
if (NOT_MARKUP.has(name)) continue;
|
|
17545
17965
|
const forLocals = extractForLocals(passage.content);
|
|
17546
17966
|
collectPassageRefs(passage.content, storeVarSet, (ref) => {
|
|
17547
17967
|
const error = validateRef(ref, schema, forLocals);
|
|
17548
17968
|
if (error) errors.push(`Passage "${name}": ${error}`);
|
|
17549
|
-
});
|
|
17969
|
+
}, parametersOf);
|
|
17550
17970
|
}
|
|
17551
17971
|
return errors;
|
|
17552
17972
|
}
|
|
@@ -17560,214 +17980,6 @@ function extractDefaults(schema) {
|
|
|
17560
17980
|
return defaults;
|
|
17561
17981
|
}
|
|
17562
17982
|
//#endregion
|
|
17563
|
-
//#region src/widgets/widget-def.ts
|
|
17564
|
-
/**
|
|
17565
|
-
* Reading widget definitions without rendering anything: the `{widget}`
|
|
17566
|
-
* macro, startup and markup validation (also in tooling) share these.
|
|
17567
|
-
*/
|
|
17568
|
-
/** A {widget} definition's name, then its `@` parameters. */
|
|
17569
|
-
var WIDGET_PARAMETERS = [{
|
|
17570
|
-
name: "name",
|
|
17571
|
-
type: "text",
|
|
17572
|
-
required: true
|
|
17573
|
-
}, {
|
|
17574
|
-
name: "parameters",
|
|
17575
|
-
type: "text"
|
|
17576
|
-
}];
|
|
17577
|
-
/**
|
|
17578
|
-
* The widget a {widget} definition's arguments declare: its name and its
|
|
17579
|
-
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
17580
|
-
* Other words, such as `$name`, are not parameters.
|
|
17581
|
-
*/
|
|
17582
|
-
function widgetDef({ name = "", parameters = "" }) {
|
|
17583
|
-
return {
|
|
17584
|
-
name,
|
|
17585
|
-
params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
|
|
17586
|
-
};
|
|
17587
|
-
}
|
|
17588
|
-
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
17589
|
-
function parseWidgetDef(rawArgs) {
|
|
17590
|
-
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
17591
|
-
}
|
|
17592
|
-
var BLOCK_WIDGET = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
|
|
17593
|
-
/**
|
|
17594
|
-
* The names of the block widgets (those whose body renders `{@children}`)
|
|
17595
|
-
* that StoryInit and the passages tagged `widget` define. They must be
|
|
17596
|
-
* known as block macros before any passage is parsed, so that passages
|
|
17597
|
-
* invoking them nest their content whatever the passage order.
|
|
17598
|
-
*/
|
|
17599
|
-
function blockWidgetNames(passages) {
|
|
17600
|
-
const names = [];
|
|
17601
|
-
for (const passage of passages) {
|
|
17602
|
-
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
17603
|
-
for (const match of passage.content.matchAll(BLOCK_WIDGET)) if (/\{@children\}/.test(match[2])) names.push(match[1]);
|
|
17604
|
-
}
|
|
17605
|
-
return names;
|
|
17606
|
-
}
|
|
17607
|
-
//#endregion
|
|
17608
|
-
//#region src/markup/validate.ts
|
|
17609
|
-
/**
|
|
17610
|
-
* Markup validation: parse every passage when the story starts and report
|
|
17611
|
-
* malformed markup, unknown macros, syntax errors in the code it runs and
|
|
17612
|
-
* passage names that name no passage (see code-check.ts) with the passage,
|
|
17613
|
-
* line and column they are at, instead of failing when the passage renders.
|
|
17614
|
-
* Tooling runs it too (see src/tooling.ts).
|
|
17615
|
-
*/
|
|
17616
|
-
/** Passages that hold no markup. */
|
|
17617
|
-
var NOT_MARKUP = new Set(["StoryVariables", "StoryTransients"]);
|
|
17618
|
-
var NOT_MARKUP_TAGS = ["script", "stylesheet"];
|
|
17619
|
-
/** Branch macros, which their parent macro renders. */
|
|
17620
|
-
var BRANCH_MACROS = new Set([
|
|
17621
|
-
"elseif",
|
|
17622
|
-
"else",
|
|
17623
|
-
"case",
|
|
17624
|
-
"default",
|
|
17625
|
-
"next"
|
|
17626
|
-
]);
|
|
17627
|
-
/** A diagnostic as one line of text. */
|
|
17628
|
-
function formatDiagnostic(d) {
|
|
17629
|
-
const source = d.file ? ` (${d.file}${d.fileLine ? `:${d.fileLine}` : ""})` : "";
|
|
17630
|
-
return `Passage "${d.passage}", line ${d.line}, column ${d.column}${source}: ${d.message}`;
|
|
17631
|
-
}
|
|
17632
|
-
/** Edit distance, for "did you mean" suggestions. */
|
|
17633
|
-
function distance(a, b) {
|
|
17634
|
-
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
17635
|
-
for (let i = 1; i <= a.length; i++) {
|
|
17636
|
-
const row = [i];
|
|
17637
|
-
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));
|
|
17638
|
-
prev = row;
|
|
17639
|
-
}
|
|
17640
|
-
return prev[b.length];
|
|
17641
|
-
}
|
|
17642
|
-
/**
|
|
17643
|
-
* " Did you mean …?" with the known name closest to `name`, written by
|
|
17644
|
-
* `quote`, if one is close.
|
|
17645
|
-
*/
|
|
17646
|
-
function didYouMean(name, names, quote) {
|
|
17647
|
-
const lower = name.toLowerCase();
|
|
17648
|
-
let best = "";
|
|
17649
|
-
let bestDistance = Math.max(2, Math.floor(lower.length / 3)) + 1;
|
|
17650
|
-
for (const candidate of names) {
|
|
17651
|
-
const d = distance(lower, candidate.toLowerCase());
|
|
17652
|
-
if (d < bestDistance) {
|
|
17653
|
-
best = candidate;
|
|
17654
|
-
bestDistance = d;
|
|
17655
|
-
}
|
|
17656
|
-
}
|
|
17657
|
-
return best ? ` Did you mean ${quote(best)}?` : "";
|
|
17658
|
-
}
|
|
17659
|
-
/** What a passage argument may be, after an error in one. */
|
|
17660
|
-
var PASSAGE_ARGUMENT_HINT = " (a passage name is a quoted string or an expression)";
|
|
17661
|
-
/** A passage argument that is one bare word (`{goto Kitchen}`). */
|
|
17662
|
-
var BARE_WORD_RE = /^[A-Za-z][\w$]*$/;
|
|
17663
|
-
/** The name of the widget a `{widget}` with these arguments defines. */
|
|
17664
|
-
function widgetName(rawArgs) {
|
|
17665
|
-
try {
|
|
17666
|
-
return parseWidgetDef(rawArgs).name.toLowerCase() || void 0;
|
|
17667
|
-
} catch {
|
|
17668
|
-
return;
|
|
17669
|
-
}
|
|
17670
|
-
}
|
|
17671
|
-
/**
|
|
17672
|
-
* Validate the markup of every passage. Passage names written out (links,
|
|
17673
|
-
* quoted `passage` arguments) must name one of `passages`.
|
|
17674
|
-
*/
|
|
17675
|
-
function validateMarkup(passages, options) {
|
|
17676
|
-
const diagnostics = [];
|
|
17677
|
-
const hooks = options.isBlockMacro ? { isBlock: options.isBlockMacro } : void 0;
|
|
17678
|
-
const report = (passage, offset, message) => {
|
|
17679
|
-
const { line, column } = lineColumn(passage.content, offset);
|
|
17680
|
-
const d = {
|
|
17681
|
-
passage: passage.name,
|
|
17682
|
-
line,
|
|
17683
|
-
column,
|
|
17684
|
-
message
|
|
17685
|
-
};
|
|
17686
|
-
const file = passage.metadata?.["data-source-file"];
|
|
17687
|
-
if (file) {
|
|
17688
|
-
d.file = file;
|
|
17689
|
-
const header = Number(passage.metadata?.["data-source-line"]);
|
|
17690
|
-
if (header) d.fileLine = header + line;
|
|
17691
|
-
}
|
|
17692
|
-
diagnostics.push(d);
|
|
17693
|
-
};
|
|
17694
|
-
const parsed = [];
|
|
17695
|
-
const widgets = /* @__PURE__ */ new Set();
|
|
17696
|
-
const passageNames = /* @__PURE__ */ new Set();
|
|
17697
|
-
for (const passage of passages) {
|
|
17698
|
-
passageNames.add(passage.name);
|
|
17699
|
-
if (NOT_MARKUP.has(passage.name)) continue;
|
|
17700
|
-
if (passage.tags?.some((tag) => NOT_MARKUP_TAGS.includes(tag))) continue;
|
|
17701
|
-
const reported = !options.only || options.only(passage);
|
|
17702
|
-
try {
|
|
17703
|
-
const tokens = tokenizeMarkup(passage.content);
|
|
17704
|
-
for (const token of tokens) {
|
|
17705
|
-
if (token.type !== "macro" || token.isClose) continue;
|
|
17706
|
-
if (token.name.toLowerCase() !== "widget") continue;
|
|
17707
|
-
const name = widgetName(token.rawArgs);
|
|
17708
|
-
if (name) widgets.add(name);
|
|
17709
|
-
}
|
|
17710
|
-
if (!reported) continue;
|
|
17711
|
-
parseMarkup(passage.content, { hooks });
|
|
17712
|
-
parsed.push([passage, tokens]);
|
|
17713
|
-
} catch (err) {
|
|
17714
|
-
if (!(err instanceof MarkupError)) throw err;
|
|
17715
|
-
if (reported) report(passage, err.offset, err.reason);
|
|
17716
|
-
}
|
|
17717
|
-
}
|
|
17718
|
-
const names = [...options.macroNames ?? [], ...widgets];
|
|
17719
|
-
/** Report the unknown macros among `tokens`, which start at `base`. */
|
|
17720
|
-
const checkMacros = (passage, tokens, base, where) => {
|
|
17721
|
-
for (const token of tokens) {
|
|
17722
|
-
if (token.type !== "macro" || token.isClose) continue;
|
|
17723
|
-
const lower = token.name.toLowerCase();
|
|
17724
|
-
if (BRANCH_MACROS.has(lower) || widgets.has(lower) || options.isKnownMacro(lower)) continue;
|
|
17725
|
-
report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
|
|
17726
|
-
}
|
|
17727
|
-
};
|
|
17728
|
-
const parametersOf = options.parametersOf ?? (() => void 0);
|
|
17729
|
-
/**
|
|
17730
|
-
* Check `tokens`, the tokens of `src`, which starts at `base` in the
|
|
17731
|
-
* passage: their macros, the code in them and the markup in their text.
|
|
17732
|
-
*/
|
|
17733
|
-
const checkTokens = (passage, src, tokens, base, where) => {
|
|
17734
|
-
checkMacros(passage, tokens, base, where);
|
|
17735
|
-
for (const piece of codeAndText(src, tokens, parametersOf)) {
|
|
17736
|
-
const at = base + piece.offset;
|
|
17737
|
-
if (piece.kind === "passage") {
|
|
17738
|
-
if (options.checkPassageNames !== false && !passageNames.has(piece.name)) {
|
|
17739
|
-
const hint = didYouMean(piece.name, passageNames, JSON.stringify);
|
|
17740
|
-
report(passage, at, `${where}No passage named ${JSON.stringify(piece.name)} in ${piece.label}.${hint}`);
|
|
17741
|
-
}
|
|
17742
|
-
continue;
|
|
17743
|
-
}
|
|
17744
|
-
if (piece.kind === "code") {
|
|
17745
|
-
const result = parseOrError(piece.code, piece.goal);
|
|
17746
|
-
const code = piece.code.trim();
|
|
17747
|
-
if (result instanceof CodeSyntaxError) {
|
|
17748
|
-
const reason = result.reasonIn(passage.content, at);
|
|
17749
|
-
report(passage, at + result.pos, `${where}${reason} in ${piece.label}` + (piece.passage ? PASSAGE_ARGUMENT_HINT : ""));
|
|
17750
|
-
} 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}`);
|
|
17751
|
-
continue;
|
|
17752
|
-
}
|
|
17753
|
-
if (!piece.text.includes("{")) continue;
|
|
17754
|
-
try {
|
|
17755
|
-
parseMarkup(piece.text, {
|
|
17756
|
-
text: true,
|
|
17757
|
-
hooks
|
|
17758
|
-
});
|
|
17759
|
-
const inner = tokenizeMarkup(piece.text, { text: true });
|
|
17760
|
-
checkTokens(passage, piece.text, inner, at, piece.where);
|
|
17761
|
-
} catch (err) {
|
|
17762
|
-
if (!(err instanceof MarkupError)) throw err;
|
|
17763
|
-
report(passage, at + err.offset, piece.where + err.reason);
|
|
17764
|
-
}
|
|
17765
|
-
}
|
|
17766
|
-
};
|
|
17767
|
-
for (const [passage, tokens] of parsed) withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ""));
|
|
17768
|
-
return diagnostics;
|
|
17769
|
-
}
|
|
17770
|
-
//#endregion
|
|
17771
17983
|
//#region src/widgets/ast-scanner.ts
|
|
17772
17984
|
/**
|
|
17773
17985
|
* Recursively scan an AST node array for a VariableNode
|
|
@@ -18403,20 +18615,27 @@ defineMacro({
|
|
|
18403
18615
|
name: "link",
|
|
18404
18616
|
block: true,
|
|
18405
18617
|
interpolate: true,
|
|
18618
|
+
merged: true,
|
|
18406
18619
|
parameters: [{
|
|
18407
18620
|
name: "text",
|
|
18408
18621
|
type: "string",
|
|
18409
18622
|
required: true
|
|
18410
18623
|
}, {
|
|
18411
18624
|
name: "passage",
|
|
18412
|
-
type: "
|
|
18625
|
+
type: "passage"
|
|
18413
18626
|
}],
|
|
18414
|
-
render({
|
|
18415
|
-
const
|
|
18416
|
-
|
|
18417
|
-
|
|
18627
|
+
render({ children = [] }, ctx) {
|
|
18628
|
+
const display = ctx.args.text ?? "";
|
|
18629
|
+
let passage = null;
|
|
18630
|
+
let failure;
|
|
18631
|
+
if (ctx.args.passage !== void 0) try {
|
|
18632
|
+
passage = evaluatePassageName(ctx.args.passage, ctx.evaluate, useStoryStore.getState());
|
|
18633
|
+
} catch (error) {
|
|
18634
|
+
failure = { error };
|
|
18635
|
+
}
|
|
18418
18636
|
const runBody = useDetachedBody();
|
|
18419
18637
|
const perform = () => {
|
|
18638
|
+
if (failure) return;
|
|
18420
18639
|
runBody(children);
|
|
18421
18640
|
if (passage) useStoryStore.getState().navigate(passage);
|
|
18422
18641
|
};
|
|
@@ -18430,8 +18649,13 @@ defineMacro({
|
|
|
18430
18649
|
authorId: ctx.id,
|
|
18431
18650
|
label: display,
|
|
18432
18651
|
target: passage ?? void 0,
|
|
18652
|
+
disabled: failure ? true : void 0,
|
|
18433
18653
|
perform
|
|
18434
18654
|
});
|
|
18655
|
+
if (failure) return /* @__PURE__ */ jsx(MacroError, {
|
|
18656
|
+
macro: "link",
|
|
18657
|
+
error: failure.error
|
|
18658
|
+
});
|
|
18435
18659
|
return /* @__PURE__ */ jsx("a", {
|
|
18436
18660
|
id: ctx.id,
|
|
18437
18661
|
class: ctx.cls,
|