@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.
@@ -101,13 +101,13 @@ function scanRegex(src, start, cache) {
101
101
  };
102
102
  let i = start + 1;
103
103
  while (i < src.length) {
104
+ const lineEnd = regexLineEnd(src, i);
105
+ if (lineEnd !== -1) return done(lineEnd, false);
104
106
  const c = src.charAt(i);
105
107
  if (c === "\\") {
106
- if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return done(i + 1, false);
107
108
  i += 2;
108
109
  continue;
109
110
  }
110
- if (LINE_TERMINATOR_RE.test(c)) return done(i, false);
111
111
  if (c === "[") {
112
112
  i = scanRegexClass(src, i, cache);
113
113
  if (src.charAt(i) !== "]") return done(i, false);
@@ -138,16 +138,17 @@ function scanRegexClass(src, open, cache) {
138
138
  const opens = [open];
139
139
  let i = open + 1;
140
140
  while (i < src.length) {
141
+ const lineEnd = regexLineEnd(src, i);
142
+ if (lineEnd !== -1) {
143
+ i = lineEnd;
144
+ break;
145
+ }
141
146
  const c = src.charAt(i);
142
147
  if (c === "\\") {
143
- if (LINE_TERMINATOR_RE.test(src.charAt(i + 1))) {
144
- i++;
145
- break;
146
- }
147
148
  i += 2;
148
149
  continue;
149
150
  }
150
- if (c === "]" || LINE_TERMINATOR_RE.test(c)) break;
151
+ if (c === "]") break;
151
152
  if (c === "[" && cache) opens.push(i);
152
153
  i++;
153
154
  }
@@ -156,6 +157,16 @@ function scanRegexClass(src, open, cache) {
156
157
  return i;
157
158
  }
158
159
  /**
160
+ * Index of the line break at `i`, escaped (`\` then a line break) or not,
161
+ * where a regex literal or class ends unterminated; -1 if there is none.
162
+ */
163
+ function regexLineEnd(src, i) {
164
+ const c = src.charAt(i);
165
+ if (LINE_TERMINATOR_RE.test(c)) return i;
166
+ if (c === "\\" && LINE_TERMINATOR_RE.test(src.charAt(i + 1))) return i + 1;
167
+ return -1;
168
+ }
169
+ /**
159
170
  * The next index from `from` on where `find` matches (-1 for none), for
160
171
  * searches from increasing positions: a search from within the stretch the
161
172
  * last one covered has the same answer.
@@ -1105,8 +1116,7 @@ function splitSigilTemplate(template) {
1105
1116
  }
1106
1117
  text += template.slice(i, brace);
1107
1118
  i = brace + 1;
1108
- const sigil = template[i];
1109
- const end = sigil === "$" || sigil === "_" || sigil === "@" || sigil === "%" ? scanBalancedBrace(template, i, memo) : -1;
1119
+ const end = isSigil(template[i]) ? scanBalancedBrace(template, i, memo) : -1;
1110
1120
  if (end === -1) {
1111
1121
  text += "{";
1112
1122
  continue;
@@ -1122,6 +1132,25 @@ function splitSigilTemplate(template) {
1122
1132
  }
1123
1133
  //#endregion
1124
1134
  //#region src/markup/tokenizer.ts
1135
+ /** Variable sigils: story ($), temporary (_), local (@), transient (%). */
1136
+ var SIGIL_SCOPES = {
1137
+ $: "variable",
1138
+ _: "temporary",
1139
+ "@": "local",
1140
+ "%": "transient"
1141
+ };
1142
+ Object.fromEntries(Object.entries(SIGIL_SCOPES).map(([sigil, scope]) => [scope, sigil]));
1143
+ var SIGIL_CHARS = new Set(Object.keys(SIGIL_SCOPES));
1144
+ /** Whether `c` is a variable sigil. */
1145
+ function isSigil(c) {
1146
+ return c !== void 0 && SIGIL_CHARS.has(c);
1147
+ }
1148
+ /** Copy the selectors set in `from` onto `target`, and return it. */
1149
+ function withSelectors(target, from) {
1150
+ if (from.className) target.className = from.className;
1151
+ if (from.id) target.id = from.id;
1152
+ return target;
1153
+ }
1125
1154
  /** Tag name must start with a letter (covers standard and custom elements). */
1126
1155
  var VALID_TAG_START = /[a-zA-Z]/;
1127
1156
  /** HTML void elements: never have children or a closing tag. */
@@ -1141,13 +1170,6 @@ var HTML_VOID_TAGS = new Set([
1141
1170
  "track",
1142
1171
  "wbr"
1143
1172
  ]);
1144
- /** Variable sigils: story ($), temporary (_), local (@), transient (%). */
1145
- var SIGIL_CHARS = new Set([
1146
- "$",
1147
- "_",
1148
- "@",
1149
- "%"
1150
- ]);
1151
1173
  /**
1152
1174
  * Characters other than a sigil that open an expression after `{`:
1153
1175
  * `{(Math.max($a, 0))}`, `{!$done}`. Other characters that can start an
@@ -1158,25 +1180,32 @@ var EXPRESSION_START = new Set(["(", "!"]);
1158
1180
  /** Macros whose body is JavaScript source, kept verbatim instead of tokenized. */
1159
1181
  var RAW_BODY_MACROS = new Set(["do"]);
1160
1182
  /**
1183
+ * Link separators, in the order they are tried, and whether the target
1184
+ * comes first: display|target, display->target, target<-display.
1185
+ */
1186
+ var LINK_SEPARATORS = [
1187
+ ["|", false],
1188
+ ["->", false],
1189
+ ["<-", true]
1190
+ ];
1191
+ /**
1161
1192
  * Parse a Twine link interior into display and target.
1162
1193
  * Supports: display|target, display->target, target<-display, plain
1163
1194
  */
1164
1195
  function parseLink(inner) {
1165
- const pipeIdx = inner.indexOf("|");
1166
- if (pipeIdx !== -1) return {
1167
- display: inner.slice(0, pipeIdx).trim(),
1168
- target: inner.slice(pipeIdx + 1).trim()
1169
- };
1170
- const arrowIdx = inner.indexOf("->");
1171
- if (arrowIdx !== -1) return {
1172
- display: inner.slice(0, arrowIdx).trim(),
1173
- target: inner.slice(arrowIdx + 2).trim()
1174
- };
1175
- const revIdx = inner.indexOf("<-");
1176
- if (revIdx !== -1) return {
1177
- target: inner.slice(0, revIdx).trim(),
1178
- display: inner.slice(revIdx + 2).trim()
1179
- };
1196
+ for (const [separator, targetFirst] of LINK_SEPARATORS) {
1197
+ const idx = inner.indexOf(separator);
1198
+ if (idx === -1) continue;
1199
+ const before = inner.slice(0, idx).trim();
1200
+ const after = inner.slice(idx + separator.length).trim();
1201
+ return targetFirst ? {
1202
+ display: after,
1203
+ target: before
1204
+ } : {
1205
+ display: before,
1206
+ target: after
1207
+ };
1208
+ }
1180
1209
  const trimmed = inner.trim();
1181
1210
  return {
1182
1211
  display: trimmed,
@@ -1207,8 +1236,10 @@ function parseMacroContent(content) {
1207
1236
  }
1208
1237
  /**
1209
1238
  * Parse CSS selectors: .foo.bar#baz → { className: "foo bar", id: "baz" }
1210
- * Scans .[a-zA-Z0-9_-]+ and #[a-zA-Z0-9_-]+ segments in any order.
1211
- * Returns space-joined class string, last id wins, and position after last segment.
1239
+ * Scans .[a-zA-Z0-9_-]+ and #[a-zA-Z0-9_-]+ segments in any order, and one
1240
+ * space after them. Returns space-joined class string, last id wins (each
1241
+ * left out if empty), and the position after them: `startIdx` if there are
1242
+ * none.
1212
1243
  */
1213
1244
  function parseSelectors(input, startIdx) {
1214
1245
  const classes = [];
@@ -1221,7 +1252,7 @@ function parseSelectors(input, startIdx) {
1221
1252
  while (i < input.length) if (/[a-zA-Z0-9_-]/.test(input[i])) {
1222
1253
  name += input[i];
1223
1254
  i++;
1224
- } else if (input[i] === "{" && SIGIL_CHARS.has(input[i + 1])) {
1255
+ } else if (input[i] === "{" && isSigil(input[i + 1])) {
1225
1256
  const braceStart = i;
1226
1257
  i += 2;
1227
1258
  while (i < input.length && /[\w.]/.test(input[i])) i++;
@@ -1236,10 +1267,13 @@ function parseSelectors(input, startIdx) {
1236
1267
  if (name) if (prefix === ".") classes.push(name);
1237
1268
  else id = name;
1238
1269
  }
1270
+ if (i > startIdx && input[i] === " ") i++;
1239
1271
  return {
1240
- className: classes.join(" "),
1241
- id,
1242
- endIdx: i
1272
+ selectors: withSelectors({}, {
1273
+ className: classes.join(" "),
1274
+ id
1275
+ }),
1276
+ end: i
1243
1277
  };
1244
1278
  }
1245
1279
  /**
@@ -1354,7 +1388,7 @@ function scanQuotedValue(input, j, quote, code, memo) {
1354
1388
  continue;
1355
1389
  }
1356
1390
  if (input[j] === "{") {
1357
- const closeIdx = !code ? scanBlockClose(input, j, memo) : SIGIL_CHARS.has(input[j + 1]) ? scanBalancedBrace(input, j + 1, memo) : -1;
1391
+ const closeIdx = !code ? scanBlockClose(input, j, memo) : isSigil(input[j + 1]) ? scanBalancedBrace(input, j + 1, memo) : -1;
1358
1392
  if (closeIdx !== -1) {
1359
1393
  j = closeIdx + 1;
1360
1394
  checkpoint = true;
@@ -1430,12 +1464,7 @@ function scanMacroClose(input, contentStart, memo) {
1430
1464
  * code from just past the {. Returns -1 if it is unclosed.
1431
1465
  */
1432
1466
  function scanBlockClose(input, open, memo) {
1433
- let at = open + 1;
1434
- const c = input[at];
1435
- if (c === "." || c === "#") {
1436
- at = parseSelectors(input, at).endIdx;
1437
- if (input[at] === " ") at++;
1438
- }
1467
+ const at = parseSelectors(input, open + 1).end;
1439
1468
  const first = input[at];
1440
1469
  if (first !== void 0 && (first === "/" || /[a-zA-Z]/.test(first))) return scanMacroClose(input, at, memo);
1441
1470
  return scanBalancedBrace(input, at, memo);
@@ -1627,13 +1656,22 @@ function tokenize(input, options = {}) {
1627
1656
  const memo = createScanMemo();
1628
1657
  let i = 0;
1629
1658
  let textStart = 0;
1659
+ /** Push a text token for the text not yet tokenized before `end`. */
1630
1660
  function flushText(end) {
1631
- if (end > textStart) tokens.push({
1632
- type: "text",
1633
- value: input.slice(textStart, end),
1634
- start: textStart,
1635
- end
1636
- });
1661
+ if (end > textStart) {
1662
+ tokens.push({
1663
+ type: "text",
1664
+ value: input.slice(textStart, end),
1665
+ start: textStart,
1666
+ end
1667
+ });
1668
+ textStart = end;
1669
+ }
1670
+ }
1671
+ /** Push `token` and go on after it. */
1672
+ function emit(token) {
1673
+ tokens.push(token);
1674
+ i = textStart = token.end;
1637
1675
  }
1638
1676
  /**
1639
1677
  * After an opening raw-body macro ({do}), emit everything up to its
@@ -1675,34 +1713,66 @@ function tokenize(input, options = {}) {
1675
1713
  atRe.lastIndex = closeStart;
1676
1714
  const closeEnd = closeStart + atRe.exec(input)[0].length;
1677
1715
  flushText(closeStart);
1678
- tokens.push({
1716
+ emit({
1679
1717
  type: "macro",
1680
1718
  ...parseMacroContent(input.slice(closeStart + 1, closeEnd - 1)),
1681
1719
  start: closeStart,
1682
1720
  end: closeEnd
1683
1721
  });
1684
- i = closeEnd;
1685
- textStart = closeEnd;
1686
1722
  }
1687
1723
  /**
1688
1724
  * Push an expression token for the `{…}` block opened at `start` whose
1689
- * expression starts at `exprStart`, flushing the text before it first
1690
- * when `flush`. Returns false, consuming nothing, if the block is unclosed.
1725
+ * expression starts at `exprStart`, flushing the text before it first.
1726
+ * Returns false, consuming nothing, if the block is unclosed.
1691
1727
  */
1692
- function pushExpression(exprStart, start, flush, className, id) {
1728
+ function pushExpression(exprStart, start, selectors) {
1693
1729
  const closeIdx = scanBalancedBrace(input, exprStart, memo);
1694
1730
  if (closeIdx === -1) return false;
1695
- if (flush) flushText(start);
1696
- const token = {
1731
+ flushText(start);
1732
+ emit(withSelectors({
1697
1733
  type: "expression",
1698
1734
  expression: input.slice(exprStart, closeIdx),
1699
1735
  start,
1700
1736
  end: closeIdx + 1
1701
- };
1702
- if (className) token.className = className;
1703
- if (id) token.id = id;
1704
- tokens.push(token);
1705
- i = textStart = closeIdx + 1;
1737
+ }, selectors));
1738
+ return true;
1739
+ }
1740
+ /**
1741
+ * Push a variable token for the `{…}` block opened at `start` whose sigil
1742
+ * is at `sigilAt`: `{$name}`, `{_name.field.subfield}`. Anything else
1743
+ * after the name makes the block an expression from the sigil on
1744
+ * (`{$expr[...]}`). Returns false, consuming nothing, if it is unclosed.
1745
+ */
1746
+ function pushVariable(sigilAt, start, selectors) {
1747
+ let nameEnd = sigilAt + 1;
1748
+ while (nameEnd < input.length && /[\w.]/.test(input[nameEnd])) nameEnd++;
1749
+ if (input[nameEnd] !== "}") return pushExpression(sigilAt, start, selectors);
1750
+ emit(withSelectors({
1751
+ type: "variable",
1752
+ name: input.slice(sigilAt + 1, nameEnd),
1753
+ scope: SIGIL_SCOPES[input[sigilAt]],
1754
+ start,
1755
+ end: nameEnd + 1
1756
+ }, selectors));
1757
+ return true;
1758
+ }
1759
+ /**
1760
+ * Push a macro token for the `{…}` block opened at `start` whose content
1761
+ * (name, then arguments) starts at `contentStart`, then consume the body
1762
+ * of a raw-body macro. Returns false, consuming nothing, if the block is
1763
+ * unclosed.
1764
+ */
1765
+ function pushMacro(contentStart, start, selectors) {
1766
+ const closeIdx = scanMacroClose(input, contentStart, memo);
1767
+ if (closeIdx === -1) return false;
1768
+ const token = withSelectors({
1769
+ type: "macro",
1770
+ ...parseMacroContent(input.slice(contentStart, closeIdx)),
1771
+ start,
1772
+ end: closeIdx + 1
1773
+ }, selectors);
1774
+ emit(token);
1775
+ consumeRawBody(token.name, token.isClose);
1706
1776
  return true;
1707
1777
  }
1708
1778
  while (i < input.length) {
@@ -1726,14 +1796,12 @@ function tokenize(input, options = {}) {
1726
1796
  }
1727
1797
  if ((next === "{" || next === "}") && (k - i) % 2 === 1) {
1728
1798
  flushText(k - 1);
1729
- tokens.push({
1799
+ emit({
1730
1800
  type: "text",
1731
1801
  value: next,
1732
1802
  start: k - 1,
1733
1803
  end: k + 1
1734
1804
  });
1735
- i = k + 1;
1736
- textStart = i;
1737
1805
  continue;
1738
1806
  }
1739
1807
  i = k;
@@ -1742,413 +1810,36 @@ function tokenize(input, options = {}) {
1742
1810
  if (!textMode && input[i] === "[" && input[i + 1] === "[") {
1743
1811
  flushText(i);
1744
1812
  const start = i;
1745
- i += 2;
1746
- let className;
1747
- let id;
1748
- if (input[i] === "." || input[i] === "#") {
1749
- const parsed = parseSelectors(input, i);
1750
- className = parsed.className || void 0;
1751
- id = parsed.id || void 0;
1752
- i = parsed.endIdx;
1753
- if (input[i] === " ") i++;
1754
- }
1755
- const innerStart = i;
1813
+ const { selectors, end: innerStart } = parseSelectors(input, i + 2);
1756
1814
  const closeIdx = scanLinkClose(input, innerStart, memo);
1757
1815
  if (closeIdx === -1) {
1758
1816
  i = start + 2;
1759
- textStart = start;
1760
1817
  continue;
1761
1818
  }
1762
- const inner = input.slice(innerStart, closeIdx);
1763
- i = closeIdx + 2;
1764
- const { display, target } = parseLink(inner);
1765
- const linkToken = {
1819
+ emit(withSelectors({
1766
1820
  type: "link",
1767
- display,
1768
- target,
1821
+ ...parseLink(input.slice(innerStart, closeIdx)),
1769
1822
  start,
1770
- end: i
1771
- };
1772
- if (className) linkToken.className = className;
1773
- if (id) linkToken.id = id;
1774
- tokens.push(linkToken);
1775
- textStart = i;
1823
+ end: closeIdx + 2
1824
+ }, selectors));
1776
1825
  continue;
1777
1826
  }
1778
1827
  if (input[i] === "{") {
1779
1828
  const start = i;
1780
- let nextChar = input[i + 1];
1781
- let className;
1782
- let id;
1783
- if (nextChar === "." || nextChar === "#") {
1784
- flushText(i);
1785
- const parsed = parseSelectors(input, i + 1);
1786
- className = parsed.className || void 0;
1787
- id = parsed.id || void 0;
1788
- let afterSelectors = parsed.endIdx;
1789
- if (input[afterSelectors] === " ") afterSelectors++;
1790
- const charAfter = input[afterSelectors];
1791
- if (charAfter === "$") {
1792
- i = afterSelectors + 1;
1793
- const nameStart = i;
1794
- while (i < input.length && /[\w.]/.test(input[i])) i++;
1795
- const name = input.slice(nameStart, i);
1796
- if (input[i] === "}") {
1797
- i++;
1798
- const token = {
1799
- type: "variable",
1800
- name,
1801
- scope: "variable",
1802
- start,
1803
- end: i
1804
- };
1805
- if (className) token.className = className;
1806
- if (id) token.id = id;
1807
- tokens.push(token);
1808
- textStart = i;
1809
- continue;
1810
- }
1811
- const closeIdx$ = scanBalancedBrace(input, nameStart - 1, memo);
1812
- if (closeIdx$ !== -1) {
1813
- const expression = input.slice(afterSelectors, closeIdx$);
1814
- i = closeIdx$ + 1;
1815
- const token = {
1816
- type: "expression",
1817
- expression,
1818
- start,
1819
- end: i
1820
- };
1821
- if (className) token.className = className;
1822
- if (id) token.id = id;
1823
- tokens.push(token);
1824
- textStart = i;
1825
- continue;
1826
- }
1827
- i = start + 1;
1828
- textStart = start;
1829
- continue;
1830
- }
1831
- if (charAfter === "_") {
1832
- i = afterSelectors + 1;
1833
- const nameStart = i;
1834
- while (i < input.length && /[\w.]/.test(input[i])) i++;
1835
- const name = input.slice(nameStart, i);
1836
- if (input[i] === "}") {
1837
- i++;
1838
- const token = {
1839
- type: "variable",
1840
- name,
1841
- scope: "temporary",
1842
- start,
1843
- end: i
1844
- };
1845
- if (className) token.className = className;
1846
- if (id) token.id = id;
1847
- tokens.push(token);
1848
- textStart = i;
1849
- continue;
1850
- }
1851
- const closeIdx_ = scanBalancedBrace(input, nameStart - 1, memo);
1852
- if (closeIdx_ !== -1) {
1853
- const expression = input.slice(afterSelectors, closeIdx_);
1854
- i = closeIdx_ + 1;
1855
- const token = {
1856
- type: "expression",
1857
- expression,
1858
- start,
1859
- end: i
1860
- };
1861
- if (className) token.className = className;
1862
- if (id) token.id = id;
1863
- tokens.push(token);
1864
- textStart = i;
1865
- continue;
1866
- }
1867
- i = start + 1;
1868
- textStart = start;
1869
- continue;
1870
- }
1871
- if (charAfter === "@") {
1872
- i = afterSelectors + 1;
1873
- const nameStart = i;
1874
- while (i < input.length && /[\w.]/.test(input[i])) i++;
1875
- const name = input.slice(nameStart, i);
1876
- if (input[i] === "}") {
1877
- i++;
1878
- const token = {
1879
- type: "variable",
1880
- name,
1881
- scope: "local",
1882
- start,
1883
- end: i
1884
- };
1885
- if (className) token.className = className;
1886
- if (id) token.id = id;
1887
- tokens.push(token);
1888
- textStart = i;
1889
- continue;
1890
- }
1891
- const closeIdx_at = scanBalancedBrace(input, nameStart - 1, memo);
1892
- if (closeIdx_at !== -1) {
1893
- const expression = input.slice(afterSelectors, closeIdx_at);
1894
- i = closeIdx_at + 1;
1895
- const token = {
1896
- type: "expression",
1897
- expression,
1898
- start,
1899
- end: i
1900
- };
1901
- if (className) token.className = className;
1902
- if (id) token.id = id;
1903
- tokens.push(token);
1904
- textStart = i;
1905
- continue;
1906
- }
1907
- i = start + 1;
1908
- textStart = start;
1909
- continue;
1910
- }
1911
- if (charAfter === "%") {
1912
- i = afterSelectors + 1;
1913
- const nameStart = i;
1914
- while (i < input.length && /[\w.]/.test(input[i])) i++;
1915
- const name = input.slice(nameStart, i);
1916
- if (input[i] === "}") {
1917
- i++;
1918
- const token = {
1919
- type: "variable",
1920
- name,
1921
- scope: "transient",
1922
- start,
1923
- end: i
1924
- };
1925
- if (className) token.className = className;
1926
- if (id) token.id = id;
1927
- tokens.push(token);
1928
- textStart = i;
1929
- continue;
1930
- }
1931
- const closeIdx_pct = scanBalancedBrace(input, nameStart - 1, memo);
1932
- if (closeIdx_pct !== -1) {
1933
- const expression = input.slice(afterSelectors, closeIdx_pct);
1934
- i = closeIdx_pct + 1;
1935
- const token = {
1936
- type: "expression",
1937
- expression,
1938
- start,
1939
- end: i
1940
- };
1941
- if (className) token.className = className;
1942
- if (id) token.id = id;
1943
- tokens.push(token);
1944
- textStart = i;
1945
- continue;
1946
- }
1947
- i = start + 1;
1948
- textStart = start;
1949
- continue;
1950
- }
1951
- if (EXPRESSION_START.has(charAfter) && pushExpression(afterSelectors, start, false, className, id)) continue;
1952
- if (charAfter !== void 0 && /[a-zA-Z]/.test(charAfter)) {
1953
- const contentStart = afterSelectors;
1954
- const closeIdx = scanMacroClose(input, contentStart, memo);
1955
- if (closeIdx === -1) {
1956
- i = start + 1;
1957
- textStart = start;
1958
- continue;
1959
- }
1960
- const content = input.slice(contentStart, closeIdx);
1961
- i = closeIdx + 1;
1962
- const { name, rawArgs, isClose } = parseMacroContent(content);
1963
- const token = {
1964
- type: "macro",
1965
- name,
1966
- rawArgs,
1967
- isClose,
1968
- start,
1969
- end: i
1970
- };
1971
- if (className) token.className = className;
1972
- if (id) token.id = id;
1973
- tokens.push(token);
1974
- textStart = i;
1975
- consumeRawBody(name, isClose);
1976
- continue;
1977
- }
1978
- i = start + 1;
1979
- textStart = start;
1980
- continue;
1981
- }
1982
- if (nextChar === "$") {
1983
- flushText(i);
1984
- i += 2;
1985
- const nameStart = i;
1986
- while (i < input.length && /[\w.]/.test(input[i])) i++;
1987
- const name = input.slice(nameStart, i);
1988
- if (input[i] === "}") {
1989
- i++;
1990
- tokens.push({
1991
- type: "variable",
1992
- name,
1993
- scope: "variable",
1994
- start,
1995
- end: i
1996
- });
1997
- textStart = i;
1998
- continue;
1999
- }
2000
- const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
2001
- if (closeIdx !== -1) {
2002
- const expression = input.slice(start + 1, closeIdx);
2003
- i = closeIdx + 1;
2004
- tokens.push({
2005
- type: "expression",
2006
- expression,
2007
- start,
2008
- end: i
2009
- });
2010
- textStart = i;
2011
- continue;
2012
- }
2013
- i = start + 1;
2014
- textStart = start;
2015
- continue;
2016
- }
2017
- if (nextChar === "_") {
2018
- flushText(i);
2019
- i += 2;
2020
- const nameStart = i;
2021
- while (i < input.length && /[\w.]/.test(input[i])) i++;
2022
- const name = input.slice(nameStart, i);
2023
- if (input[i] === "}") {
2024
- i++;
2025
- tokens.push({
2026
- type: "variable",
2027
- name,
2028
- scope: "temporary",
2029
- start,
2030
- end: i
2031
- });
2032
- textStart = i;
2033
- continue;
2034
- }
2035
- const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
2036
- if (closeIdx !== -1) {
2037
- const expression = input.slice(start + 1, closeIdx);
2038
- i = closeIdx + 1;
2039
- tokens.push({
2040
- type: "expression",
2041
- expression,
2042
- start,
2043
- end: i
2044
- });
2045
- textStart = i;
2046
- continue;
2047
- }
2048
- i = start + 1;
2049
- textStart = start;
2050
- continue;
1829
+ const { selectors, end: at } = parseSelectors(input, i + 1);
1830
+ const prefixed = at > i + 1;
1831
+ if (prefixed) flushText(start);
1832
+ const c = input[at];
1833
+ if (isSigil(c)) {
1834
+ flushText(start);
1835
+ if (pushVariable(at, start, selectors)) continue;
1836
+ } else if (EXPRESSION_START.has(c)) {
1837
+ if (pushExpression(at, start, selectors)) continue;
1838
+ } else if (c !== void 0 && (/[a-zA-Z]/.test(c) || c === "/" && !prefixed)) {
1839
+ flushText(start);
1840
+ if (pushMacro(at, start, selectors)) continue;
2051
1841
  }
2052
- if (nextChar === "@") {
2053
- flushText(i);
2054
- i += 2;
2055
- const nameStart = i;
2056
- while (i < input.length && /[\w.]/.test(input[i])) i++;
2057
- const name = input.slice(nameStart, i);
2058
- if (input[i] === "}") {
2059
- i++;
2060
- tokens.push({
2061
- type: "variable",
2062
- name,
2063
- scope: "local",
2064
- start,
2065
- end: i
2066
- });
2067
- textStart = i;
2068
- continue;
2069
- }
2070
- const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
2071
- if (closeIdx !== -1) {
2072
- const expression = input.slice(start + 1, closeIdx);
2073
- i = closeIdx + 1;
2074
- tokens.push({
2075
- type: "expression",
2076
- expression,
2077
- start,
2078
- end: i
2079
- });
2080
- textStart = i;
2081
- continue;
2082
- }
2083
- i = start + 1;
2084
- textStart = start;
2085
- continue;
2086
- }
2087
- if (nextChar === "%") {
2088
- flushText(i);
2089
- i += 2;
2090
- const nameStart = i;
2091
- while (i < input.length && /[\w.]/.test(input[i])) i++;
2092
- const name = input.slice(nameStart, i);
2093
- if (input[i] === "}") {
2094
- i++;
2095
- tokens.push({
2096
- type: "variable",
2097
- name,
2098
- scope: "transient",
2099
- start,
2100
- end: i
2101
- });
2102
- textStart = i;
2103
- continue;
2104
- }
2105
- const closeIdx = scanBalancedBrace(input, nameStart - 1, memo);
2106
- if (closeIdx !== -1) {
2107
- const expression = input.slice(start + 1, closeIdx);
2108
- i = closeIdx + 1;
2109
- tokens.push({
2110
- type: "expression",
2111
- expression,
2112
- start,
2113
- end: i
2114
- });
2115
- textStart = i;
2116
- continue;
2117
- }
2118
- i = start + 1;
2119
- textStart = start;
2120
- continue;
2121
- }
2122
- if (EXPRESSION_START.has(nextChar)) {
2123
- if (pushExpression(i + 1, start, true)) continue;
2124
- i++;
2125
- continue;
2126
- }
2127
- if (nextChar !== void 0 && (nextChar === "/" || /[a-zA-Z]/.test(nextChar))) {
2128
- flushText(i);
2129
- const contentStart = i + 1;
2130
- const closeIdx = scanMacroClose(input, contentStart, memo);
2131
- if (closeIdx === -1) {
2132
- i = start + 1;
2133
- textStart = start;
2134
- continue;
2135
- }
2136
- const content = input.slice(contentStart, closeIdx);
2137
- i = closeIdx + 1;
2138
- const { name, rawArgs, isClose } = parseMacroContent(content);
2139
- tokens.push({
2140
- type: "macro",
2141
- name,
2142
- rawArgs,
2143
- isClose,
2144
- start,
2145
- end: i
2146
- });
2147
- textStart = i;
2148
- consumeRawBody(name, isClose);
2149
- continue;
2150
- }
2151
- i++;
1842
+ i = start + 1;
2152
1843
  continue;
2153
1844
  }
2154
1845
  if (!textMode && input[i] === "<") {
@@ -2166,11 +1857,10 @@ function tokenize(input, options = {}) {
2166
1857
  j++;
2167
1858
  flushText(start);
2168
1859
  if (HTML_VOID_TAGS.has(tagLower)) {
2169
- textStart = j;
2170
- i = j;
1860
+ i = textStart = j;
2171
1861
  continue;
2172
1862
  }
2173
- tokens.push({
1863
+ emit({
2174
1864
  type: "html",
2175
1865
  tag,
2176
1866
  attributes: {},
@@ -2179,8 +1869,6 @@ function tokenize(input, options = {}) {
2179
1869
  start,
2180
1870
  end: j
2181
1871
  });
2182
- textStart = j;
2183
- i = j;
2184
1872
  continue;
2185
1873
  }
2186
1874
  } else {
@@ -2194,7 +1882,7 @@ function tokenize(input, options = {}) {
2194
1882
  if (input[j] === ">") {
2195
1883
  j++;
2196
1884
  flushText(start);
2197
- tokens.push({
1885
+ emit({
2198
1886
  type: "html",
2199
1887
  tag,
2200
1888
  attributes: parseHtmlAttributes(input, attrsStart, memo).attributes,
@@ -2203,8 +1891,6 @@ function tokenize(input, options = {}) {
2203
1891
  start,
2204
1892
  end: j
2205
1893
  });
2206
- textStart = j;
2207
- i = j;
2208
1894
  continue;
2209
1895
  }
2210
1896
  }