@rohal12/spindle 0.52.2 → 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 +430 -786
- package/dist/pkg/story-variables.js +179 -476
- package/package.json +1 -1
- package/src/components/App.tsx +6 -3
- package/src/components/macros/ExprDisplay.tsx +2 -7
- package/src/components/macros/MenubarButtons.tsx +13 -12
- package/src/components/macros/PassageDisplay.tsx +8 -4
- package/src/components/macros/SaveManager.tsx +8 -4
- package/src/components/macros/VarDisplay.tsx +11 -12
- package/src/components/macros/Widget.tsx +18 -7
- package/src/event-emitter.ts +15 -11
- package/src/expression.ts +44 -42
- package/src/hooks/use-merged-locals.ts +6 -4
- package/src/hooks/use-story-fields.ts +19 -0
- 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/src/settings.ts +20 -22
- package/src/store.ts +2 -1
- package/src/story-api.ts +29 -77
- package/src/story-variables.ts +5 -9
- package/src/utils/live-locals.ts +2 -4
- package/src/utils/namespace.ts +89 -25
- package/src/utils/object-path.ts +1 -3
- package/src/utils/counts.ts +0 -45
package/dist/pkg/headless.js
CHANGED
|
@@ -4,6 +4,7 @@ import { useSyncExternalStore } from "preact/compat";
|
|
|
4
4
|
import { createStore } from "zustand/vanilla";
|
|
5
5
|
import { immer } from "zustand/middleware/immer";
|
|
6
6
|
import { Immer, applyPatches, current, enableMapSet, enablePatches, freeze, isDraft, produceWithPatches } from "immer";
|
|
7
|
+
import { shallow } from "zustand/vanilla/shallow";
|
|
7
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "preact/jsx-runtime";
|
|
8
9
|
import { micromark } from "micromark";
|
|
9
10
|
import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
|
|
@@ -485,13 +486,13 @@ function scanRegex(src, start, cache) {
|
|
|
485
486
|
};
|
|
486
487
|
let i = start + 1;
|
|
487
488
|
while (i < src.length) {
|
|
489
|
+
const lineEnd = regexLineEnd(src, i);
|
|
490
|
+
if (lineEnd !== -1) return done(lineEnd, false);
|
|
488
491
|
const c = src.charAt(i);
|
|
489
492
|
if (c === "\\") {
|
|
490
|
-
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return done(i + 1, false);
|
|
491
493
|
i += 2;
|
|
492
494
|
continue;
|
|
493
495
|
}
|
|
494
|
-
if (LINE_TERMINATOR_RE.test(c)) return done(i, false);
|
|
495
496
|
if (c === "[") {
|
|
496
497
|
i = scanRegexClass(src, i, cache);
|
|
497
498
|
if (src.charAt(i) !== "]") return done(i, false);
|
|
@@ -522,16 +523,17 @@ function scanRegexClass(src, open, cache) {
|
|
|
522
523
|
const opens = [open];
|
|
523
524
|
let i = open + 1;
|
|
524
525
|
while (i < src.length) {
|
|
526
|
+
const lineEnd = regexLineEnd(src, i);
|
|
527
|
+
if (lineEnd !== -1) {
|
|
528
|
+
i = lineEnd;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
525
531
|
const c = src.charAt(i);
|
|
526
532
|
if (c === "\\") {
|
|
527
|
-
if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) {
|
|
528
|
-
i++;
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
533
|
i += 2;
|
|
532
534
|
continue;
|
|
533
535
|
}
|
|
534
|
-
if (c === "]"
|
|
536
|
+
if (c === "]") break;
|
|
535
537
|
if (c === "[" && cache) opens.push(i);
|
|
536
538
|
i++;
|
|
537
539
|
}
|
|
@@ -540,6 +542,16 @@ function scanRegexClass(src, open, cache) {
|
|
|
540
542
|
return i;
|
|
541
543
|
}
|
|
542
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
|
+
/**
|
|
543
555
|
* The next index from `from` on where `find` matches (-1 for none), for
|
|
544
556
|
* searches from increasing positions: a search from within the stretch the
|
|
545
557
|
* last one covered has the same answer.
|
|
@@ -1489,8 +1501,7 @@ function splitSigilTemplate(template) {
|
|
|
1489
1501
|
}
|
|
1490
1502
|
text += template.slice(i, brace);
|
|
1491
1503
|
i = brace + 1;
|
|
1492
|
-
const
|
|
1493
|
-
const end = sigil === "$" || sigil === "_" || sigil === "@" || sigil === "%" ? scanBalancedBrace(template, i, memo) : -1;
|
|
1504
|
+
const end = isSigil(template[i]) ? scanBalancedBrace(template, i, memo) : -1;
|
|
1494
1505
|
if (end === -1) {
|
|
1495
1506
|
text += "{";
|
|
1496
1507
|
continue;
|
|
@@ -1506,6 +1517,26 @@ function splitSigilTemplate(template) {
|
|
|
1506
1517
|
}
|
|
1507
1518
|
//#endregion
|
|
1508
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
|
+
}
|
|
1509
1540
|
/** Tag name must start with a letter (covers standard and custom elements). */
|
|
1510
1541
|
var VALID_TAG_START = /[a-zA-Z]/;
|
|
1511
1542
|
/** HTML void elements: never have children or a closing tag. */
|
|
@@ -1525,13 +1556,6 @@ var HTML_VOID_TAGS = new Set([
|
|
|
1525
1556
|
"track",
|
|
1526
1557
|
"wbr"
|
|
1527
1558
|
]);
|
|
1528
|
-
/** Variable sigils: story ($), temporary (_), local (@), transient (%). */
|
|
1529
|
-
var SIGIL_CHARS = new Set([
|
|
1530
|
-
"$",
|
|
1531
|
-
"_",
|
|
1532
|
-
"@",
|
|
1533
|
-
"%"
|
|
1534
|
-
]);
|
|
1535
1559
|
/**
|
|
1536
1560
|
* Characters other than a sigil that open an expression after `{`:
|
|
1537
1561
|
* `{(Math.max($a, 0))}`, `{!$done}`. Other characters that can start an
|
|
@@ -1542,25 +1566,32 @@ var EXPRESSION_START = new Set(["(", "!"]);
|
|
|
1542
1566
|
/** Macros whose body is JavaScript source, kept verbatim instead of tokenized. */
|
|
1543
1567
|
var RAW_BODY_MACROS = new Set(["do"]);
|
|
1544
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
|
+
/**
|
|
1545
1578
|
* Parse a Twine link interior into display and target.
|
|
1546
1579
|
* Supports: display|target, display->target, target<-display, plain
|
|
1547
1580
|
*/
|
|
1548
1581
|
function parseLink(inner) {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
display: inner.slice(revIdx + 2).trim()
|
|
1563
|
-
};
|
|
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
|
+
}
|
|
1564
1595
|
const trimmed = inner.trim();
|
|
1565
1596
|
return {
|
|
1566
1597
|
display: trimmed,
|
|
@@ -1591,8 +1622,10 @@ function parseMacroContent(content) {
|
|
|
1591
1622
|
}
|
|
1592
1623
|
/**
|
|
1593
1624
|
* Parse CSS selectors: .foo.bar#baz → { className: "foo bar", id: "baz" }
|
|
1594
|
-
* Scans .[a-zA-Z0-9_-]+ and #[a-zA-Z0-9_-]+ segments in any order
|
|
1595
|
-
* 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.
|
|
1596
1629
|
*/
|
|
1597
1630
|
function parseSelectors(input, startIdx) {
|
|
1598
1631
|
const classes = [];
|
|
@@ -1605,7 +1638,7 @@ function parseSelectors(input, startIdx) {
|
|
|
1605
1638
|
while (i < input.length) if (/[a-zA-Z0-9_-]/.test(input[i])) {
|
|
1606
1639
|
name += input[i];
|
|
1607
1640
|
i++;
|
|
1608
|
-
} else if (input[i] === "{" &&
|
|
1641
|
+
} else if (input[i] === "{" && isSigil(input[i + 1])) {
|
|
1609
1642
|
const braceStart = i;
|
|
1610
1643
|
i += 2;
|
|
1611
1644
|
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
@@ -1620,10 +1653,13 @@ function parseSelectors(input, startIdx) {
|
|
|
1620
1653
|
if (name) if (prefix === ".") classes.push(name);
|
|
1621
1654
|
else id = name;
|
|
1622
1655
|
}
|
|
1656
|
+
if (i > startIdx && input[i] === " ") i++;
|
|
1623
1657
|
return {
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1658
|
+
selectors: withSelectors({}, {
|
|
1659
|
+
className: classes.join(" "),
|
|
1660
|
+
id
|
|
1661
|
+
}),
|
|
1662
|
+
end: i
|
|
1627
1663
|
};
|
|
1628
1664
|
}
|
|
1629
1665
|
/**
|
|
@@ -1738,7 +1774,7 @@ function scanQuotedValue(input, j, quote, code, memo) {
|
|
|
1738
1774
|
continue;
|
|
1739
1775
|
}
|
|
1740
1776
|
if (input[j] === "{") {
|
|
1741
|
-
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;
|
|
1742
1778
|
if (closeIdx !== -1) {
|
|
1743
1779
|
j = closeIdx + 1;
|
|
1744
1780
|
checkpoint = true;
|
|
@@ -1814,12 +1850,7 @@ function scanMacroClose(input, contentStart, memo) {
|
|
|
1814
1850
|
* code from just past the {. Returns -1 if it is unclosed.
|
|
1815
1851
|
*/
|
|
1816
1852
|
function scanBlockClose(input, open, memo) {
|
|
1817
|
-
|
|
1818
|
-
const c = input[at];
|
|
1819
|
-
if (c === "." || c === "#") {
|
|
1820
|
-
at = parseSelectors(input, at).endIdx;
|
|
1821
|
-
if (input[at] === " ") at++;
|
|
1822
|
-
}
|
|
1853
|
+
const at = parseSelectors(input, open + 1).end;
|
|
1823
1854
|
const first = input[at];
|
|
1824
1855
|
if (first !== void 0 && (first === "/" || /[a-zA-Z]/.test(first))) return scanMacroClose(input, at, memo);
|
|
1825
1856
|
return scanBalancedBrace(input, at, memo);
|
|
@@ -2011,13 +2042,22 @@ function tokenize(input, options = {}) {
|
|
|
2011
2042
|
const memo = createScanMemo();
|
|
2012
2043
|
let i = 0;
|
|
2013
2044
|
let textStart = 0;
|
|
2045
|
+
/** Push a text token for the text not yet tokenized before `end`. */
|
|
2014
2046
|
function flushText(end) {
|
|
2015
|
-
if (end > textStart)
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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;
|
|
2021
2061
|
}
|
|
2022
2062
|
/**
|
|
2023
2063
|
* After an opening raw-body macro ({do}), emit everything up to its
|
|
@@ -2059,34 +2099,66 @@ function tokenize(input, options = {}) {
|
|
|
2059
2099
|
atRe.lastIndex = closeStart;
|
|
2060
2100
|
const closeEnd = closeStart + atRe.exec(input)[0].length;
|
|
2061
2101
|
flushText(closeStart);
|
|
2062
|
-
|
|
2102
|
+
emit({
|
|
2063
2103
|
type: "macro",
|
|
2064
2104
|
...parseMacroContent(input.slice(closeStart + 1, closeEnd - 1)),
|
|
2065
2105
|
start: closeStart,
|
|
2066
2106
|
end: closeEnd
|
|
2067
2107
|
});
|
|
2068
|
-
i = closeEnd;
|
|
2069
|
-
textStart = closeEnd;
|
|
2070
2108
|
}
|
|
2071
2109
|
/**
|
|
2072
2110
|
* Push an expression token for the `{…}` block opened at `start` whose
|
|
2073
|
-
* expression starts at `exprStart`, flushing the text before it first
|
|
2074
|
-
*
|
|
2111
|
+
* expression starts at `exprStart`, flushing the text before it first.
|
|
2112
|
+
* Returns false, consuming nothing, if the block is unclosed.
|
|
2075
2113
|
*/
|
|
2076
|
-
function pushExpression(exprStart, start,
|
|
2114
|
+
function pushExpression(exprStart, start, selectors) {
|
|
2077
2115
|
const closeIdx = scanBalancedBrace(input, exprStart, memo);
|
|
2078
2116
|
if (closeIdx === -1) return false;
|
|
2079
|
-
|
|
2080
|
-
|
|
2117
|
+
flushText(start);
|
|
2118
|
+
emit(withSelectors({
|
|
2081
2119
|
type: "expression",
|
|
2082
2120
|
expression: input.slice(exprStart, closeIdx),
|
|
2083
2121
|
start,
|
|
2084
2122
|
end: closeIdx + 1
|
|
2085
|
-
};
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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);
|
|
2090
2162
|
return true;
|
|
2091
2163
|
}
|
|
2092
2164
|
while (i < input.length) {
|
|
@@ -2110,14 +2182,12 @@ function tokenize(input, options = {}) {
|
|
|
2110
2182
|
}
|
|
2111
2183
|
if ((next === "{" || next === "}") && (k - i) % 2 === 1) {
|
|
2112
2184
|
flushText(k - 1);
|
|
2113
|
-
|
|
2185
|
+
emit({
|
|
2114
2186
|
type: "text",
|
|
2115
2187
|
value: next,
|
|
2116
2188
|
start: k - 1,
|
|
2117
2189
|
end: k + 1
|
|
2118
2190
|
});
|
|
2119
|
-
i = k + 1;
|
|
2120
|
-
textStart = i;
|
|
2121
2191
|
continue;
|
|
2122
2192
|
}
|
|
2123
2193
|
i = k;
|
|
@@ -2126,413 +2196,36 @@ function tokenize(input, options = {}) {
|
|
|
2126
2196
|
if (!textMode && input[i] === "[" && input[i + 1] === "[") {
|
|
2127
2197
|
flushText(i);
|
|
2128
2198
|
const start = i;
|
|
2129
|
-
i
|
|
2130
|
-
let className;
|
|
2131
|
-
let id;
|
|
2132
|
-
if (input[i] === "." || input[i] === "#") {
|
|
2133
|
-
const parsed = parseSelectors(input, i);
|
|
2134
|
-
className = parsed.className || void 0;
|
|
2135
|
-
id = parsed.id || void 0;
|
|
2136
|
-
i = parsed.endIdx;
|
|
2137
|
-
if (input[i] === " ") i++;
|
|
2138
|
-
}
|
|
2139
|
-
const innerStart = i;
|
|
2199
|
+
const { selectors, end: innerStart } = parseSelectors(input, i + 2);
|
|
2140
2200
|
const closeIdx = scanLinkClose(input, innerStart, memo);
|
|
2141
2201
|
if (closeIdx === -1) {
|
|
2142
2202
|
i = start + 2;
|
|
2143
|
-
textStart = start;
|
|
2144
2203
|
continue;
|
|
2145
2204
|
}
|
|
2146
|
-
|
|
2147
|
-
i = closeIdx + 2;
|
|
2148
|
-
const { display, target } = parseLink(inner);
|
|
2149
|
-
const linkToken = {
|
|
2205
|
+
emit(withSelectors({
|
|
2150
2206
|
type: "link",
|
|
2151
|
-
|
|
2152
|
-
target,
|
|
2207
|
+
...parseLink(input.slice(innerStart, closeIdx)),
|
|
2153
2208
|
start,
|
|
2154
|
-
end:
|
|
2155
|
-
};
|
|
2156
|
-
if (className) linkToken.className = className;
|
|
2157
|
-
if (id) linkToken.id = id;
|
|
2158
|
-
tokens.push(linkToken);
|
|
2159
|
-
textStart = i;
|
|
2209
|
+
end: closeIdx + 2
|
|
2210
|
+
}, selectors));
|
|
2160
2211
|
continue;
|
|
2161
2212
|
}
|
|
2162
2213
|
if (input[i] === "{") {
|
|
2163
2214
|
const start = i;
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
if (
|
|
2176
|
-
i = afterSelectors + 1;
|
|
2177
|
-
const nameStart = i;
|
|
2178
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2179
|
-
const name = input.slice(nameStart, i);
|
|
2180
|
-
if (input[i] === "}") {
|
|
2181
|
-
i++;
|
|
2182
|
-
const token = {
|
|
2183
|
-
type: "variable",
|
|
2184
|
-
name,
|
|
2185
|
-
scope: "variable",
|
|
2186
|
-
start,
|
|
2187
|
-
end: i
|
|
2188
|
-
};
|
|
2189
|
-
if (className) token.className = className;
|
|
2190
|
-
if (id) token.id = id;
|
|
2191
|
-
tokens.push(token);
|
|
2192
|
-
textStart = i;
|
|
2193
|
-
continue;
|
|
2194
|
-
}
|
|
2195
|
-
const closeIdx$ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2196
|
-
if (closeIdx$ !== -1) {
|
|
2197
|
-
const expression = input.slice(afterSelectors, closeIdx$);
|
|
2198
|
-
i = closeIdx$ + 1;
|
|
2199
|
-
const token = {
|
|
2200
|
-
type: "expression",
|
|
2201
|
-
expression,
|
|
2202
|
-
start,
|
|
2203
|
-
end: i
|
|
2204
|
-
};
|
|
2205
|
-
if (className) token.className = className;
|
|
2206
|
-
if (id) token.id = id;
|
|
2207
|
-
tokens.push(token);
|
|
2208
|
-
textStart = i;
|
|
2209
|
-
continue;
|
|
2210
|
-
}
|
|
2211
|
-
i = start + 1;
|
|
2212
|
-
textStart = start;
|
|
2213
|
-
continue;
|
|
2214
|
-
}
|
|
2215
|
-
if (charAfter === "_") {
|
|
2216
|
-
i = afterSelectors + 1;
|
|
2217
|
-
const nameStart = i;
|
|
2218
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2219
|
-
const name = input.slice(nameStart, i);
|
|
2220
|
-
if (input[i] === "}") {
|
|
2221
|
-
i++;
|
|
2222
|
-
const token = {
|
|
2223
|
-
type: "variable",
|
|
2224
|
-
name,
|
|
2225
|
-
scope: "temporary",
|
|
2226
|
-
start,
|
|
2227
|
-
end: i
|
|
2228
|
-
};
|
|
2229
|
-
if (className) token.className = className;
|
|
2230
|
-
if (id) token.id = id;
|
|
2231
|
-
tokens.push(token);
|
|
2232
|
-
textStart = i;
|
|
2233
|
-
continue;
|
|
2234
|
-
}
|
|
2235
|
-
const closeIdx_ = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2236
|
-
if (closeIdx_ !== -1) {
|
|
2237
|
-
const expression = input.slice(afterSelectors, closeIdx_);
|
|
2238
|
-
i = closeIdx_ + 1;
|
|
2239
|
-
const token = {
|
|
2240
|
-
type: "expression",
|
|
2241
|
-
expression,
|
|
2242
|
-
start,
|
|
2243
|
-
end: i
|
|
2244
|
-
};
|
|
2245
|
-
if (className) token.className = className;
|
|
2246
|
-
if (id) token.id = id;
|
|
2247
|
-
tokens.push(token);
|
|
2248
|
-
textStart = i;
|
|
2249
|
-
continue;
|
|
2250
|
-
}
|
|
2251
|
-
i = start + 1;
|
|
2252
|
-
textStart = start;
|
|
2253
|
-
continue;
|
|
2254
|
-
}
|
|
2255
|
-
if (charAfter === "@") {
|
|
2256
|
-
i = afterSelectors + 1;
|
|
2257
|
-
const nameStart = i;
|
|
2258
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2259
|
-
const name = input.slice(nameStart, i);
|
|
2260
|
-
if (input[i] === "}") {
|
|
2261
|
-
i++;
|
|
2262
|
-
const token = {
|
|
2263
|
-
type: "variable",
|
|
2264
|
-
name,
|
|
2265
|
-
scope: "local",
|
|
2266
|
-
start,
|
|
2267
|
-
end: i
|
|
2268
|
-
};
|
|
2269
|
-
if (className) token.className = className;
|
|
2270
|
-
if (id) token.id = id;
|
|
2271
|
-
tokens.push(token);
|
|
2272
|
-
textStart = i;
|
|
2273
|
-
continue;
|
|
2274
|
-
}
|
|
2275
|
-
const closeIdx_at = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2276
|
-
if (closeIdx_at !== -1) {
|
|
2277
|
-
const expression = input.slice(afterSelectors, closeIdx_at);
|
|
2278
|
-
i = closeIdx_at + 1;
|
|
2279
|
-
const token = {
|
|
2280
|
-
type: "expression",
|
|
2281
|
-
expression,
|
|
2282
|
-
start,
|
|
2283
|
-
end: i
|
|
2284
|
-
};
|
|
2285
|
-
if (className) token.className = className;
|
|
2286
|
-
if (id) token.id = id;
|
|
2287
|
-
tokens.push(token);
|
|
2288
|
-
textStart = i;
|
|
2289
|
-
continue;
|
|
2290
|
-
}
|
|
2291
|
-
i = start + 1;
|
|
2292
|
-
textStart = start;
|
|
2293
|
-
continue;
|
|
2294
|
-
}
|
|
2295
|
-
if (charAfter === "%") {
|
|
2296
|
-
i = afterSelectors + 1;
|
|
2297
|
-
const nameStart = i;
|
|
2298
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2299
|
-
const name = input.slice(nameStart, i);
|
|
2300
|
-
if (input[i] === "}") {
|
|
2301
|
-
i++;
|
|
2302
|
-
const token = {
|
|
2303
|
-
type: "variable",
|
|
2304
|
-
name,
|
|
2305
|
-
scope: "transient",
|
|
2306
|
-
start,
|
|
2307
|
-
end: i
|
|
2308
|
-
};
|
|
2309
|
-
if (className) token.className = className;
|
|
2310
|
-
if (id) token.id = id;
|
|
2311
|
-
tokens.push(token);
|
|
2312
|
-
textStart = i;
|
|
2313
|
-
continue;
|
|
2314
|
-
}
|
|
2315
|
-
const closeIdx_pct = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2316
|
-
if (closeIdx_pct !== -1) {
|
|
2317
|
-
const expression = input.slice(afterSelectors, closeIdx_pct);
|
|
2318
|
-
i = closeIdx_pct + 1;
|
|
2319
|
-
const token = {
|
|
2320
|
-
type: "expression",
|
|
2321
|
-
expression,
|
|
2322
|
-
start,
|
|
2323
|
-
end: i
|
|
2324
|
-
};
|
|
2325
|
-
if (className) token.className = className;
|
|
2326
|
-
if (id) token.id = id;
|
|
2327
|
-
tokens.push(token);
|
|
2328
|
-
textStart = i;
|
|
2329
|
-
continue;
|
|
2330
|
-
}
|
|
2331
|
-
i = start + 1;
|
|
2332
|
-
textStart = start;
|
|
2333
|
-
continue;
|
|
2334
|
-
}
|
|
2335
|
-
if (EXPRESSION_START.has(charAfter) && pushExpression(afterSelectors, start, false, className, id)) continue;
|
|
2336
|
-
if (charAfter !== void 0 && /[a-zA-Z]/.test(charAfter)) {
|
|
2337
|
-
const contentStart = afterSelectors;
|
|
2338
|
-
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
2339
|
-
if (closeIdx === -1) {
|
|
2340
|
-
i = start + 1;
|
|
2341
|
-
textStart = start;
|
|
2342
|
-
continue;
|
|
2343
|
-
}
|
|
2344
|
-
const content = input.slice(contentStart, closeIdx);
|
|
2345
|
-
i = closeIdx + 1;
|
|
2346
|
-
const { name, rawArgs, isClose } = parseMacroContent(content);
|
|
2347
|
-
const token = {
|
|
2348
|
-
type: "macro",
|
|
2349
|
-
name,
|
|
2350
|
-
rawArgs,
|
|
2351
|
-
isClose,
|
|
2352
|
-
start,
|
|
2353
|
-
end: i
|
|
2354
|
-
};
|
|
2355
|
-
if (className) token.className = className;
|
|
2356
|
-
if (id) token.id = id;
|
|
2357
|
-
tokens.push(token);
|
|
2358
|
-
textStart = i;
|
|
2359
|
-
consumeRawBody(name, isClose);
|
|
2360
|
-
continue;
|
|
2361
|
-
}
|
|
2362
|
-
i = start + 1;
|
|
2363
|
-
textStart = start;
|
|
2364
|
-
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;
|
|
2365
2227
|
}
|
|
2366
|
-
|
|
2367
|
-
flushText(i);
|
|
2368
|
-
i += 2;
|
|
2369
|
-
const nameStart = i;
|
|
2370
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2371
|
-
const name = input.slice(nameStart, i);
|
|
2372
|
-
if (input[i] === "}") {
|
|
2373
|
-
i++;
|
|
2374
|
-
tokens.push({
|
|
2375
|
-
type: "variable",
|
|
2376
|
-
name,
|
|
2377
|
-
scope: "variable",
|
|
2378
|
-
start,
|
|
2379
|
-
end: i
|
|
2380
|
-
});
|
|
2381
|
-
textStart = i;
|
|
2382
|
-
continue;
|
|
2383
|
-
}
|
|
2384
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2385
|
-
if (closeIdx !== -1) {
|
|
2386
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2387
|
-
i = closeIdx + 1;
|
|
2388
|
-
tokens.push({
|
|
2389
|
-
type: "expression",
|
|
2390
|
-
expression,
|
|
2391
|
-
start,
|
|
2392
|
-
end: i
|
|
2393
|
-
});
|
|
2394
|
-
textStart = i;
|
|
2395
|
-
continue;
|
|
2396
|
-
}
|
|
2397
|
-
i = start + 1;
|
|
2398
|
-
textStart = start;
|
|
2399
|
-
continue;
|
|
2400
|
-
}
|
|
2401
|
-
if (nextChar === "_") {
|
|
2402
|
-
flushText(i);
|
|
2403
|
-
i += 2;
|
|
2404
|
-
const nameStart = i;
|
|
2405
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2406
|
-
const name = input.slice(nameStart, i);
|
|
2407
|
-
if (input[i] === "}") {
|
|
2408
|
-
i++;
|
|
2409
|
-
tokens.push({
|
|
2410
|
-
type: "variable",
|
|
2411
|
-
name,
|
|
2412
|
-
scope: "temporary",
|
|
2413
|
-
start,
|
|
2414
|
-
end: i
|
|
2415
|
-
});
|
|
2416
|
-
textStart = i;
|
|
2417
|
-
continue;
|
|
2418
|
-
}
|
|
2419
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2420
|
-
if (closeIdx !== -1) {
|
|
2421
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2422
|
-
i = closeIdx + 1;
|
|
2423
|
-
tokens.push({
|
|
2424
|
-
type: "expression",
|
|
2425
|
-
expression,
|
|
2426
|
-
start,
|
|
2427
|
-
end: i
|
|
2428
|
-
});
|
|
2429
|
-
textStart = i;
|
|
2430
|
-
continue;
|
|
2431
|
-
}
|
|
2432
|
-
i = start + 1;
|
|
2433
|
-
textStart = start;
|
|
2434
|
-
continue;
|
|
2435
|
-
}
|
|
2436
|
-
if (nextChar === "@") {
|
|
2437
|
-
flushText(i);
|
|
2438
|
-
i += 2;
|
|
2439
|
-
const nameStart = i;
|
|
2440
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2441
|
-
const name = input.slice(nameStart, i);
|
|
2442
|
-
if (input[i] === "}") {
|
|
2443
|
-
i++;
|
|
2444
|
-
tokens.push({
|
|
2445
|
-
type: "variable",
|
|
2446
|
-
name,
|
|
2447
|
-
scope: "local",
|
|
2448
|
-
start,
|
|
2449
|
-
end: i
|
|
2450
|
-
});
|
|
2451
|
-
textStart = i;
|
|
2452
|
-
continue;
|
|
2453
|
-
}
|
|
2454
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2455
|
-
if (closeIdx !== -1) {
|
|
2456
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2457
|
-
i = closeIdx + 1;
|
|
2458
|
-
tokens.push({
|
|
2459
|
-
type: "expression",
|
|
2460
|
-
expression,
|
|
2461
|
-
start,
|
|
2462
|
-
end: i
|
|
2463
|
-
});
|
|
2464
|
-
textStart = i;
|
|
2465
|
-
continue;
|
|
2466
|
-
}
|
|
2467
|
-
i = start + 1;
|
|
2468
|
-
textStart = start;
|
|
2469
|
-
continue;
|
|
2470
|
-
}
|
|
2471
|
-
if (nextChar === "%") {
|
|
2472
|
-
flushText(i);
|
|
2473
|
-
i += 2;
|
|
2474
|
-
const nameStart = i;
|
|
2475
|
-
while (i < input.length && /[\w.]/.test(input[i])) i++;
|
|
2476
|
-
const name = input.slice(nameStart, i);
|
|
2477
|
-
if (input[i] === "}") {
|
|
2478
|
-
i++;
|
|
2479
|
-
tokens.push({
|
|
2480
|
-
type: "variable",
|
|
2481
|
-
name,
|
|
2482
|
-
scope: "transient",
|
|
2483
|
-
start,
|
|
2484
|
-
end: i
|
|
2485
|
-
});
|
|
2486
|
-
textStart = i;
|
|
2487
|
-
continue;
|
|
2488
|
-
}
|
|
2489
|
-
const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
|
|
2490
|
-
if (closeIdx !== -1) {
|
|
2491
|
-
const expression = input.slice(start + 1, closeIdx);
|
|
2492
|
-
i = closeIdx + 1;
|
|
2493
|
-
tokens.push({
|
|
2494
|
-
type: "expression",
|
|
2495
|
-
expression,
|
|
2496
|
-
start,
|
|
2497
|
-
end: i
|
|
2498
|
-
});
|
|
2499
|
-
textStart = i;
|
|
2500
|
-
continue;
|
|
2501
|
-
}
|
|
2502
|
-
i = start + 1;
|
|
2503
|
-
textStart = start;
|
|
2504
|
-
continue;
|
|
2505
|
-
}
|
|
2506
|
-
if (EXPRESSION_START.has(nextChar)) {
|
|
2507
|
-
if (pushExpression(i + 1, start, true)) continue;
|
|
2508
|
-
i++;
|
|
2509
|
-
continue;
|
|
2510
|
-
}
|
|
2511
|
-
if (nextChar !== void 0 && (nextChar === "/" || /[a-zA-Z]/.test(nextChar))) {
|
|
2512
|
-
flushText(i);
|
|
2513
|
-
const contentStart = i + 1;
|
|
2514
|
-
const closeIdx = scanMacroClose(input, contentStart, memo);
|
|
2515
|
-
if (closeIdx === -1) {
|
|
2516
|
-
i = start + 1;
|
|
2517
|
-
textStart = start;
|
|
2518
|
-
continue;
|
|
2519
|
-
}
|
|
2520
|
-
const content = input.slice(contentStart, closeIdx);
|
|
2521
|
-
i = closeIdx + 1;
|
|
2522
|
-
const { name, rawArgs, isClose } = parseMacroContent(content);
|
|
2523
|
-
tokens.push({
|
|
2524
|
-
type: "macro",
|
|
2525
|
-
name,
|
|
2526
|
-
rawArgs,
|
|
2527
|
-
isClose,
|
|
2528
|
-
start,
|
|
2529
|
-
end: i
|
|
2530
|
-
});
|
|
2531
|
-
textStart = i;
|
|
2532
|
-
consumeRawBody(name, isClose);
|
|
2533
|
-
continue;
|
|
2534
|
-
}
|
|
2535
|
-
i++;
|
|
2228
|
+
i = start + 1;
|
|
2536
2229
|
continue;
|
|
2537
2230
|
}
|
|
2538
2231
|
if (!textMode && input[i] === "<") {
|
|
@@ -2550,11 +2243,10 @@ function tokenize(input, options = {}) {
|
|
|
2550
2243
|
j++;
|
|
2551
2244
|
flushText(start);
|
|
2552
2245
|
if (HTML_VOID_TAGS.has(tagLower)) {
|
|
2553
|
-
textStart = j;
|
|
2554
|
-
i = j;
|
|
2246
|
+
i = textStart = j;
|
|
2555
2247
|
continue;
|
|
2556
2248
|
}
|
|
2557
|
-
|
|
2249
|
+
emit({
|
|
2558
2250
|
type: "html",
|
|
2559
2251
|
tag,
|
|
2560
2252
|
attributes: {},
|
|
@@ -2563,8 +2255,6 @@ function tokenize(input, options = {}) {
|
|
|
2563
2255
|
start,
|
|
2564
2256
|
end: j
|
|
2565
2257
|
});
|
|
2566
|
-
textStart = j;
|
|
2567
|
-
i = j;
|
|
2568
2258
|
continue;
|
|
2569
2259
|
}
|
|
2570
2260
|
} else {
|
|
@@ -2578,7 +2268,7 @@ function tokenize(input, options = {}) {
|
|
|
2578
2268
|
if (input[j] === ">") {
|
|
2579
2269
|
j++;
|
|
2580
2270
|
flushText(start);
|
|
2581
|
-
|
|
2271
|
+
emit({
|
|
2582
2272
|
type: "html",
|
|
2583
2273
|
tag,
|
|
2584
2274
|
attributes: parseHtmlAttributes(input, attrsStart, memo).attributes,
|
|
@@ -2587,8 +2277,6 @@ function tokenize(input, options = {}) {
|
|
|
2587
2277
|
start,
|
|
2588
2278
|
end: j
|
|
2589
2279
|
});
|
|
2590
|
-
textStart = j;
|
|
2591
|
-
i = j;
|
|
2592
2280
|
continue;
|
|
2593
2281
|
}
|
|
2594
2282
|
}
|
|
@@ -2644,6 +2332,21 @@ var BRANCHING_BLOCK_MACROS = new Set([
|
|
|
2644
2332
|
function quoteArg(value) {
|
|
2645
2333
|
return `"${value.replace(/[\\"]/g, "\\$&")}"`;
|
|
2646
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
|
+
}
|
|
2647
2350
|
/**
|
|
2648
2351
|
* Build an AST from a token array. Block macros are nested into trees
|
|
2649
2352
|
* using a stack. Throws on unclosed or mismatched macros.
|
|
@@ -2657,6 +2360,18 @@ function buildAST(tokens) {
|
|
|
2657
2360
|
if (top.type === "macro" && top.branches && top.branches.length > 0) return top.branches[top.branches.length - 1].children;
|
|
2658
2361
|
return top.children;
|
|
2659
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
|
+
}
|
|
2660
2375
|
for (const token of tokens) switch (token.type) {
|
|
2661
2376
|
case "text":
|
|
2662
2377
|
current().push({
|
|
@@ -2665,133 +2380,72 @@ function buildAST(tokens) {
|
|
|
2665
2380
|
});
|
|
2666
2381
|
break;
|
|
2667
2382
|
case "link": {
|
|
2668
|
-
const
|
|
2383
|
+
const rawArgs = `${quoteArg(token.display)} ${quoteArg(token.target)}`;
|
|
2384
|
+
current().push(withSelectors({
|
|
2669
2385
|
type: "macro",
|
|
2670
2386
|
name: "link",
|
|
2671
|
-
rawArgs
|
|
2387
|
+
rawArgs,
|
|
2672
2388
|
children: []
|
|
2673
|
-
};
|
|
2674
|
-
if (token.className) macroNode.className = token.className;
|
|
2675
|
-
if (token.id) macroNode.id = token.id;
|
|
2676
|
-
current().push(macroNode);
|
|
2389
|
+
}, token));
|
|
2677
2390
|
break;
|
|
2678
2391
|
}
|
|
2679
|
-
case "variable":
|
|
2680
|
-
|
|
2392
|
+
case "variable":
|
|
2393
|
+
current().push(withSelectors({
|
|
2681
2394
|
type: "variable",
|
|
2682
2395
|
name: token.name,
|
|
2683
2396
|
scope: token.scope
|
|
2684
|
-
};
|
|
2685
|
-
if (token.className) varNode.className = token.className;
|
|
2686
|
-
if (token.id) varNode.id = token.id;
|
|
2687
|
-
current().push(varNode);
|
|
2397
|
+
}, token));
|
|
2688
2398
|
break;
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
const exprNode = {
|
|
2399
|
+
case "expression":
|
|
2400
|
+
current().push(withSelectors({
|
|
2692
2401
|
type: "expression",
|
|
2693
2402
|
expression: token.expression
|
|
2694
|
-
};
|
|
2695
|
-
if (token.className) exprNode.className = token.className;
|
|
2696
|
-
if (token.id) exprNode.id = token.id;
|
|
2697
|
-
current().push(exprNode);
|
|
2403
|
+
}, token));
|
|
2698
2404
|
break;
|
|
2699
|
-
}
|
|
2700
2405
|
case "html": {
|
|
2701
|
-
if (token.isSelfClose) {
|
|
2702
|
-
current().push({
|
|
2703
|
-
type: "html",
|
|
2704
|
-
tag: token.tag,
|
|
2705
|
-
attributes: token.attributes,
|
|
2706
|
-
children: []
|
|
2707
|
-
});
|
|
2708
|
-
break;
|
|
2709
|
-
}
|
|
2710
|
-
if (token.isClose) {
|
|
2711
|
-
if (stack.length === 0) throw new Error(`Unexpected closing </${token.tag}> (at character ${token.start})`);
|
|
2712
|
-
const top = stack[stack.length - 1];
|
|
2713
|
-
if (top.node.type !== "html" || top.node.tag.toLowerCase() !== token.tag.toLowerCase()) {
|
|
2714
|
-
const expected = top.node.type === "html" ? `</${top.node.tag}>` : `{/${top.node.name}}`;
|
|
2715
|
-
throw new Error(`Expected ${expected} but found </${token.tag}> (at character ${token.start})`);
|
|
2716
|
-
}
|
|
2717
|
-
stack.pop();
|
|
2718
|
-
current().push(top.node);
|
|
2719
|
-
break;
|
|
2720
|
-
}
|
|
2721
2406
|
const htmlNode = {
|
|
2722
2407
|
type: "html",
|
|
2723
2408
|
tag: token.tag,
|
|
2724
2409
|
attributes: token.attributes,
|
|
2725
2410
|
children: []
|
|
2726
2411
|
};
|
|
2727
|
-
|
|
2412
|
+
if (token.isSelfClose) current().push(htmlNode);
|
|
2413
|
+
else if (token.isClose) close(token);
|
|
2414
|
+
else stack.push({
|
|
2728
2415
|
node: htmlNode,
|
|
2729
2416
|
start: token.start
|
|
2730
2417
|
});
|
|
2731
2418
|
break;
|
|
2732
2419
|
}
|
|
2733
2420
|
case "macro": {
|
|
2734
|
-
const name = token.name.toLowerCase();
|
|
2735
2421
|
if (token.isClose) {
|
|
2736
|
-
|
|
2737
|
-
const top = stack[stack.length - 1];
|
|
2738
|
-
if (top.node.type !== "macro" || top.node.name !== name) {
|
|
2739
|
-
const expected = top.node.type === "macro" ? `{/${top.node.name}}` : `</${top.node.tag}>`;
|
|
2740
|
-
throw new Error(`Expected ${expected} but found {/${token.name}} (at character ${token.start})`);
|
|
2741
|
-
}
|
|
2742
|
-
stack.pop();
|
|
2743
|
-
current().push(top.node);
|
|
2422
|
+
close(token);
|
|
2744
2423
|
break;
|
|
2745
2424
|
}
|
|
2425
|
+
const name = token.name.toLowerCase();
|
|
2746
2426
|
const expectedParent = Object.prototype.hasOwnProperty.call(BRANCH_PARENT, name) ? BRANCH_PARENT[name] : void 0;
|
|
2747
2427
|
if (expectedParent) {
|
|
2748
2428
|
const topNode = stack.length > 0 ? stack[stack.length - 1].node : null;
|
|
2749
2429
|
if (!topNode || topNode.type !== "macro" || topNode.name !== expectedParent) throw new Error(`{${token.name}} without matching {${expectedParent}} (at character ${token.start})`);
|
|
2750
|
-
|
|
2751
|
-
rawArgs: token.rawArgs,
|
|
2752
|
-
children: []
|
|
2753
|
-
};
|
|
2754
|
-
if (token.className) branch.className = token.className;
|
|
2755
|
-
if (token.id) branch.id = token.id;
|
|
2756
|
-
topNode.branches.push(branch);
|
|
2430
|
+
topNode.branches.push(newBranch(token));
|
|
2757
2431
|
break;
|
|
2758
2432
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
const firstBranch = {
|
|
2768
|
-
rawArgs: token.rawArgs,
|
|
2769
|
-
children: []
|
|
2770
|
-
};
|
|
2771
|
-
if (token.className) firstBranch.className = token.className;
|
|
2772
|
-
if (token.id) firstBranch.id = token.id;
|
|
2773
|
-
node.branches = [firstBranch];
|
|
2774
|
-
} else {
|
|
2775
|
-
if (token.className) node.className = token.className;
|
|
2776
|
-
if (token.id) node.id = token.id;
|
|
2777
|
-
}
|
|
2778
|
-
stack.push({
|
|
2779
|
-
node,
|
|
2780
|
-
start: token.start
|
|
2781
|
-
});
|
|
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));
|
|
2782
2441
|
break;
|
|
2783
2442
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
};
|
|
2791
|
-
if (token.className) macroNode.className = token.className;
|
|
2792
|
-
if (token.id) macroNode.id = token.id;
|
|
2793
|
-
current().push(macroNode);
|
|
2794
|
-
}
|
|
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
|
+
});
|
|
2795
2449
|
break;
|
|
2796
2450
|
}
|
|
2797
2451
|
default: throw new Error(`Unknown token type: ${token.type}`);
|
|
@@ -2804,15 +2458,28 @@ function buildAST(tokens) {
|
|
|
2804
2458
|
return root;
|
|
2805
2459
|
}
|
|
2806
2460
|
//#endregion
|
|
2461
|
+
//#region src/hooks/use-story-fields.ts
|
|
2462
|
+
/**
|
|
2463
|
+
* Story state fields `keys`, re-rendering when any of them changes (as one
|
|
2464
|
+
* useStoryStore() selector per field would). The result keeps its identity
|
|
2465
|
+
* while the fields don't change, as the store's snapshot must.
|
|
2466
|
+
*/
|
|
2467
|
+
function useStoryFields(...keys) {
|
|
2468
|
+
const prev = useRef();
|
|
2469
|
+
return useStoryStore((s) => {
|
|
2470
|
+
const next = {};
|
|
2471
|
+
for (const key of keys) next[key] = s[key];
|
|
2472
|
+
return shallow(prev.current, next) ? prev.current : prev.current = next;
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
//#endregion
|
|
2807
2476
|
//#region src/hooks/use-merged-locals.ts
|
|
2808
2477
|
/**
|
|
2809
2478
|
* Return store variables, temporary, locals from context, and transient.
|
|
2810
2479
|
* All four dicts use unprefixed keys suitable for evaluate/execute.
|
|
2811
2480
|
*/
|
|
2812
2481
|
function useMergedLocals() {
|
|
2813
|
-
const variables =
|
|
2814
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
2815
|
-
const transient = useStoryStore((s) => s.transient);
|
|
2482
|
+
const { variables, temporary, transient } = useStoryFields("variables", "temporary", "transient");
|
|
2816
2483
|
const localsValues = useContext(LocalsValuesContext);
|
|
2817
2484
|
return useMemo(() => {
|
|
2818
2485
|
return [
|
|
@@ -2957,21 +2624,38 @@ function getPRNGPull() {
|
|
|
2957
2624
|
/** The variable name no namespace can hold. */
|
|
2958
2625
|
var RESERVED_NAME = "__proto__";
|
|
2959
2626
|
/**
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2627
|
+
* Why `name` (without its sigil) cannot be a variable name, or undefined if
|
|
2628
|
+
* it can. `label` is the name as the author wrote it (e.g. `$__proto__`).
|
|
2962
2629
|
*/
|
|
2630
|
+
function variableNameError(name, label = name) {
|
|
2631
|
+
return name === "__proto__" ? `"${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
|
|
2632
|
+
}
|
|
2633
|
+
/** Throw a TypeError if `name` cannot be a variable name. */
|
|
2963
2634
|
function checkVariableName(name, label = name) {
|
|
2964
|
-
|
|
2635
|
+
const error = variableNameError(name, label);
|
|
2636
|
+
if (error) throw new TypeError(`spindle: ${error}`);
|
|
2965
2637
|
}
|
|
2966
|
-
/**
|
|
2967
|
-
|
|
2968
|
-
|
|
2638
|
+
/** Whether `obj` holds `key` as an own property. */
|
|
2639
|
+
var hasOwn$2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
2640
|
+
/** `ns[key]` if `ns` holds it as an own property, else undefined. */
|
|
2641
|
+
var ownValue = (ns, key) => ns && hasOwn$2(ns, key) ? ns[key] : void 0;
|
|
2642
|
+
/**
|
|
2643
|
+
* A record with no prototype holding the own entries of `sources`, or only
|
|
2644
|
+
* those whose value passes `keep`.
|
|
2645
|
+
*/
|
|
2646
|
+
function ownEntries(sources, keep = () => true) {
|
|
2647
|
+
const record = Object.create(null);
|
|
2969
2648
|
for (const source of sources) {
|
|
2970
2649
|
if (!source) continue;
|
|
2971
|
-
for (const key of Object.keys(source))
|
|
2650
|
+
for (const key of Object.keys(source)) {
|
|
2651
|
+
const value = source[key];
|
|
2652
|
+
if (keep(value)) record[key] = value;
|
|
2653
|
+
}
|
|
2972
2654
|
}
|
|
2973
|
-
return
|
|
2655
|
+
return record;
|
|
2974
2656
|
}
|
|
2657
|
+
/** A namespace with no prototype holding the own entries of `sources`. */
|
|
2658
|
+
var createNamespace = (...sources) => ownEntries(sources);
|
|
2975
2659
|
/** Whether `value` is an object with no prototype. */
|
|
2976
2660
|
var isNamespace = (value) => Object.getPrototypeOf(value) === null;
|
|
2977
2661
|
/** `ns` itself if it has no prototype, else a namespace copy of it. */
|
|
@@ -2984,15 +2668,14 @@ function withEntry(ns, key, value) {
|
|
|
2984
2668
|
}
|
|
2985
2669
|
/** An empty namespace that cannot change. */
|
|
2986
2670
|
var EMPTY_NAMESPACE = Object.freeze(createNamespace());
|
|
2987
|
-
/**
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2671
|
+
/** The names whose own entries differ between `a` and `b`, `a`'s first. */
|
|
2672
|
+
function changedNames(a, b) {
|
|
2673
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((name) => ownValue(a, name) !== ownValue(b, name));
|
|
2674
|
+
}
|
|
2991
2675
|
var isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
2992
2676
|
/** The count of `name` in `counts`: its own entry, or 0. */
|
|
2993
2677
|
function countOf(counts, name) {
|
|
2994
|
-
|
|
2995
|
-
const value = counts[name];
|
|
2678
|
+
const value = ownValue(counts, name);
|
|
2996
2679
|
return isCount(value) ? value : 0;
|
|
2997
2680
|
}
|
|
2998
2681
|
/**
|
|
@@ -3002,11 +2685,7 @@ function countOf(counts, name) {
|
|
|
3002
2685
|
* more than counting in place.
|
|
3003
2686
|
*/
|
|
3004
2687
|
function createCounts(source, name) {
|
|
3005
|
-
const counts =
|
|
3006
|
-
if (source) for (const key of Object.keys(source)) {
|
|
3007
|
-
const value = source[key];
|
|
3008
|
-
if (isCount(value)) counts[key] = value;
|
|
3009
|
-
}
|
|
2688
|
+
const counts = ownEntries([source], isCount);
|
|
3010
2689
|
if (name !== void 0) counts[name] = countOf(source, name) + 1;
|
|
3011
2690
|
return counts;
|
|
3012
2691
|
}
|
|
@@ -3045,7 +2724,8 @@ function transform(expr, goal = "expression") {
|
|
|
3045
2724
|
result += text;
|
|
3046
2725
|
},
|
|
3047
2726
|
variable(sigil, name) {
|
|
3048
|
-
|
|
2727
|
+
const error = variableNameError(name, sigil + name);
|
|
2728
|
+
if (error) throw new SyntaxError(`spindle: ${error}`);
|
|
3049
2729
|
if (IDENT_END_RE.test(result.slice(-2))) result += " ";
|
|
3050
2730
|
result += `${NAMESPACES$1[sigil]}["${name}"]`;
|
|
3051
2731
|
}
|
|
@@ -3068,41 +2748,50 @@ function getOrCompile(key, body) {
|
|
|
3068
2748
|
}
|
|
3069
2749
|
return fn;
|
|
3070
2750
|
}
|
|
2751
|
+
/** The passage being shown. */
|
|
2752
|
+
function currentPassage() {
|
|
2753
|
+
const s = useStoryStore.getState();
|
|
2754
|
+
return s.storyData?.passages.get(s.currentPassage);
|
|
2755
|
+
}
|
|
2756
|
+
/** The passage shown before the current one in history. */
|
|
2757
|
+
function previousPassage() {
|
|
2758
|
+
const s = useStoryStore.getState();
|
|
2759
|
+
if (s.historyIndex <= 0) return void 0;
|
|
2760
|
+
const prevName = s.history[s.historyIndex - 1]?.passage;
|
|
2761
|
+
return prevName ? s.storyData?.passages.get(prevName) : void 0;
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* visited()/rendered() and their has-forms, counting in the counters
|
|
2765
|
+
* `counts()` returns. A missing name means the current passage.
|
|
2766
|
+
*/
|
|
2767
|
+
function countQueries(counts) {
|
|
2768
|
+
const count = (name) => countOf(counts(), name ?? useStoryStore.getState().currentPassage);
|
|
2769
|
+
return {
|
|
2770
|
+
count,
|
|
2771
|
+
has: (name) => count(name) > 0,
|
|
2772
|
+
any: (...names) => names.some((n) => count(n) > 0),
|
|
2773
|
+
all: (...names) => names.every((n) => count(n) > 0)
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
3071
2776
|
var cachedFns = null;
|
|
3072
2777
|
var cachedVisitCounts = null;
|
|
3073
2778
|
var cachedRenderCounts = null;
|
|
3074
2779
|
function buildExpressionFns() {
|
|
3075
2780
|
const { visitCounts, renderCounts } = useStoryStore.getState();
|
|
3076
2781
|
if (cachedFns && cachedVisitCounts === visitCounts && cachedRenderCounts === renderCounts) return cachedFns;
|
|
3077
|
-
const
|
|
3078
|
-
const
|
|
3079
|
-
const hasVisitedAny = (...names) => names.some((n) => visited(n) > 0);
|
|
3080
|
-
const hasVisitedAll = (...names) => names.every((n) => visited(n) > 0);
|
|
3081
|
-
const rendered = (name) => countOf(renderCounts, name ?? useStoryStore.getState().currentPassage);
|
|
3082
|
-
const hasRendered = (name) => rendered(name) > 0;
|
|
3083
|
-
const hasRenderedAny = (...names) => names.some((n) => rendered(n) > 0);
|
|
3084
|
-
const hasRenderedAll = (...names) => names.every((n) => rendered(n) > 0);
|
|
3085
|
-
const currentPassage = () => {
|
|
3086
|
-
const s = useStoryStore.getState();
|
|
3087
|
-
return s.storyData?.passages.get(s.currentPassage);
|
|
3088
|
-
};
|
|
3089
|
-
const previousPassage = () => {
|
|
3090
|
-
const s = useStoryStore.getState();
|
|
3091
|
-
if (s.historyIndex <= 0) return void 0;
|
|
3092
|
-
const prevName = s.history[s.historyIndex - 1]?.passage;
|
|
3093
|
-
return prevName ? s.storyData?.passages.get(prevName) : void 0;
|
|
3094
|
-
};
|
|
2782
|
+
const visits = countQueries(() => visitCounts);
|
|
2783
|
+
const renders = countQueries(() => renderCounts);
|
|
3095
2784
|
cachedFns = {
|
|
3096
2785
|
currentPassage,
|
|
3097
2786
|
previousPassage,
|
|
3098
|
-
visited,
|
|
3099
|
-
hasVisited,
|
|
3100
|
-
hasVisitedAny,
|
|
3101
|
-
hasVisitedAll,
|
|
3102
|
-
rendered,
|
|
3103
|
-
hasRendered,
|
|
3104
|
-
hasRenderedAny,
|
|
3105
|
-
hasRenderedAll,
|
|
2787
|
+
visited: visits.count,
|
|
2788
|
+
hasVisited: visits.has,
|
|
2789
|
+
hasVisitedAny: visits.any,
|
|
2790
|
+
hasVisitedAll: visits.all,
|
|
2791
|
+
rendered: renders.count,
|
|
2792
|
+
hasRendered: renders.has,
|
|
2793
|
+
hasRenderedAny: renders.any,
|
|
2794
|
+
hasRenderedAll: renders.all,
|
|
3106
2795
|
random,
|
|
3107
2796
|
randomInt
|
|
3108
2797
|
};
|
|
@@ -3427,12 +3116,6 @@ var MAX_DEPTH = 100;
|
|
|
3427
3116
|
function display(value) {
|
|
3428
3117
|
return value == null ? "" : String(value);
|
|
3429
3118
|
}
|
|
3430
|
-
var KIND_SIGILS = {
|
|
3431
|
-
variable: "$",
|
|
3432
|
-
temporary: "_",
|
|
3433
|
-
local: "@",
|
|
3434
|
-
transient: "%"
|
|
3435
|
-
};
|
|
3436
3119
|
/**
|
|
3437
3120
|
* The value of a variable reference (`name` without its sigil, with an
|
|
3438
3121
|
* optional dot path), as `{$name}` shows it in passage text (see
|
|
@@ -3442,7 +3125,7 @@ var KIND_SIGILS = {
|
|
|
3442
3125
|
function resolveVariable(scope, kind, name) {
|
|
3443
3126
|
const parts = name.split(".");
|
|
3444
3127
|
const root = parts[0];
|
|
3445
|
-
checkVariableName(root,
|
|
3128
|
+
checkVariableName(root, SCOPE_SIGILS[kind] + root);
|
|
3446
3129
|
let value = ownValue(kind === "variable" ? scope.variables : kind === "temporary" ? scope.temporary : kind === "transient" ? scope.transient : scope.locals, root);
|
|
3447
3130
|
for (let i = 1; i < parts.length; i++) {
|
|
3448
3131
|
if (value == null) return void 0;
|
|
@@ -3492,7 +3175,7 @@ var TextEvaluator = class {
|
|
|
3492
3175
|
return display(resolveVariable(scope, kind, name));
|
|
3493
3176
|
} catch (error) {
|
|
3494
3177
|
this.errors.push({
|
|
3495
|
-
macro:
|
|
3178
|
+
macro: SCOPE_SIGILS[kind] + name,
|
|
3496
3179
|
error
|
|
3497
3180
|
});
|
|
3498
3181
|
return "";
|
|
@@ -3600,12 +3283,6 @@ function interpolateText(template, scope) {
|
|
|
3600
3283
|
};
|
|
3601
3284
|
return renderText(parsed.nodes, scope);
|
|
3602
3285
|
}
|
|
3603
|
-
var SIGIL_KINDS = {
|
|
3604
|
-
$: "variable",
|
|
3605
|
-
_: "temporary",
|
|
3606
|
-
"@": "local",
|
|
3607
|
-
"%": "transient"
|
|
3608
|
-
};
|
|
3609
3286
|
/**
|
|
3610
3287
|
* Resolve the sigil references in the value of a code attribute (see
|
|
3611
3288
|
* isCodeAttribute): `{$x}`, `{_x.y}`, `{$n + 1}`. Everything else, other
|
|
@@ -3618,7 +3295,7 @@ function interpolateCode(template, scope, decode) {
|
|
|
3618
3295
|
for (const part of splitSigilTemplate(template)) if ("text" in part) text += decode ? decode(part.text) : part.text;
|
|
3619
3296
|
else if ("verbatim" in part) text += part.verbatim;
|
|
3620
3297
|
else if (/^[$_@%][\w.]+$/.test(part.expr)) {
|
|
3621
|
-
const kind =
|
|
3298
|
+
const kind = SIGIL_SCOPES[part.expr[0]];
|
|
3622
3299
|
try {
|
|
3623
3300
|
text += display(resolveVariable(scope, kind, part.expr.slice(1)));
|
|
3624
3301
|
} catch (error) {
|
|
@@ -3700,6 +3377,11 @@ var SIGILS = {
|
|
|
3700
3377
|
local: "@",
|
|
3701
3378
|
transient: "%"
|
|
3702
3379
|
};
|
|
3380
|
+
var STORE_NAMESPACES = {
|
|
3381
|
+
variable: "variables",
|
|
3382
|
+
temporary: "temporary",
|
|
3383
|
+
transient: "transient"
|
|
3384
|
+
};
|
|
3703
3385
|
function VarDisplay({ name, scope, className, id }) {
|
|
3704
3386
|
const resolve = useInterpolate();
|
|
3705
3387
|
className = resolve(className);
|
|
@@ -3707,10 +3389,11 @@ function VarDisplay({ name, scope, className, id }) {
|
|
|
3707
3389
|
const localsValues = useContext(LocalsValuesContext);
|
|
3708
3390
|
const parts = name.split(".");
|
|
3709
3391
|
const root = parts[0];
|
|
3710
|
-
const storeValue = useStoryStore((s) => scope === "
|
|
3711
|
-
|
|
3392
|
+
const storeValue = useStoryStore((s) => scope === "local" ? void 0 : ownValue(s[STORE_NAMESPACES[scope]], root));
|
|
3393
|
+
const nameError = variableNameError(root, SIGILS[scope] + root);
|
|
3394
|
+
if (nameError) return /* @__PURE__ */ jsx("span", {
|
|
3712
3395
|
class: "error",
|
|
3713
|
-
children: `{${SIGILS[scope]}${name} error:
|
|
3396
|
+
children: `{${SIGILS[scope]}${name} error: ${nameError}}`
|
|
3714
3397
|
});
|
|
3715
3398
|
let value;
|
|
3716
3399
|
if (scope === "local") value = ownValue(localsValues, root);
|
|
@@ -3737,10 +3420,7 @@ function ExprDisplay({ expression, className, id }) {
|
|
|
3737
3420
|
const resolve = useInterpolate();
|
|
3738
3421
|
className = resolve(className);
|
|
3739
3422
|
id = resolve(id);
|
|
3740
|
-
const localsValues =
|
|
3741
|
-
const variables = useStoryStore((s) => s.variables);
|
|
3742
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
3743
|
-
const transient = useStoryStore((s) => s.transient);
|
|
3423
|
+
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
3744
3424
|
let display;
|
|
3745
3425
|
try {
|
|
3746
3426
|
const value = evaluate(expression, variables, temporary, localsValues, transient);
|
|
@@ -5049,10 +4729,14 @@ function on(event, cb) {
|
|
|
5049
4729
|
set.delete(cb);
|
|
5050
4730
|
};
|
|
5051
4731
|
}
|
|
5052
|
-
|
|
4732
|
+
/** Call `call` with each listener of `event`. */
|
|
4733
|
+
function forEachListener(event, call) {
|
|
5053
4734
|
const set = listeners.get(event);
|
|
5054
4735
|
if (!set) return;
|
|
5055
|
-
for (const cb of [...set]) cb
|
|
4736
|
+
for (const cb of [...set]) call(cb);
|
|
4737
|
+
}
|
|
4738
|
+
function emit(event, ...args) {
|
|
4739
|
+
forEachListener(event, (cb) => cb(...args));
|
|
5056
4740
|
}
|
|
5057
4741
|
/**
|
|
5058
4742
|
* Emit an event from inside a render commit (layout effect). A throwing
|
|
@@ -5060,13 +4744,13 @@ function emit(event, ...args) {
|
|
|
5060
4744
|
* render; the remaining handlers still run.
|
|
5061
4745
|
*/
|
|
5062
4746
|
function emitFromRender(event, ...args) {
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
}
|
|
4747
|
+
forEachListener(event, (cb) => {
|
|
4748
|
+
try {
|
|
4749
|
+
cb(...args);
|
|
4750
|
+
} catch (err) {
|
|
4751
|
+
console.error(`spindle: Error in ${event} handler:`, err);
|
|
4752
|
+
}
|
|
4753
|
+
});
|
|
5070
4754
|
}
|
|
5071
4755
|
//#endregion
|
|
5072
4756
|
//#region src/saves/save-manager.ts
|
|
@@ -5784,7 +5468,6 @@ function initializeStory(session) {
|
|
|
5784
5468
|
}
|
|
5785
5469
|
//#endregion
|
|
5786
5470
|
//#region src/utils/object-path.ts
|
|
5787
|
-
var hasOwn$2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
5788
5471
|
/**
|
|
5789
5472
|
* Traverse dot-path segments on an object and return the nested value.
|
|
5790
5473
|
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
@@ -7638,9 +7321,7 @@ function TriggerDialogHost() {
|
|
|
7638
7321
|
//#endregion
|
|
7639
7322
|
//#region src/components/App.tsx
|
|
7640
7323
|
function App() {
|
|
7641
|
-
const storyData =
|
|
7642
|
-
const currentPassage = useStoryStore((s) => s.currentPassage);
|
|
7643
|
-
const nobr = useStoryStore((s) => s.nobr);
|
|
7324
|
+
const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
|
|
7644
7325
|
useEffect(() => {
|
|
7645
7326
|
const onKeyDown = (e) => {
|
|
7646
7327
|
const { quickSaveKey, quickLoadKey, save, load } = useStoryStore.getState();
|
|
@@ -7759,45 +7440,47 @@ function loadFromStorage() {
|
|
|
7759
7440
|
}
|
|
7760
7441
|
} catch {}
|
|
7761
7442
|
}
|
|
7443
|
+
/** Register a setting, starting from its default unless it has a value. */
|
|
7444
|
+
function define(name, def) {
|
|
7445
|
+
definitions.set(name, def);
|
|
7446
|
+
if (!hasOwn$2(values, name)) values[name] = def.config.default;
|
|
7447
|
+
loadFromStorage();
|
|
7448
|
+
}
|
|
7449
|
+
/** The value of setting `name` if it has the type of `fallback`, else that. */
|
|
7450
|
+
function valueOf(name, fallback) {
|
|
7451
|
+
const v = ownValue(values, name);
|
|
7452
|
+
return typeof v === typeof fallback ? v : fallback;
|
|
7453
|
+
}
|
|
7762
7454
|
var settings = {
|
|
7763
7455
|
addToggle(name, config) {
|
|
7764
|
-
|
|
7456
|
+
define(name, {
|
|
7765
7457
|
type: "toggle",
|
|
7766
7458
|
config
|
|
7767
7459
|
});
|
|
7768
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) values[name] = config.default;
|
|
7769
|
-
loadFromStorage();
|
|
7770
7460
|
},
|
|
7771
7461
|
addList(name, config) {
|
|
7772
|
-
|
|
7462
|
+
define(name, {
|
|
7773
7463
|
type: "list",
|
|
7774
7464
|
config
|
|
7775
7465
|
});
|
|
7776
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) values[name] = config.default;
|
|
7777
|
-
loadFromStorage();
|
|
7778
7466
|
},
|
|
7779
7467
|
addRange(name, config) {
|
|
7780
|
-
|
|
7468
|
+
define(name, {
|
|
7781
7469
|
type: "range",
|
|
7782
7470
|
config
|
|
7783
7471
|
});
|
|
7784
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) values[name] = config.default;
|
|
7785
|
-
loadFromStorage();
|
|
7786
7472
|
},
|
|
7787
7473
|
get(name) {
|
|
7788
7474
|
return ownValue(values, name);
|
|
7789
7475
|
},
|
|
7790
7476
|
getToggle(name) {
|
|
7791
|
-
|
|
7792
|
-
return typeof v === "boolean" ? v : false;
|
|
7477
|
+
return valueOf(name, false);
|
|
7793
7478
|
},
|
|
7794
7479
|
getList(name) {
|
|
7795
|
-
|
|
7796
|
-
return typeof v === "string" ? v : "";
|
|
7480
|
+
return valueOf(name, "");
|
|
7797
7481
|
},
|
|
7798
7482
|
getRange(name) {
|
|
7799
|
-
|
|
7800
|
-
return typeof v === "number" ? v : 0;
|
|
7483
|
+
return valueOf(name, 0);
|
|
7801
7484
|
},
|
|
7802
7485
|
set(name, value) {
|
|
7803
7486
|
values[name] = value;
|
|
@@ -8096,26 +7779,17 @@ function ensureVariableChangedSubscription() {
|
|
|
8096
7779
|
let prevTrans = useStoryStore.getState().transient;
|
|
8097
7780
|
useStoryStore.subscribe((state) => {
|
|
8098
7781
|
const changed = {};
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
from: prevVars[key],
|
|
8104
|
-
to: state.variables[key]
|
|
7782
|
+
const collect = (prev, next, prefix) => {
|
|
7783
|
+
for (const key of changedNames(prev, next)) changed[prefix + key] = {
|
|
7784
|
+
from: ownValue(prev, key),
|
|
7785
|
+
to: ownValue(next, key)
|
|
8105
7786
|
};
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
for (const key of allTransKeys) if (state.transient[key] !== prevTrans[key]) {
|
|
8110
|
-
changed[`%${key}`] = {
|
|
8111
|
-
from: prevTrans[key],
|
|
8112
|
-
to: state.transient[key]
|
|
8113
|
-
};
|
|
8114
|
-
hasChanges = true;
|
|
8115
|
-
}
|
|
7787
|
+
};
|
|
7788
|
+
collect(prevVars, state.variables, "");
|
|
7789
|
+
collect(prevTrans, state.transient, "%");
|
|
8116
7790
|
prevVars = state.variables;
|
|
8117
7791
|
prevTrans = state.transient;
|
|
8118
|
-
if (
|
|
7792
|
+
if (Object.keys(changed).length > 0) emit("variableChanged", changed);
|
|
8119
7793
|
});
|
|
8120
7794
|
}
|
|
8121
7795
|
var declaredVariables = null;
|
|
@@ -8160,13 +7834,15 @@ function setOne(draft, name, value) {
|
|
|
8160
7834
|
else namespace[key] = value;
|
|
8161
7835
|
}
|
|
8162
7836
|
function createStoryAPI() {
|
|
7837
|
+
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
7838
|
+
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
8163
7839
|
return {
|
|
8164
7840
|
get(name) {
|
|
8165
7841
|
const { isTransient, key } = parseName(name);
|
|
8166
7842
|
const scope = getActiveMutationScope();
|
|
8167
7843
|
const source = scope ?? useStoryStore.getState();
|
|
8168
7844
|
const namespace = isTransient ? source.transient : source.variables;
|
|
8169
|
-
const value = key.includes(".") ? getByPath(namespace, key.split(".")) : namespace
|
|
7845
|
+
const value = key.includes(".") ? getByPath(namespace, key.split(".")) : ownValue(namespace, key);
|
|
8170
7846
|
return scope ? frozenCopy(value) : value;
|
|
8171
7847
|
},
|
|
8172
7848
|
set(nameOrVars, value) {
|
|
@@ -8215,46 +7891,16 @@ function createStoryAPI() {
|
|
|
8215
7891
|
importSave(data, slot) {
|
|
8216
7892
|
return useStoryStore.getState().importSave(data, slot);
|
|
8217
7893
|
},
|
|
8218
|
-
visited
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
},
|
|
8229
|
-
hasVisitedAll(...names) {
|
|
8230
|
-
const { visitCounts } = useStoryStore.getState();
|
|
8231
|
-
return names.every((n) => countOf(visitCounts, n) > 0);
|
|
8232
|
-
},
|
|
8233
|
-
rendered(name) {
|
|
8234
|
-
const state = useStoryStore.getState();
|
|
8235
|
-
return countOf(state.renderCounts, name ?? state.currentPassage);
|
|
8236
|
-
},
|
|
8237
|
-
hasRendered(name) {
|
|
8238
|
-
return this.rendered(name) > 0;
|
|
8239
|
-
},
|
|
8240
|
-
hasRenderedAny(...names) {
|
|
8241
|
-
const { renderCounts } = useStoryStore.getState();
|
|
8242
|
-
return names.some((n) => countOf(renderCounts, n) > 0);
|
|
8243
|
-
},
|
|
8244
|
-
hasRenderedAll(...names) {
|
|
8245
|
-
const { renderCounts } = useStoryStore.getState();
|
|
8246
|
-
return names.every((n) => countOf(renderCounts, n) > 0);
|
|
8247
|
-
},
|
|
8248
|
-
currentPassage() {
|
|
8249
|
-
const state = useStoryStore.getState();
|
|
8250
|
-
return state.storyData?.passages.get(state.currentPassage);
|
|
8251
|
-
},
|
|
8252
|
-
previousPassage() {
|
|
8253
|
-
const state = useStoryStore.getState();
|
|
8254
|
-
if (state.historyIndex <= 0) return void 0;
|
|
8255
|
-
const prevName = state.history[state.historyIndex - 1]?.passage;
|
|
8256
|
-
return prevName ? state.storyData?.passages.get(prevName) : void 0;
|
|
8257
|
-
},
|
|
7894
|
+
visited: visits.count,
|
|
7895
|
+
hasVisited: visits.has,
|
|
7896
|
+
hasVisitedAny: visits.any,
|
|
7897
|
+
hasVisitedAll: visits.all,
|
|
7898
|
+
rendered: renders.count,
|
|
7899
|
+
hasRendered: renders.has,
|
|
7900
|
+
hasRenderedAny: renders.any,
|
|
7901
|
+
hasRenderedAll: renders.all,
|
|
7902
|
+
currentPassage,
|
|
7903
|
+
previousPassage,
|
|
8258
7904
|
get title() {
|
|
8259
7905
|
return useStoryStore.getState().storyData?.name || "";
|
|
8260
7906
|
},
|
|
@@ -8492,7 +8138,8 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
8492
8138
|
const match = line.match(DECLARATION_RE);
|
|
8493
8139
|
if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
|
|
8494
8140
|
const [, name, expr] = match;
|
|
8495
|
-
|
|
8141
|
+
const nameError = variableNameError(name, sigil + name);
|
|
8142
|
+
if (nameError) throw new Error(`${passageName}: ${nameError}`);
|
|
8496
8143
|
let value;
|
|
8497
8144
|
try {
|
|
8498
8145
|
value = new Function("return (" + expr + ")")();
|
|
@@ -8535,7 +8182,8 @@ function extractForLocals(content) {
|
|
|
8535
8182
|
function validateRef(ref, schema, forLocals) {
|
|
8536
8183
|
const parts = ref.split(".");
|
|
8537
8184
|
const rootName = parts[0];
|
|
8538
|
-
|
|
8185
|
+
const nameError = variableNameError(rootName, "$" + rootName);
|
|
8186
|
+
if (nameError) return nameError;
|
|
8539
8187
|
if (forLocals.has(rootName)) return null;
|
|
8540
8188
|
const rootSchema = schema.get(rootName);
|
|
8541
8189
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
|
@@ -8713,6 +8361,56 @@ function astContainsChildren(nodes) {
|
|
|
8713
8361
|
return false;
|
|
8714
8362
|
}
|
|
8715
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
|
|
8716
8414
|
//#region src/components/macros/Set.tsx
|
|
8717
8415
|
defineMacro({
|
|
8718
8416
|
name: "set",
|
|
@@ -9159,7 +8857,7 @@ defineMacro({
|
|
|
9159
8857
|
function liveLocalsView(getValues) {
|
|
9160
8858
|
return new Proxy(createNamespace(), {
|
|
9161
8859
|
get: (_, key) => typeof key === "string" ? ownValue(getValues(), key) : void 0,
|
|
9162
|
-
has: (_, key) => typeof key === "string" &&
|
|
8860
|
+
has: (_, key) => typeof key === "string" && hasOwn$2(getValues(), key),
|
|
9163
8861
|
ownKeys: () => Reflect.ownKeys(getValues()),
|
|
9164
8862
|
getOwnPropertyDescriptor: (_, key) => {
|
|
9165
8863
|
const desc = Object.getOwnPropertyDescriptor(getValues(), key);
|
|
@@ -9507,52 +9205,6 @@ defineMacro({
|
|
|
9507
9205
|
}
|
|
9508
9206
|
});
|
|
9509
9207
|
//#endregion
|
|
9510
|
-
//#region src/components/macros/Widget.tsx
|
|
9511
|
-
function parseWidgetDef(rawArgs) {
|
|
9512
|
-
const tokens = rawArgs.trim().split(/\s+/);
|
|
9513
|
-
const name = tokens[0].replace(/["']/g, "");
|
|
9514
|
-
const params = tokens.slice(1).filter((t) => t.startsWith("@"));
|
|
9515
|
-
for (const param of params) checkVariableName(param.slice(1), param);
|
|
9516
|
-
return {
|
|
9517
|
-
name,
|
|
9518
|
-
params
|
|
9519
|
-
};
|
|
9520
|
-
}
|
|
9521
|
-
defineMacro({
|
|
9522
|
-
name: "widget",
|
|
9523
|
-
block: true,
|
|
9524
|
-
render({ rawArgs, children = [] }, ctx) {
|
|
9525
|
-
let parsed;
|
|
9526
|
-
let error;
|
|
9527
|
-
try {
|
|
9528
|
-
parsed = parseWidgetDef(rawArgs);
|
|
9529
|
-
} catch (err) {
|
|
9530
|
-
error = err;
|
|
9531
|
-
}
|
|
9532
|
-
const { name, params } = parsed ?? {
|
|
9533
|
-
name: "",
|
|
9534
|
-
params: []
|
|
9535
|
-
};
|
|
9536
|
-
const childrenKey = JSON.stringify(children);
|
|
9537
|
-
const paramsKey = params.join(",");
|
|
9538
|
-
ctx.hooks.useLayoutEffect(() => {
|
|
9539
|
-
if (!parsed) return;
|
|
9540
|
-
const isBlock = astContainsChildren(children);
|
|
9541
|
-
registerWidget(name, children, params, isBlock);
|
|
9542
|
-
if (isBlock) registerBlockMacro(name);
|
|
9543
|
-
}, [
|
|
9544
|
-
name,
|
|
9545
|
-
childrenKey,
|
|
9546
|
-
paramsKey
|
|
9547
|
-
]);
|
|
9548
|
-
if (error) return /* @__PURE__ */ jsx(MacroError, {
|
|
9549
|
-
macro: "widget",
|
|
9550
|
-
error
|
|
9551
|
-
});
|
|
9552
|
-
return null;
|
|
9553
|
-
}
|
|
9554
|
-
});
|
|
9555
|
-
//#endregion
|
|
9556
9208
|
//#region src/components/macros/StoryTitle.tsx
|
|
9557
9209
|
defineMacro({
|
|
9558
9210
|
name: "story-title",
|
|
@@ -9631,10 +9283,13 @@ defineMenubarAction({
|
|
|
9631
9283
|
name: "quicksave",
|
|
9632
9284
|
label: "QuickSave",
|
|
9633
9285
|
actionType: "save",
|
|
9634
|
-
setup: () =>
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9286
|
+
setup: () => {
|
|
9287
|
+
const { save, quickSaveKey } = useStoryFields("save", "quickSaveKey");
|
|
9288
|
+
return {
|
|
9289
|
+
perform: save,
|
|
9290
|
+
title: hotkeyTitle("Quick Save", quickSaveKey)
|
|
9291
|
+
};
|
|
9292
|
+
}
|
|
9638
9293
|
});
|
|
9639
9294
|
defineMenubarAction({
|
|
9640
9295
|
name: "quickload",
|
|
@@ -9642,14 +9297,11 @@ defineMenubarAction({
|
|
|
9642
9297
|
actionType: "load",
|
|
9643
9298
|
confirm: "Load saved game? Current progress will be lost.",
|
|
9644
9299
|
setup: () => {
|
|
9645
|
-
const load =
|
|
9646
|
-
const hasSave = useStoryStore((s) => s.hasSave);
|
|
9647
|
-
const key = useStoryStore((s) => s.quickLoadKey);
|
|
9648
|
-
useStoryStore((s) => s.knownSaves);
|
|
9300
|
+
const { load, hasSave, quickLoadKey } = useStoryFields("load", "hasSave", "quickLoadKey", "knownSaves");
|
|
9649
9301
|
return {
|
|
9650
9302
|
perform: () => load(),
|
|
9651
9303
|
disabled: !hasSave(),
|
|
9652
|
-
title: hotkeyTitle("Quick Load",
|
|
9304
|
+
title: hotkeyTitle("Quick Load", quickLoadKey)
|
|
9653
9305
|
};
|
|
9654
9306
|
}
|
|
9655
9307
|
});
|
|
@@ -9737,10 +9389,7 @@ function SaveManagerContent() {
|
|
|
9737
9389
|
const renameInputRef = useRef(null);
|
|
9738
9390
|
const fileInputRef = useRef(null);
|
|
9739
9391
|
const closeDialog = useContext(DialogCloseContext);
|
|
9740
|
-
const storyData =
|
|
9741
|
-
const playthroughId = useStoryStore((s) => s.playthroughId);
|
|
9742
|
-
const beginSave = useStoryStore((s) => s.beginSave);
|
|
9743
|
-
const loadFromPayload = useStoryStore((s) => s.loadFromPayload);
|
|
9392
|
+
const { storyData, playthroughId, beginSave, loadFromPayload } = useStoryFields("storyData", "playthroughId", "beginSave", "loadFromPayload");
|
|
9744
9393
|
const ifid = storyData?.ifid ?? "";
|
|
9745
9394
|
const refresh = useCallback(async () => {
|
|
9746
9395
|
if (!ifid) return;
|
|
@@ -10680,10 +10329,7 @@ defineMacro({
|
|
|
10680
10329
|
name: "passage",
|
|
10681
10330
|
interpolate: true,
|
|
10682
10331
|
render(_props, ctx) {
|
|
10683
|
-
const currentPassage =
|
|
10684
|
-
const navigationId = useStoryStore((s) => s.navigationId);
|
|
10685
|
-
const storyData = useStoryStore((s) => s.storyData);
|
|
10686
|
-
const renderDeferred = useStoryStore((s) => s.renderDeferred);
|
|
10332
|
+
const { currentPassage, navigationId, storyData, renderDeferred } = useStoryFields("currentPassage", "navigationId", "storyData", "renderDeferred");
|
|
10687
10333
|
const [displayed, setDisplayed] = useState({
|
|
10688
10334
|
passage: currentPassage,
|
|
10689
10335
|
id: navigationId
|
|
@@ -11004,9 +10650,7 @@ function boot() {
|
|
|
11004
10650
|
for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
|
|
11005
10651
|
const widgetAST = buildAST(tokenize(passage.content));
|
|
11006
10652
|
for (const node of widgetAST) if (node.type === "macro" && node.name === "widget" && node.rawArgs) {
|
|
11007
|
-
const
|
|
11008
|
-
const widgetName = tokens2[0].replace(/["']/g, "");
|
|
11009
|
-
const params = tokens2.slice(1).filter((t) => t.startsWith("$") || t.startsWith("_") || t.startsWith("@"));
|
|
10653
|
+
const { name: widgetName, params } = parseWidgetDef(node.rawArgs, "$_@");
|
|
11010
10654
|
const children = node.children;
|
|
11011
10655
|
const isBlock = astContainsChildren(children);
|
|
11012
10656
|
try {
|