@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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +584 -426
- package/dist/pkg/story-variables.js +567 -154
- package/dist/pkg/tooling.js +39 -2
- package/package.json +1 -1
- package/src/code-check.ts +107 -17
- package/src/components/macros/Include.tsx +2 -6
- package/src/components/macros/MacroLink.tsx +3 -0
- 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 +35 -3
- package/src/components/macros/option-utils.ts +9 -3
- 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 +11 -4
- 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) {
|
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
17282
|
-
const
|
|
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")
|
|
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
|
|
17308
|
-
|
|
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/
|
|
17474
|
+
//#region src/widgets/widget-def.ts
|
|
17336
17475
|
/**
|
|
17337
|
-
*
|
|
17338
|
-
*
|
|
17476
|
+
* Reading widget definitions without rendering anything: the `{widget}`
|
|
17477
|
+
* macro, startup and markup validation (also in tooling) share these.
|
|
17339
17478
|
*/
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
|
|
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
|
-
/**
|
|
17353
|
-
|
|
17354
|
-
|
|
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
|
-
*
|
|
17384
|
-
*
|
|
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
|
|
17387
|
-
const
|
|
17388
|
-
const
|
|
17389
|
-
|
|
17390
|
-
|
|
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
|
|
17516
|
+
return names;
|
|
17417
17517
|
}
|
|
17518
|
+
//#endregion
|
|
17519
|
+
//#region src/markup/validate.ts
|
|
17418
17520
|
/**
|
|
17419
|
-
*
|
|
17420
|
-
*
|
|
17421
|
-
*
|
|
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
|
-
*
|
|
17435
|
-
*
|
|
17528
|
+
* Passages that hold no markup: declarations, and the JavaScript function
|
|
17529
|
+
* body of SaveTitle.
|
|
17436
17530
|
*/
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
17440
|
-
|
|
17441
|
-
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
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
|
|
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, {
|