@rohal12/spindle 0.52.3 → 0.52.4
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 +267 -624
- package/dist/pkg/story-variables.js +155 -469
- package/package.json +1 -1
- package/src/components/macros/Widget.tsx +18 -7
- package/src/index.tsx +5 -8
- package/src/interpolation.ts +16 -22
- package/src/js-lexer.ts +20 -9
- package/src/markup/ast.ts +108 -147
- package/src/markup/code-attributes.ts +4 -6
- package/src/markup/tokenizer.ts +218 -574
package/dist/pkg/headless.js
CHANGED
|
@@ -486,13 +486,13 @@ function scanRegex(src, start, cache) {
|
|
|
486
486
|
};
|
|
487
487
|
let i = start + 1;
|
|
488
488
|
while (i < src.length) {
|
|
489
|
+
const lineEnd = regexLineEnd(src, i);
|
|
490
|
+
if (lineEnd !== -1) return done(lineEnd, false);
|
|
489
491
|
const c = src.charAt(i);
|
|
490
492
|
if (c === "\\") {
|
|
491
|
-
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return done(i + 1, false);
|
|
492
493
|
i += 2;
|
|
493
494
|
continue;
|
|
494
495
|
}
|
|
495
|
-
if (LINE_TERMINATOR_RE.test(c)) return done(i, false);
|
|
496
496
|
if (c === "[") {
|
|
497
497
|
i = scanRegexClass(src, i, cache);
|
|
498
498
|
if (src.charAt(i) !== "]") return done(i, false);
|
|
@@ -523,16 +523,17 @@ function scanRegexClass(src, open, cache) {
|
|
|
523
523
|
const opens = [open];
|
|
524
524
|
let i = open + 1;
|
|
525
525
|
while (i < src.length) {
|
|
526
|
+
const lineEnd = regexLineEnd(src, i);
|
|
527
|
+
if (lineEnd !== -1) {
|
|
528
|
+
i = lineEnd;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
526
531
|
const c = src.charAt(i);
|
|
527
532
|
if (c === "\\") {
|
|
528
|
-
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) {
|
|
529
|
-
i++;
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
533
|
i += 2;
|
|
533
534
|
continue;
|
|
534
535
|
}
|
|
535
|
-
if (c === "]"
|
|
536
|
+
if (c === "]") break;
|
|
536
537
|
if (c === "[" && cache) opens.push(i);
|
|
537
538
|
i++;
|
|
538
539
|
}
|
|
@@ -541,6 +542,16 @@ function scanRegexClass(src, open, cache) {
|
|
|
541
542
|
return i;
|
|
542
543
|
}
|
|
543
544
|
/**
|
|
545
|
+
* Index of the line break at `i`, escaped (`\` then a line break) or not,
|
|
546
|
+
* where a regex literal or class ends unterminated; -1 if there is none.
|
|
547
|
+
*/
|
|
548
|
+
function regexLineEnd(src, i) {
|
|
549
|
+
const c = src.charAt(i);
|
|
550
|
+
if (LINE_TERMINATOR_RE.test(c)) return i;
|
|
551
|
+
if (c === "\\" && LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return i + 1;
|
|
552
|
+
return -1;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
544
555
|
* The next index from `from` on where `find` matches (-1 for none), for
|
|
545
556
|
* searches from increasing positions: a search from within the stretch the
|
|
546
557
|
* last one covered has the same answer.
|
|
@@ -1490,8 +1501,7 @@ function splitSigilTemplate(template) {
|
|
|
1490
1501
|
}
|
|
1491
1502
|
text += template.slice(i, brace);
|
|
1492
1503
|
i = brace + 1;
|
|
1493
|
-
const
|
|
1494
|
-
const end = sigil === "$" || sigil === "_" || sigil === "@" || sigil === "%" ? scanBalancedBrace(template, i, memo) : -1;
|
|
1504
|
+
const end = isSigil(template[i]) ? scanBalancedBrace(template, i, memo) : -1;
|
|
1495
1505
|
if (end === -1) {
|
|
1496
1506
|
text += "{";
|
|
1497
1507
|
continue;
|
|
@@ -1507,6 +1517,26 @@ function splitSigilTemplate(template) {
|
|
|
1507
1517
|
}
|
|
1508
1518
|
//#endregion
|
|
1509
1519
|
//#region src/markup/tokenizer.ts
|
|
1520
|
+
/** Variable sigils: story ($), temporary (_), local (@), transient (%). */
|
|
1521
|
+
var SIGIL_SCOPES = {
|
|
1522
|
+
$: "variable",
|
|
1523
|
+
_: "temporary",
|
|
1524
|
+
"@": "local",
|
|
1525
|
+
"%": "transient"
|
|
1526
|
+
};
|
|
1527
|
+
/** The sigil of each variable scope. */
|
|
1528
|
+
var SCOPE_SIGILS = Object.fromEntries(Object.entries(SIGIL_SCOPES).map(([sigil, scope]) => [scope, sigil]));
|
|
1529
|
+
var SIGIL_CHARS = new Set(Object.keys(SIGIL_SCOPES));
|
|
1530
|
+
/** Whether `c` is a variable sigil. */
|
|
1531
|
+
function isSigil(c) {
|
|
1532
|
+
return c !== void 0 && SIGIL_CHARS.has(c);
|
|
1533
|
+
}
|
|
1534
|
+
/** Copy the selectors set in `from` onto `target`, and return it. */
|
|
1535
|
+
function withSelectors(target, from) {
|
|
1536
|
+
if (from.className) target.className = from.className;
|
|
1537
|
+
if (from.id) target.id = from.id;
|
|
1538
|
+
return target;
|
|
1539
|
+
}
|
|
1510
1540
|
/** Tag name must start with a letter (covers standard and custom elements). */
|
|
1511
1541
|
var VALID_TAG_START = /[a-zA-Z]/;
|
|
1512
1542
|
/** HTML void elements: never have children or a closing tag. */
|
|
@@ -1526,13 +1556,6 @@ var HTML_VOID_TAGS = new Set([
|
|
|
1526
1556
|
"track",
|
|
1527
1557
|
"wbr"
|
|
1528
1558
|
]);
|
|
1529
|
-
/** Variable sigils: story ($), temporary (_), local (@), transient (%). */
|
|
1530
|
-
var SIGIL_CHARS = new Set([
|
|
1531
|
-
"$",
|
|
1532
|
-
"_",
|
|
1533
|
-
"@",
|
|
1534
|
-
"%"
|
|
1535
|
-
]);
|
|
1536
1559
|
/**
|
|
1537
1560
|
* Characters other than a sigil that open an expression after `{`:
|
|
1538
1561
|
* `{(Math.max($a, 0))}`, `{!$done}`. Other characters that can start an
|
|
@@ -1543,25 +1566,32 @@ var EXPRESSION_START = new Set(["(", "!"]);
|
|
|
1543
1566
|
/** Macros whose body is JavaScript source, kept verbatim instead of tokenized. */
|
|
1544
1567
|
var RAW_BODY_MACROS = new Set(["do"]);
|
|
1545
1568
|
/**
|
|
1569
|
+
* Link separators, in the order they are tried, and whether the target
|
|
1570
|
+
* comes first: display|target, display->target, target<-display.
|
|
1571
|
+
*/
|
|
1572
|
+
var LINK_SEPARATORS = [
|
|
1573
|
+
["|", false],
|
|
1574
|
+
["->", false],
|
|
1575
|
+
["<-", true]
|
|
1576
|
+
];
|
|
1577
|
+
/**
|
|
1546
1578
|
* Parse a Twine link interior into display and target.
|
|
1547
1579
|
* Supports: display|target, display->target, target<-display, plain
|
|
1548
1580
|
*/
|
|
1549
1581
|
function parseLink(inner) {
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
display: inner.slice(revIdx + 2).trim()
|
|
1564
|
-
};
|
|
1582
|
+
for (const [separator, targetFirst] of LINK_SEPARATORS) {
|
|
1583
|
+
const idx = inner.indexOf(separator);
|
|
1584
|
+
if (idx === -1) continue;
|
|
1585
|
+
const before = inner.slice(0, idx).trim();
|
|
1586
|
+
const after = inner.slice(idx + separator.length).trim();
|
|
1587
|
+
return targetFirst ? {
|
|
1588
|
+
display: after,
|
|
1589
|
+
target: before
|
|
1590
|
+
} : {
|
|
1591
|
+
display: before,
|
|
1592
|
+
target: after
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1565
1595
|
const trimmed = inner.trim();
|
|
1566
1596
|
return {
|
|
1567
1597
|
display: trimmed,
|
|
@@ -1592,8 +1622,10 @@ function parseMacroContent(content) {
|
|
|
1592
1622
|
}
|
|
1593
1623
|
/**
|
|
1594
1624
|
* Parse CSS selectors: .foo.bar#baz → { className: "foo bar", id: "baz" }
|
|
1595
|
-
* Scans .[a-zA-Z0-9_-]+ and #[a-zA-Z0-9_-]+ segments in any order
|
|
1596
|
-
* Returns space-joined class string, last id wins
|
|
1625
|
+
* Scans .[a-zA-Z0-9_-]+ and #[a-zA-Z0-9_-]+ segments in any order, and one
|
|
1626
|
+
* space after them. Returns space-joined class string, last id wins (each
|
|
1627
|
+
* left out if empty), and the position after them: `startIdx` if there are
|
|
1628
|
+
* none.
|
|
1597
1629
|
*/
|
|
1598
1630
|
function parseSelectors(input, startIdx) {
|
|
1599
1631
|
const classes = [];
|
|
@@ -1606,7 +1638,7 @@ function parseSelectors(input, startIdx) {
|
|
|
1606
1638
|
while (i < input.length) if (/[a-zA-Z0-9_-]/.test(input[i])) {
|
|
1607
1639
|
name += input[i];
|
|
1608
1640
|
i++;
|
|
1609
|
-
} else if (input[i] === "{" &&
|
|
1641
|
+
} else if (input[i] === "{" && isSigil(input[i + 1])) {
|
|
1610
1642
|
const braceStart = i;
|
|
1611
1643
|
i += 2;
|
|
1612
1644
|
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
@@ -1621,10 +1653,13 @@ function parseSelectors(input, startIdx) {
|
|
|
1621
1653
|
if (name) if (prefix === ".") classes.push(name);
|
|
1622
1654
|
else id = name;
|
|
1623
1655
|
}
|
|
1656
|
+
if (i > startIdx && input[i] === " ") i++;
|
|
1624
1657
|
return {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1658
|
+
selectors: withSelectors({}, {
|
|
1659
|
+
className: classes.join(" "),
|
|
1660
|
+
id
|
|
1661
|
+
}),
|
|
1662
|
+
end: i
|
|
1628
1663
|
};
|
|
1629
1664
|
}
|
|
1630
1665
|
/**
|
|
@@ -1739,7 +1774,7 @@ function scanQuotedValue(input, j, quote, code, memo) {
|
|
|
1739
1774
|
continue;
|
|
1740
1775
|
}
|
|
1741
1776
|
if (input[j] === "{") {
|
|
1742
|
-
const closeIdx = !code ? scanBlockClose(input, j, memo) :
|
|
1777
|
+
const closeIdx = !code ? scanBlockClose(input, j, memo) : isSigil(input[j + 1]) ? scanBalancedBrace(input, j + 1, memo) : -1;
|
|
1743
1778
|
if (closeIdx !== -1) {
|
|
1744
1779
|
j = closeIdx + 1;
|
|
1745
1780
|
checkpoint = true;
|
|
@@ -1815,12 +1850,7 @@ function scanMacroClose(input, contentStart, memo) {
|
|
|
1815
1850
|
* code from just past the {. Returns -1 if it is unclosed.
|
|
1816
1851
|
*/
|
|
1817
1852
|
function scanBlockClose(input, open, memo) {
|
|
1818
|
-
|
|
1819
|
-
const c = input[at];
|
|
1820
|
-
if (c === "." || c === "#") {
|
|
1821
|
-
at = parseSelectors(input, at).endIdx;
|
|
1822
|
-
if (input[at] === " ") at++;
|
|
1823
|
-
}
|
|
1853
|
+
const at = parseSelectors(input, open + 1).end;
|
|
1824
1854
|
const first = input[at];
|
|
1825
1855
|
if (first !== void 0 && (first === "/" || /[a-zA-Z]/.test(first))) return scanMacroClose(input, at, memo);
|
|
1826
1856
|
return scanBalancedBrace(input, at, memo);
|
|
@@ -2012,13 +2042,22 @@ function tokenize(input, options = {}) {
|
|
|
2012
2042
|
const memo = createScanMemo();
|
|
2013
2043
|
let i = 0;
|
|
2014
2044
|
let textStart = 0;
|
|
2045
|
+
/** Push a text token for the text not yet tokenized before `end`. */
|
|
2015
2046
|
function flushText(end) {
|
|
2016
|
-
if (end > textStart)
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2047
|
+
if (end > textStart) {
|
|
2048
|
+
tokens.push({
|
|
2049
|
+
type: "text",
|
|
2050
|
+
value: input.slice(textStart, end),
|
|
2051
|
+
start: textStart,
|
|
2052
|
+
end
|
|
2053
|
+
});
|
|
2054
|
+
textStart = end;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
/** Push `token` and go on after it. */
|
|
2058
|
+
function emit(token) {
|
|
2059
|
+
tokens.push(token);
|
|
2060
|
+
i = textStart = token.end;
|
|
2022
2061
|
}
|
|
2023
2062
|
/**
|
|
2024
2063
|
* After an opening raw-body macro ({do}), emit everything up to its
|
|
@@ -2060,34 +2099,66 @@ function tokenize(input, options = {}) {
|
|
|
2060
2099
|
atRe.lastIndex = closeStart;
|
|
2061
2100
|
const closeEnd = closeStart + atRe.exec(input)[0].length;
|
|
2062
2101
|
flushText(closeStart);
|
|
2063
|
-
|
|
2102
|
+
emit({
|
|
2064
2103
|
type: "macro",
|
|
2065
2104
|
...parseMacroContent(input.slice(closeStart + 1, closeEnd - 1)),
|
|
2066
2105
|
start: closeStart,
|
|
2067
2106
|
end: closeEnd
|
|
2068
2107
|
});
|
|
2069
|
-
i = closeEnd;
|
|
2070
|
-
textStart = closeEnd;
|
|
2071
2108
|
}
|
|
2072
2109
|
/**
|
|
2073
2110
|
* Push an expression token for the `{…}` block opened at `start` whose
|
|
2074
|
-
* expression starts at `exprStart`, flushing the text before it first
|
|
2075
|
-
*
|
|
2111
|
+
* expression starts at `exprStart`, flushing the text before it first.
|
|
2112
|
+
* Returns false, consuming nothing, if the block is unclosed.
|
|
2076
2113
|
*/
|
|
2077
|
-
function pushExpression(exprStart, start,
|
|
2114
|
+
function pushExpression(exprStart, start, selectors) {
|
|
2078
2115
|
const closeIdx = scanBalancedBrace(input, exprStart, memo);
|
|
2079
2116
|
if (closeIdx === -1) return false;
|
|
2080
|
-
|
|
2081
|
-
|
|
2117
|
+
flushText(start);
|
|
2118
|
+
emit(withSelectors({
|
|
2082
2119
|
type: "expression",
|
|
2083
2120
|
expression: input.slice(exprStart, closeIdx),
|
|
2084
2121
|
start,
|
|
2085
2122
|
end: closeIdx + 1
|
|
2086
|
-
};
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2123
|
+
}, selectors));
|
|
2124
|
+
return true;
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Push a variable token for the `{…}` block opened at `start` whose sigil
|
|
2128
|
+
* is at `sigilAt`: `{$name}`, `{_name.field.subfield}`. Anything else
|
|
2129
|
+
* after the name makes the block an expression from the sigil on
|
|
2130
|
+
* (`{$expr[...]}`). Returns false, consuming nothing, if it is unclosed.
|
|
2131
|
+
*/
|
|
2132
|
+
function pushVariable(sigilAt, start, selectors) {
|
|
2133
|
+
let nameEnd = sigilAt + 1;
|
|
2134
|
+
while (nameEnd < input.length && /[\w.]/.test(input[nameEnd])) nameEnd++;
|
|
2135
|
+
if (input[nameEnd] !== "}") return pushExpression(sigilAt, start, selectors);
|
|
2136
|
+
emit(withSelectors({
|
|
2137
|
+
type: "variable",
|
|
2138
|
+
name: input.slice(sigilAt + 1, nameEnd),
|
|
2139
|
+
scope: SIGIL_SCOPES[input[sigilAt]],
|
|
2140
|
+
start,
|
|
2141
|
+
end: nameEnd + 1
|
|
2142
|
+
}, selectors));
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Push a macro token for the `{…}` block opened at `start` whose content
|
|
2147
|
+
* (name, then arguments) starts at `contentStart`, then consume the body
|
|
2148
|
+
* of a raw-body macro. Returns false, consuming nothing, if the block is
|
|
2149
|
+
* unclosed.
|
|
2150
|
+
*/
|
|
2151
|
+
function pushMacro(contentStart, start, selectors) {
|
|
2152
|
+
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
2153
|
+
if (closeIdx === -1) return false;
|
|
2154
|
+
const token = withSelectors({
|
|
2155
|
+
type: "macro",
|
|
2156
|
+
...parseMacroContent(input.slice(contentStart, closeIdx)),
|
|
2157
|
+
start,
|
|
2158
|
+
end: closeIdx + 1
|
|
2159
|
+
}, selectors);
|
|
2160
|
+
emit(token);
|
|
2161
|
+
consumeRawBody(token.name, token.isClose);
|
|
2091
2162
|
return true;
|
|
2092
2163
|
}
|
|
2093
2164
|
while (i < input.length) {
|
|
@@ -2111,14 +2182,12 @@ function tokenize(input, options = {}) {
|
|
|
2111
2182
|
}
|
|
2112
2183
|
if ((next === "{" || next === "}") && (k - i) % 2 === 1) {
|
|
2113
2184
|
flushText(k - 1);
|
|
2114
|
-
|
|
2185
|
+
emit({
|
|
2115
2186
|
type: "text",
|
|
2116
2187
|
value: next,
|
|
2117
2188
|
start: k - 1,
|
|
2118
2189
|
end: k + 1
|
|
2119
2190
|
});
|
|
2120
|
-
i = k + 1;
|
|
2121
|
-
textStart = i;
|
|
2122
2191
|
continue;
|
|
2123
2192
|
}
|
|
2124
2193
|
i = k;
|
|
@@ -2127,413 +2196,36 @@ function tokenize(input, options = {}) {
|
|
|
2127
2196
|
if (!textMode && input[i] === "[" && input[i + 1] === "[") {
|
|
2128
2197
|
flushText(i);
|
|
2129
2198
|
const start = i;
|
|
2130
|
-
i
|
|
2131
|
-
let className;
|
|
2132
|
-
let id;
|
|
2133
|
-
if (input[i] === "." || input[i] === "#") {
|
|
2134
|
-
const parsed = parseSelectors(input, i);
|
|
2135
|
-
className = parsed.className || void 0;
|
|
2136
|
-
id = parsed.id || void 0;
|
|
2137
|
-
i = parsed.endIdx;
|
|
2138
|
-
if (input[i] === " ") i++;
|
|
2139
|
-
}
|
|
2140
|
-
const innerStart = i;
|
|
2199
|
+
const { selectors, end: innerStart } = parseSelectors(input, i + 2);
|
|
2141
2200
|
const closeIdx = scanLinkClose(input, innerStart, memo);
|
|
2142
2201
|
if (closeIdx === -1) {
|
|
2143
2202
|
i = start + 2;
|
|
2144
|
-
textStart = start;
|
|
2145
2203
|
continue;
|
|
2146
2204
|
}
|
|
2147
|
-
|
|
2148
|
-
i = closeIdx + 2;
|
|
2149
|
-
const { display, target } = parseLink(inner);
|
|
2150
|
-
const linkToken = {
|
|
2205
|
+
emit(withSelectors({
|
|
2151
2206
|
type: "link",
|
|
2152
|
-
|
|
2153
|
-
target,
|
|
2207
|
+
...parseLink(input.slice(innerStart, closeIdx)),
|
|
2154
2208
|
start,
|
|
2155
|
-
end:
|
|
2156
|
-
};
|
|
2157
|
-
if (className) linkToken.className = className;
|
|
2158
|
-
if (id) linkToken.id = id;
|
|
2159
|
-
tokens.push(linkToken);
|
|
2160
|
-
textStart = i;
|
|
2209
|
+
end: closeIdx + 2
|
|
2210
|
+
}, selectors));
|
|
2161
2211
|
continue;
|
|
2162
2212
|
}
|
|
2163
2213
|
if (input[i] === "{") {
|
|
2164
2214
|
const start = i;
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
if (
|
|
2177
|
-
i = afterSelectors + 1;
|
|
2178
|
-
const nameStart = i;
|
|
2179
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2180
|
-
const name = input.slice(nameStart, i);
|
|
2181
|
-
if (input[i] === "}") {
|
|
2182
|
-
i++;
|
|
2183
|
-
const token = {
|
|
2184
|
-
type: "variable",
|
|
2185
|
-
name,
|
|
2186
|
-
scope: "variable",
|
|
2187
|
-
start,
|
|
2188
|
-
end: i
|
|
2189
|
-
};
|
|
2190
|
-
if (className) token.className = className;
|
|
2191
|
-
if (id) token.id = id;
|
|
2192
|
-
tokens.push(token);
|
|
2193
|
-
textStart = i;
|
|
2194
|
-
continue;
|
|
2195
|
-
}
|
|
2196
|
-
const closeIdx$ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2197
|
-
if (closeIdx$ !== -1) {
|
|
2198
|
-
const expression = input.slice(afterSelectors, closeIdx$);
|
|
2199
|
-
i = closeIdx$ + 1;
|
|
2200
|
-
const token = {
|
|
2201
|
-
type: "expression",
|
|
2202
|
-
expression,
|
|
2203
|
-
start,
|
|
2204
|
-
end: i
|
|
2205
|
-
};
|
|
2206
|
-
if (className) token.className = className;
|
|
2207
|
-
if (id) token.id = id;
|
|
2208
|
-
tokens.push(token);
|
|
2209
|
-
textStart = i;
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
i = start + 1;
|
|
2213
|
-
textStart = start;
|
|
2214
|
-
continue;
|
|
2215
|
-
}
|
|
2216
|
-
if (charAfter === "_") {
|
|
2217
|
-
i = afterSelectors + 1;
|
|
2218
|
-
const nameStart = i;
|
|
2219
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2220
|
-
const name = input.slice(nameStart, i);
|
|
2221
|
-
if (input[i] === "}") {
|
|
2222
|
-
i++;
|
|
2223
|
-
const token = {
|
|
2224
|
-
type: "variable",
|
|
2225
|
-
name,
|
|
2226
|
-
scope: "temporary",
|
|
2227
|
-
start,
|
|
2228
|
-
end: i
|
|
2229
|
-
};
|
|
2230
|
-
if (className) token.className = className;
|
|
2231
|
-
if (id) token.id = id;
|
|
2232
|
-
tokens.push(token);
|
|
2233
|
-
textStart = i;
|
|
2234
|
-
continue;
|
|
2235
|
-
}
|
|
2236
|
-
const closeIdx_ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2237
|
-
if (closeIdx_ !== -1) {
|
|
2238
|
-
const expression = input.slice(afterSelectors, closeIdx_);
|
|
2239
|
-
i = closeIdx_ + 1;
|
|
2240
|
-
const token = {
|
|
2241
|
-
type: "expression",
|
|
2242
|
-
expression,
|
|
2243
|
-
start,
|
|
2244
|
-
end: i
|
|
2245
|
-
};
|
|
2246
|
-
if (className) token.className = className;
|
|
2247
|
-
if (id) token.id = id;
|
|
2248
|
-
tokens.push(token);
|
|
2249
|
-
textStart = i;
|
|
2250
|
-
continue;
|
|
2251
|
-
}
|
|
2252
|
-
i = start + 1;
|
|
2253
|
-
textStart = start;
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
if (charAfter === "@") {
|
|
2257
|
-
i = afterSelectors + 1;
|
|
2258
|
-
const nameStart = i;
|
|
2259
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2260
|
-
const name = input.slice(nameStart, i);
|
|
2261
|
-
if (input[i] === "}") {
|
|
2262
|
-
i++;
|
|
2263
|
-
const token = {
|
|
2264
|
-
type: "variable",
|
|
2265
|
-
name,
|
|
2266
|
-
scope: "local",
|
|
2267
|
-
start,
|
|
2268
|
-
end: i
|
|
2269
|
-
};
|
|
2270
|
-
if (className) token.className = className;
|
|
2271
|
-
if (id) token.id = id;
|
|
2272
|
-
tokens.push(token);
|
|
2273
|
-
textStart = i;
|
|
2274
|
-
continue;
|
|
2275
|
-
}
|
|
2276
|
-
const closeIdx_at = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2277
|
-
if (closeIdx_at !== -1) {
|
|
2278
|
-
const expression = input.slice(afterSelectors, closeIdx_at);
|
|
2279
|
-
i = closeIdx_at + 1;
|
|
2280
|
-
const token = {
|
|
2281
|
-
type: "expression",
|
|
2282
|
-
expression,
|
|
2283
|
-
start,
|
|
2284
|
-
end: i
|
|
2285
|
-
};
|
|
2286
|
-
if (className) token.className = className;
|
|
2287
|
-
if (id) token.id = id;
|
|
2288
|
-
tokens.push(token);
|
|
2289
|
-
textStart = i;
|
|
2290
|
-
continue;
|
|
2291
|
-
}
|
|
2292
|
-
i = start + 1;
|
|
2293
|
-
textStart = start;
|
|
2294
|
-
continue;
|
|
2295
|
-
}
|
|
2296
|
-
if (charAfter === "%") {
|
|
2297
|
-
i = afterSelectors + 1;
|
|
2298
|
-
const nameStart = i;
|
|
2299
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2300
|
-
const name = input.slice(nameStart, i);
|
|
2301
|
-
if (input[i] === "}") {
|
|
2302
|
-
i++;
|
|
2303
|
-
const token = {
|
|
2304
|
-
type: "variable",
|
|
2305
|
-
name,
|
|
2306
|
-
scope: "transient",
|
|
2307
|
-
start,
|
|
2308
|
-
end: i
|
|
2309
|
-
};
|
|
2310
|
-
if (className) token.className = className;
|
|
2311
|
-
if (id) token.id = id;
|
|
2312
|
-
tokens.push(token);
|
|
2313
|
-
textStart = i;
|
|
2314
|
-
continue;
|
|
2315
|
-
}
|
|
2316
|
-
const closeIdx_pct = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2317
|
-
if (closeIdx_pct !== -1) {
|
|
2318
|
-
const expression = input.slice(afterSelectors, closeIdx_pct);
|
|
2319
|
-
i = closeIdx_pct + 1;
|
|
2320
|
-
const token = {
|
|
2321
|
-
type: "expression",
|
|
2322
|
-
expression,
|
|
2323
|
-
start,
|
|
2324
|
-
end: i
|
|
2325
|
-
};
|
|
2326
|
-
if (className) token.className = className;
|
|
2327
|
-
if (id) token.id = id;
|
|
2328
|
-
tokens.push(token);
|
|
2329
|
-
textStart = i;
|
|
2330
|
-
continue;
|
|
2331
|
-
}
|
|
2332
|
-
i = start + 1;
|
|
2333
|
-
textStart = start;
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
if (EXPRESSION_START.has(charAfter) && pushExpression(afterSelectors, start, false, className, id)) continue;
|
|
2337
|
-
if (charAfter !== void 0 && /[a-zA-Z]/.test(charAfter)) {
|
|
2338
|
-
const contentStart = afterSelectors;
|
|
2339
|
-
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
2340
|
-
if (closeIdx === -1) {
|
|
2341
|
-
i = start + 1;
|
|
2342
|
-
textStart = start;
|
|
2343
|
-
continue;
|
|
2344
|
-
}
|
|
2345
|
-
const content = input.slice(contentStart, closeIdx);
|
|
2346
|
-
i = closeIdx + 1;
|
|
2347
|
-
const { name, rawArgs, isClose } = parseMacroContent(content);
|
|
2348
|
-
const token = {
|
|
2349
|
-
type: "macro",
|
|
2350
|
-
name,
|
|
2351
|
-
rawArgs,
|
|
2352
|
-
isClose,
|
|
2353
|
-
start,
|
|
2354
|
-
end: i
|
|
2355
|
-
};
|
|
2356
|
-
if (className) token.className = className;
|
|
2357
|
-
if (id) token.id = id;
|
|
2358
|
-
tokens.push(token);
|
|
2359
|
-
textStart = i;
|
|
2360
|
-
consumeRawBody(name, isClose);
|
|
2361
|
-
continue;
|
|
2362
|
-
}
|
|
2363
|
-
i = start + 1;
|
|
2364
|
-
textStart = start;
|
|
2365
|
-
continue;
|
|
2366
|
-
}
|
|
2367
|
-
if (nextChar === "$") {
|
|
2368
|
-
flushText(i);
|
|
2369
|
-
i += 2;
|
|
2370
|
-
const nameStart = i;
|
|
2371
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2372
|
-
const name = input.slice(nameStart, i);
|
|
2373
|
-
if (input[i] === "}") {
|
|
2374
|
-
i++;
|
|
2375
|
-
tokens.push({
|
|
2376
|
-
type: "variable",
|
|
2377
|
-
name,
|
|
2378
|
-
scope: "variable",
|
|
2379
|
-
start,
|
|
2380
|
-
end: i
|
|
2381
|
-
});
|
|
2382
|
-
textStart = i;
|
|
2383
|
-
continue;
|
|
2384
|
-
}
|
|
2385
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2386
|
-
if (closeIdx !== -1) {
|
|
2387
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2388
|
-
i = closeIdx + 1;
|
|
2389
|
-
tokens.push({
|
|
2390
|
-
type: "expression",
|
|
2391
|
-
expression,
|
|
2392
|
-
start,
|
|
2393
|
-
end: i
|
|
2394
|
-
});
|
|
2395
|
-
textStart = i;
|
|
2396
|
-
continue;
|
|
2397
|
-
}
|
|
2398
|
-
i = start + 1;
|
|
2399
|
-
textStart = start;
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
if (nextChar === "_") {
|
|
2403
|
-
flushText(i);
|
|
2404
|
-
i += 2;
|
|
2405
|
-
const nameStart = i;
|
|
2406
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2407
|
-
const name = input.slice(nameStart, i);
|
|
2408
|
-
if (input[i] === "}") {
|
|
2409
|
-
i++;
|
|
2410
|
-
tokens.push({
|
|
2411
|
-
type: "variable",
|
|
2412
|
-
name,
|
|
2413
|
-
scope: "temporary",
|
|
2414
|
-
start,
|
|
2415
|
-
end: i
|
|
2416
|
-
});
|
|
2417
|
-
textStart = i;
|
|
2418
|
-
continue;
|
|
2419
|
-
}
|
|
2420
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2421
|
-
if (closeIdx !== -1) {
|
|
2422
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2423
|
-
i = closeIdx + 1;
|
|
2424
|
-
tokens.push({
|
|
2425
|
-
type: "expression",
|
|
2426
|
-
expression,
|
|
2427
|
-
start,
|
|
2428
|
-
end: i
|
|
2429
|
-
});
|
|
2430
|
-
textStart = i;
|
|
2431
|
-
continue;
|
|
2432
|
-
}
|
|
2433
|
-
i = start + 1;
|
|
2434
|
-
textStart = start;
|
|
2435
|
-
continue;
|
|
2436
|
-
}
|
|
2437
|
-
if (nextChar === "@") {
|
|
2438
|
-
flushText(i);
|
|
2439
|
-
i += 2;
|
|
2440
|
-
const nameStart = i;
|
|
2441
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2442
|
-
const name = input.slice(nameStart, i);
|
|
2443
|
-
if (input[i] === "}") {
|
|
2444
|
-
i++;
|
|
2445
|
-
tokens.push({
|
|
2446
|
-
type: "variable",
|
|
2447
|
-
name,
|
|
2448
|
-
scope: "local",
|
|
2449
|
-
start,
|
|
2450
|
-
end: i
|
|
2451
|
-
});
|
|
2452
|
-
textStart = i;
|
|
2453
|
-
continue;
|
|
2454
|
-
}
|
|
2455
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2456
|
-
if (closeIdx !== -1) {
|
|
2457
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2458
|
-
i = closeIdx + 1;
|
|
2459
|
-
tokens.push({
|
|
2460
|
-
type: "expression",
|
|
2461
|
-
expression,
|
|
2462
|
-
start,
|
|
2463
|
-
end: i
|
|
2464
|
-
});
|
|
2465
|
-
textStart = i;
|
|
2466
|
-
continue;
|
|
2467
|
-
}
|
|
2468
|
-
i = start + 1;
|
|
2469
|
-
textStart = start;
|
|
2470
|
-
continue;
|
|
2471
|
-
}
|
|
2472
|
-
if (nextChar === "%") {
|
|
2473
|
-
flushText(i);
|
|
2474
|
-
i += 2;
|
|
2475
|
-
const nameStart = i;
|
|
2476
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2477
|
-
const name = input.slice(nameStart, i);
|
|
2478
|
-
if (input[i] === "}") {
|
|
2479
|
-
i++;
|
|
2480
|
-
tokens.push({
|
|
2481
|
-
type: "variable",
|
|
2482
|
-
name,
|
|
2483
|
-
scope: "transient",
|
|
2484
|
-
start,
|
|
2485
|
-
end: i
|
|
2486
|
-
});
|
|
2487
|
-
textStart = i;
|
|
2488
|
-
continue;
|
|
2489
|
-
}
|
|
2490
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2491
|
-
if (closeIdx !== -1) {
|
|
2492
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2493
|
-
i = closeIdx + 1;
|
|
2494
|
-
tokens.push({
|
|
2495
|
-
type: "expression",
|
|
2496
|
-
expression,
|
|
2497
|
-
start,
|
|
2498
|
-
end: i
|
|
2499
|
-
});
|
|
2500
|
-
textStart = i;
|
|
2501
|
-
continue;
|
|
2502
|
-
}
|
|
2503
|
-
i = start + 1;
|
|
2504
|
-
textStart = start;
|
|
2505
|
-
continue;
|
|
2506
|
-
}
|
|
2507
|
-
if (EXPRESSION_START.has(nextChar)) {
|
|
2508
|
-
if (pushExpression(i + 1, start, true)) continue;
|
|
2509
|
-
i++;
|
|
2510
|
-
continue;
|
|
2511
|
-
}
|
|
2512
|
-
if (nextChar !== void 0 && (nextChar === "/" || /[a-zA-Z]/.test(nextChar))) {
|
|
2513
|
-
flushText(i);
|
|
2514
|
-
const contentStart = i + 1;
|
|
2515
|
-
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
2516
|
-
if (closeIdx === -1) {
|
|
2517
|
-
i = start + 1;
|
|
2518
|
-
textStart = start;
|
|
2519
|
-
continue;
|
|
2520
|
-
}
|
|
2521
|
-
const content = input.slice(contentStart, closeIdx);
|
|
2522
|
-
i = closeIdx + 1;
|
|
2523
|
-
const { name, rawArgs, isClose } = parseMacroContent(content);
|
|
2524
|
-
tokens.push({
|
|
2525
|
-
type: "macro",
|
|
2526
|
-
name,
|
|
2527
|
-
rawArgs,
|
|
2528
|
-
isClose,
|
|
2529
|
-
start,
|
|
2530
|
-
end: i
|
|
2531
|
-
});
|
|
2532
|
-
textStart = i;
|
|
2533
|
-
consumeRawBody(name, isClose);
|
|
2534
|
-
continue;
|
|
2215
|
+
const { selectors, end: at } = parseSelectors(input, i + 1);
|
|
2216
|
+
const prefixed = at > i + 1;
|
|
2217
|
+
if (prefixed) flushText(start);
|
|
2218
|
+
const c = input[at];
|
|
2219
|
+
if (isSigil(c)) {
|
|
2220
|
+
flushText(start);
|
|
2221
|
+
if (pushVariable(at, start, selectors)) continue;
|
|
2222
|
+
} else if (EXPRESSION_START.has(c)) {
|
|
2223
|
+
if (pushExpression(at, start, selectors)) continue;
|
|
2224
|
+
} else if (c !== void 0 && (/[a-zA-Z]/.test(c) || c === "/" && !prefixed)) {
|
|
2225
|
+
flushText(start);
|
|
2226
|
+
if (pushMacro(at, start, selectors)) continue;
|
|
2535
2227
|
}
|
|
2536
|
-
i
|
|
2228
|
+
i = start + 1;
|
|
2537
2229
|
continue;
|
|
2538
2230
|
}
|
|
2539
2231
|
if (!textMode && input[i] === "<") {
|
|
@@ -2551,11 +2243,10 @@ function tokenize(input, options = {}) {
|
|
|
2551
2243
|
j++;
|
|
2552
2244
|
flushText(start);
|
|
2553
2245
|
if (HTML_VOID_TAGS.has(tagLower)) {
|
|
2554
|
-
textStart = j;
|
|
2555
|
-
i = j;
|
|
2246
|
+
i = textStart = j;
|
|
2556
2247
|
continue;
|
|
2557
2248
|
}
|
|
2558
|
-
|
|
2249
|
+
emit({
|
|
2559
2250
|
type: "html",
|
|
2560
2251
|
tag,
|
|
2561
2252
|
attributes: {},
|
|
@@ -2564,8 +2255,6 @@ function tokenize(input, options = {}) {
|
|
|
2564
2255
|
start,
|
|
2565
2256
|
end: j
|
|
2566
2257
|
});
|
|
2567
|
-
textStart = j;
|
|
2568
|
-
i = j;
|
|
2569
2258
|
continue;
|
|
2570
2259
|
}
|
|
2571
2260
|
} else {
|
|
@@ -2579,7 +2268,7 @@ function tokenize(input, options = {}) {
|
|
|
2579
2268
|
if (input[j] === ">") {
|
|
2580
2269
|
j++;
|
|
2581
2270
|
flushText(start);
|
|
2582
|
-
|
|
2271
|
+
emit({
|
|
2583
2272
|
type: "html",
|
|
2584
2273
|
tag,
|
|
2585
2274
|
attributes: parseHtmlAttributes(input, attrsStart, memo).attributes,
|
|
@@ -2588,8 +2277,6 @@ function tokenize(input, options = {}) {
|
|
|
2588
2277
|
start,
|
|
2589
2278
|
end: j
|
|
2590
2279
|
});
|
|
2591
|
-
textStart = j;
|
|
2592
|
-
i = j;
|
|
2593
2280
|
continue;
|
|
2594
2281
|
}
|
|
2595
2282
|
}
|
|
@@ -2645,6 +2332,21 @@ var BRANCHING_BLOCK_MACROS = new Set([
|
|
|
2645
2332
|
function quoteArg(value) {
|
|
2646
2333
|
return `"${value.replace(/[\\"]/g, "\\$&")}"`;
|
|
2647
2334
|
}
|
|
2335
|
+
/** A new branch for the macro token opening it, with its selectors. */
|
|
2336
|
+
function newBranch(token) {
|
|
2337
|
+
return withSelectors({
|
|
2338
|
+
rawArgs: token.rawArgs,
|
|
2339
|
+
children: []
|
|
2340
|
+
}, token);
|
|
2341
|
+
}
|
|
2342
|
+
/** The name of a macro or HTML element. */
|
|
2343
|
+
function tagName(tagged) {
|
|
2344
|
+
return tagged.type === "html" ? tagged.tag : tagged.name;
|
|
2345
|
+
}
|
|
2346
|
+
/** The tag closing a macro or HTML element, as written in markup. */
|
|
2347
|
+
function closerOf(tagged) {
|
|
2348
|
+
return tagged.type === "html" ? `</${tagged.tag}>` : `{/${tagged.name}}`;
|
|
2349
|
+
}
|
|
2648
2350
|
/**
|
|
2649
2351
|
* Build an AST from a token array. Block macros are nested into trees
|
|
2650
2352
|
* using a stack. Throws on unclosed or mismatched macros.
|
|
@@ -2658,6 +2360,18 @@ function buildAST(tokens) {
|
|
|
2658
2360
|
if (top.type === "macro" && top.branches && top.branches.length > 0) return top.branches[top.branches.length - 1].children;
|
|
2659
2361
|
return top.children;
|
|
2660
2362
|
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Pop the node on top of the stack, which the closing `token` (`</b>`,
|
|
2365
|
+
* `{/if}`) must close. Names match ignoring case.
|
|
2366
|
+
*/
|
|
2367
|
+
function close(token) {
|
|
2368
|
+
const found = closerOf(token);
|
|
2369
|
+
if (stack.length === 0) throw new Error(`Unexpected closing ${found} (at character ${token.start})`);
|
|
2370
|
+
const top = stack[stack.length - 1];
|
|
2371
|
+
if (top.node.type !== token.type || tagName(top.node).toLowerCase() !== tagName(token).toLowerCase()) throw new Error(`Expected ${closerOf(top.node)} but found ${found} (at character ${token.start})`);
|
|
2372
|
+
stack.pop();
|
|
2373
|
+
current().push(top.node);
|
|
2374
|
+
}
|
|
2661
2375
|
for (const token of tokens) switch (token.type) {
|
|
2662
2376
|
case "text":
|
|
2663
2377
|
current().push({
|
|
@@ -2666,133 +2380,72 @@ function buildAST(tokens) {
|
|
|
2666
2380
|
});
|
|
2667
2381
|
break;
|
|
2668
2382
|
case "link": {
|
|
2669
|
-
const
|
|
2383
|
+
const rawArgs = `${quoteArg(token.display)} ${quoteArg(token.target)}`;
|
|
2384
|
+
current().push(withSelectors({
|
|
2670
2385
|
type: "macro",
|
|
2671
2386
|
name: "link",
|
|
2672
|
-
rawArgs
|
|
2387
|
+
rawArgs,
|
|
2673
2388
|
children: []
|
|
2674
|
-
};
|
|
2675
|
-
if (token.className) macroNode.className = token.className;
|
|
2676
|
-
if (token.id) macroNode.id = token.id;
|
|
2677
|
-
current().push(macroNode);
|
|
2389
|
+
}, token));
|
|
2678
2390
|
break;
|
|
2679
2391
|
}
|
|
2680
|
-
case "variable":
|
|
2681
|
-
|
|
2392
|
+
case "variable":
|
|
2393
|
+
current().push(withSelectors({
|
|
2682
2394
|
type: "variable",
|
|
2683
2395
|
name: token.name,
|
|
2684
2396
|
scope: token.scope
|
|
2685
|
-
};
|
|
2686
|
-
if (token.className) varNode.className = token.className;
|
|
2687
|
-
if (token.id) varNode.id = token.id;
|
|
2688
|
-
current().push(varNode);
|
|
2397
|
+
}, token));
|
|
2689
2398
|
break;
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
const exprNode = {
|
|
2399
|
+
case "expression":
|
|
2400
|
+
current().push(withSelectors({
|
|
2693
2401
|
type: "expression",
|
|
2694
2402
|
expression: token.expression
|
|
2695
|
-
};
|
|
2696
|
-
if (token.className) exprNode.className = token.className;
|
|
2697
|
-
if (token.id) exprNode.id = token.id;
|
|
2698
|
-
current().push(exprNode);
|
|
2403
|
+
}, token));
|
|
2699
2404
|
break;
|
|
2700
|
-
}
|
|
2701
2405
|
case "html": {
|
|
2702
|
-
if (token.isSelfClose) {
|
|
2703
|
-
current().push({
|
|
2704
|
-
type: "html",
|
|
2705
|
-
tag: token.tag,
|
|
2706
|
-
attributes: token.attributes,
|
|
2707
|
-
children: []
|
|
2708
|
-
});
|
|
2709
|
-
break;
|
|
2710
|
-
}
|
|
2711
|
-
if (token.isClose) {
|
|
2712
|
-
if (stack.length === 0) throw new Error(`Unexpected closing </${token.tag}> (at character ${token.start})`);
|
|
2713
|
-
const top = stack[stack.length - 1];
|
|
2714
|
-
if (top.node.type !== "html" || top.node.tag.toLowerCase() !== token.tag.toLowerCase()) {
|
|
2715
|
-
const expected = top.node.type === "html" ? `</${top.node.tag}>` : `{/${top.node.name}}`;
|
|
2716
|
-
throw new Error(`Expected ${expected} but found </${token.tag}> (at character ${token.start})`);
|
|
2717
|
-
}
|
|
2718
|
-
stack.pop();
|
|
2719
|
-
current().push(top.node);
|
|
2720
|
-
break;
|
|
2721
|
-
}
|
|
2722
2406
|
const htmlNode = {
|
|
2723
2407
|
type: "html",
|
|
2724
2408
|
tag: token.tag,
|
|
2725
2409
|
attributes: token.attributes,
|
|
2726
2410
|
children: []
|
|
2727
2411
|
};
|
|
2728
|
-
|
|
2412
|
+
if (token.isSelfClose) current().push(htmlNode);
|
|
2413
|
+
else if (token.isClose) close(token);
|
|
2414
|
+
else stack.push({
|
|
2729
2415
|
node: htmlNode,
|
|
2730
2416
|
start: token.start
|
|
2731
2417
|
});
|
|
2732
2418
|
break;
|
|
2733
2419
|
}
|
|
2734
2420
|
case "macro": {
|
|
2735
|
-
const name = token.name.toLowerCase();
|
|
2736
2421
|
if (token.isClose) {
|
|
2737
|
-
|
|
2738
|
-
const top = stack[stack.length - 1];
|
|
2739
|
-
if (top.node.type !== "macro" || top.node.name !== name) {
|
|
2740
|
-
const expected = top.node.type === "macro" ? `{/${top.node.name}}` : `</${top.node.tag}>`;
|
|
2741
|
-
throw new Error(`Expected ${expected} but found {/${token.name}} (at character ${token.start})`);
|
|
2742
|
-
}
|
|
2743
|
-
stack.pop();
|
|
2744
|
-
current().push(top.node);
|
|
2422
|
+
close(token);
|
|
2745
2423
|
break;
|
|
2746
2424
|
}
|
|
2425
|
+
const name = token.name.toLowerCase();
|
|
2747
2426
|
const expectedParent = Object.prototype.hasOwnProperty.call(BRANCH_PARENT, name) ? BRANCH_PARENT[name] : void 0;
|
|
2748
2427
|
if (expectedParent) {
|
|
2749
2428
|
const topNode = stack.length > 0 ? stack[stack.length - 1].node : null;
|
|
2750
2429
|
if (!topNode || topNode.type !== "macro" || topNode.name !== expectedParent) throw new Error(`{${token.name}} without matching {${expectedParent}} (at character ${token.start})`);
|
|
2751
|
-
|
|
2752
|
-
rawArgs: token.rawArgs,
|
|
2753
|
-
children: []
|
|
2754
|
-
};
|
|
2755
|
-
if (token.className) branch.className = token.className;
|
|
2756
|
-
if (token.id) branch.id = token.id;
|
|
2757
|
-
topNode.branches.push(branch);
|
|
2430
|
+
topNode.branches.push(newBranch(token));
|
|
2758
2431
|
break;
|
|
2759
2432
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
const firstBranch = {
|
|
2769
|
-
rawArgs: token.rawArgs,
|
|
2770
|
-
children: []
|
|
2771
|
-
};
|
|
2772
|
-
if (token.className) firstBranch.className = token.className;
|
|
2773
|
-
if (token.id) firstBranch.id = token.id;
|
|
2774
|
-
node.branches = [firstBranch];
|
|
2775
|
-
} else {
|
|
2776
|
-
if (token.className) node.className = token.className;
|
|
2777
|
-
if (token.id) node.id = token.id;
|
|
2778
|
-
}
|
|
2779
|
-
stack.push({
|
|
2780
|
-
node,
|
|
2781
|
-
start: token.start
|
|
2782
|
-
});
|
|
2433
|
+
const node = {
|
|
2434
|
+
type: "macro",
|
|
2435
|
+
name,
|
|
2436
|
+
rawArgs: token.rawArgs,
|
|
2437
|
+
children: []
|
|
2438
|
+
};
|
|
2439
|
+
if (!BLOCK_MACROS.has(name)) {
|
|
2440
|
+
current().push(withSelectors(node, token));
|
|
2783
2441
|
break;
|
|
2784
2442
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
};
|
|
2792
|
-
if (token.className) macroNode.className = token.className;
|
|
2793
|
-
if (token.id) macroNode.id = token.id;
|
|
2794
|
-
current().push(macroNode);
|
|
2795
|
-
}
|
|
2443
|
+
if (BRANCHING_BLOCK_MACROS.has(name)) node.branches = [newBranch(token)];
|
|
2444
|
+
else withSelectors(node, token);
|
|
2445
|
+
stack.push({
|
|
2446
|
+
node,
|
|
2447
|
+
start: token.start
|
|
2448
|
+
});
|
|
2796
2449
|
break;
|
|
2797
2450
|
}
|
|
2798
2451
|
default: throw new Error(`Unknown token type: ${token.type}`);
|
|
@@ -3463,12 +3116,6 @@ var MAX_DEPTH = 100;
|
|
|
3463
3116
|
function display(value) {
|
|
3464
3117
|
return value == null ? "" : String(value);
|
|
3465
3118
|
}
|
|
3466
|
-
var KIND_SIGILS = {
|
|
3467
|
-
variable: "$",
|
|
3468
|
-
temporary: "_",
|
|
3469
|
-
local: "@",
|
|
3470
|
-
transient: "%"
|
|
3471
|
-
};
|
|
3472
3119
|
/**
|
|
3473
3120
|
* The value of a variable reference (`name` without its sigil, with an
|
|
3474
3121
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -3478,7 +3125,7 @@ var KIND_SIGILS = {
|
|
|
3478
3125
|
function resolveVariable(scope, kind, name) {
|
|
3479
3126
|
const parts = name.split(".");
|
|
3480
3127
|
const root = parts[0];
|
|
3481
|
-
checkVariableName(root,
|
|
3128
|
+
checkVariableName(root, SCOPE_SIGILS[kind] + root);
|
|
3482
3129
|
let value = ownValue(kind === "variable" ? scope.variables : kind === "temporary" ? scope.temporary : kind === "transient" ? scope.transient : scope.locals, root);
|
|
3483
3130
|
for (let i = 1; i < parts.length; i++) {
|
|
3484
3131
|
if (value == null) return void 0;
|
|
@@ -3528,7 +3175,7 @@ var TextEvaluator = class {
|
|
|
3528
3175
|
return display(resolveVariable(scope, kind, name));
|
|
3529
3176
|
} catch (error) {
|
|
3530
3177
|
this.errors.push({
|
|
3531
|
-
macro:
|
|
3178
|
+
macro: SCOPE_SIGILS[kind] + name,
|
|
3532
3179
|
error
|
|
3533
3180
|
});
|
|
3534
3181
|
return "";
|
|
@@ -3636,12 +3283,6 @@ function interpolateText(template, scope) {
|
|
|
3636
3283
|
};
|
|
3637
3284
|
return renderText(parsed.nodes, scope);
|
|
3638
3285
|
}
|
|
3639
|
-
var SIGIL_KINDS = {
|
|
3640
|
-
$: "variable",
|
|
3641
|
-
_: "temporary",
|
|
3642
|
-
"@": "local",
|
|
3643
|
-
"%": "transient"
|
|
3644
|
-
};
|
|
3645
3286
|
/**
|
|
3646
3287
|
* Resolve the sigil references in the value of a code attribute (see
|
|
3647
3288
|
* isCodeAttribute): `{$x}`, `{_x.y}`, `{$n + 1}`. Everything else, other
|
|
@@ -3654,7 +3295,7 @@ function interpolateCode(template, scope, decode) {
|
|
|
3654
3295
|
for (const part of splitSigilTemplate(template)) if ("text" in part) text += decode ? decode(part.text) : part.text;
|
|
3655
3296
|
else if ("verbatim" in part) text += part.verbatim;
|
|
3656
3297
|
else if (/^[$_@%][\w.]+$/.test(part.expr)) {
|
|
3657
|
-
const kind =
|
|
3298
|
+
const kind = SIGIL_SCOPES[part.expr[0]];
|
|
3658
3299
|
try {
|
|
3659
3300
|
text += display(resolveVariable(scope, kind, part.expr.slice(1)));
|
|
3660
3301
|
} catch (error) {
|
|
@@ -8720,6 +8361,56 @@ function astContainsChildren(nodes) {
|
|
|
8720
8361
|
return false;
|
|
8721
8362
|
}
|
|
8722
8363
|
//#endregion
|
|
8364
|
+
//#region src/components/macros/Widget.tsx
|
|
8365
|
+
/**
|
|
8366
|
+
* Read the arguments of a {widget} definition: its name (quotes dropped)
|
|
8367
|
+
* and its parameters, the words after it that start with one of the
|
|
8368
|
+
* characters of `sigils`.
|
|
8369
|
+
*/
|
|
8370
|
+
function parseWidgetDef(rawArgs, sigils = "@") {
|
|
8371
|
+
const [first, ...rest] = rawArgs.trim().split(/\s+/);
|
|
8372
|
+
return {
|
|
8373
|
+
name: first.replace(/["']/g, ""),
|
|
8374
|
+
params: rest.filter((word) => sigils.includes(word[0]))
|
|
8375
|
+
};
|
|
8376
|
+
}
|
|
8377
|
+
defineMacro({
|
|
8378
|
+
name: "widget",
|
|
8379
|
+
block: true,
|
|
8380
|
+
render({ rawArgs, children = [] }, ctx) {
|
|
8381
|
+
let parsed;
|
|
8382
|
+
let error;
|
|
8383
|
+
try {
|
|
8384
|
+
const def = parseWidgetDef(rawArgs);
|
|
8385
|
+
for (const param of def.params) checkVariableName(param.slice(1), param);
|
|
8386
|
+
parsed = def;
|
|
8387
|
+
} catch (err) {
|
|
8388
|
+
error = err;
|
|
8389
|
+
}
|
|
8390
|
+
const { name, params } = parsed ?? {
|
|
8391
|
+
name: "",
|
|
8392
|
+
params: []
|
|
8393
|
+
};
|
|
8394
|
+
const childrenKey = JSON.stringify(children);
|
|
8395
|
+
const paramsKey = params.join(",");
|
|
8396
|
+
ctx.hooks.useLayoutEffect(() => {
|
|
8397
|
+
if (!parsed) return;
|
|
8398
|
+
const isBlock = astContainsChildren(children);
|
|
8399
|
+
registerWidget(name, children, params, isBlock);
|
|
8400
|
+
if (isBlock) registerBlockMacro(name);
|
|
8401
|
+
}, [
|
|
8402
|
+
name,
|
|
8403
|
+
childrenKey,
|
|
8404
|
+
paramsKey
|
|
8405
|
+
]);
|
|
8406
|
+
if (error) return /* @__PURE__ */ jsx(MacroError, {
|
|
8407
|
+
macro: "widget",
|
|
8408
|
+
error
|
|
8409
|
+
});
|
|
8410
|
+
return null;
|
|
8411
|
+
}
|
|
8412
|
+
});
|
|
8413
|
+
//#endregion
|
|
8723
8414
|
//#region src/components/macros/Set.tsx
|
|
8724
8415
|
defineMacro({
|
|
8725
8416
|
name: "set",
|
|
@@ -9514,52 +9205,6 @@ defineMacro({
|
|
|
9514
9205
|
}
|
|
9515
9206
|
});
|
|
9516
9207
|
//#endregion
|
|
9517
|
-
//#region src/components/macros/Widget.tsx
|
|
9518
|
-
function parseWidgetDef(rawArgs) {
|
|
9519
|
-
const tokens = rawArgs.trim().split(/\s+/);
|
|
9520
|
-
const name = tokens[0].replace(/["']/g, "");
|
|
9521
|
-
const params = tokens.slice(1).filter((t) => t.startsWith("@"));
|
|
9522
|
-
for (const param of params) checkVariableName(param.slice(1), param);
|
|
9523
|
-
return {
|
|
9524
|
-
name,
|
|
9525
|
-
params
|
|
9526
|
-
};
|
|
9527
|
-
}
|
|
9528
|
-
defineMacro({
|
|
9529
|
-
name: "widget",
|
|
9530
|
-
block: true,
|
|
9531
|
-
render({ rawArgs, children = [] }, ctx) {
|
|
9532
|
-
let parsed;
|
|
9533
|
-
let error;
|
|
9534
|
-
try {
|
|
9535
|
-
parsed = parseWidgetDef(rawArgs);
|
|
9536
|
-
} catch (err) {
|
|
9537
|
-
error = err;
|
|
9538
|
-
}
|
|
9539
|
-
const { name, params } = parsed ?? {
|
|
9540
|
-
name: "",
|
|
9541
|
-
params: []
|
|
9542
|
-
};
|
|
9543
|
-
const childrenKey = JSON.stringify(children);
|
|
9544
|
-
const paramsKey = params.join(",");
|
|
9545
|
-
ctx.hooks.useLayoutEffect(() => {
|
|
9546
|
-
if (!parsed) return;
|
|
9547
|
-
const isBlock = astContainsChildren(children);
|
|
9548
|
-
registerWidget(name, children, params, isBlock);
|
|
9549
|
-
if (isBlock) registerBlockMacro(name);
|
|
9550
|
-
}, [
|
|
9551
|
-
name,
|
|
9552
|
-
childrenKey,
|
|
9553
|
-
paramsKey
|
|
9554
|
-
]);
|
|
9555
|
-
if (error) return /* @__PURE__ */ jsx(MacroError, {
|
|
9556
|
-
macro: "widget",
|
|
9557
|
-
error
|
|
9558
|
-
});
|
|
9559
|
-
return null;
|
|
9560
|
-
}
|
|
9561
|
-
});
|
|
9562
|
-
//#endregion
|
|
9563
9208
|
//#region src/components/macros/StoryTitle.tsx
|
|
9564
9209
|
defineMacro({
|
|
9565
9210
|
name: "story-title",
|
|
@@ -11005,9 +10650,7 @@ function boot() {
|
|
|
11005
10650
|
for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
|
|
11006
10651
|
const widgetAST = buildAST(tokenize(passage.content));
|
|
11007
10652
|
for (const node of widgetAST) if (node.type === "macro" && node.name === "widget" && node.rawArgs) {
|
|
11008
|
-
const
|
|
11009
|
-
const widgetName = tokens2[0].replace(/["']/g, "");
|
|
11010
|
-
const params = tokens2.slice(1).filter((t) => t.startsWith("$") || t.startsWith("_") || t.startsWith("@"));
|
|
10653
|
+
const { name: widgetName, params } = parseWidgetDef(node.rawArgs, "$_@");
|
|
11011
10654
|
const children = node.children;
|
|
11012
10655
|
const isBlock = astContainsChildren(children);
|
|
11013
10656
|
try {
|