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