binja 0.1.0 → 0.1.1

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/index.js CHANGED
@@ -1209,102 +1209,120 @@ class Parser {
1209
1209
  }
1210
1210
 
1211
1211
  // src/runtime/context.ts
1212
+ function createForLoop(items, index, depth, lastCycleValue, parentloop) {
1213
+ const length = items.length;
1214
+ const forloop = {
1215
+ _items: items,
1216
+ _idx: index,
1217
+ counter: index + 1,
1218
+ counter0: index,
1219
+ first: index === 0,
1220
+ last: index === length - 1,
1221
+ length,
1222
+ index: index + 1,
1223
+ index0: index,
1224
+ depth,
1225
+ depth0: depth - 1,
1226
+ revcounter: length - index,
1227
+ revcounter0: length - index - 1,
1228
+ revindex: length - index,
1229
+ revindex0: length - index - 1,
1230
+ get previtem() {
1231
+ return this._idx > 0 ? this._items[this._idx - 1] : undefined;
1232
+ },
1233
+ get nextitem() {
1234
+ return this._idx < this._items.length - 1 ? this._items[this._idx + 1] : undefined;
1235
+ },
1236
+ cycle: (...args) => args[index % args.length],
1237
+ changed: (value) => {
1238
+ const changed = value !== lastCycleValue.value;
1239
+ lastCycleValue.value = value;
1240
+ return changed;
1241
+ }
1242
+ };
1243
+ if (parentloop) {
1244
+ forloop.parentloop = parentloop;
1245
+ }
1246
+ return forloop;
1247
+ }
1248
+
1212
1249
  class Context {
1213
1250
  scopes = [];
1214
1251
  parent = null;
1215
1252
  _forloopStack = [];
1216
- _lastCycleValue = null;
1253
+ _lastCycleValue = { value: null };
1254
+ _currentForloop = null;
1255
+ _currentScope;
1217
1256
  constructor(data = {}, parent = null) {
1218
1257
  this.parent = parent;
1219
- this.scopes.push(new Map(Object.entries(data)));
1258
+ this._currentScope = Object.assign(Object.create(null), data);
1259
+ this.scopes.push(this._currentScope);
1220
1260
  }
1221
1261
  get(name) {
1222
1262
  if (name === "forloop" || name === "loop") {
1223
- return this._forloopStack[this._forloopStack.length - 1] || null;
1263
+ return this._currentForloop;
1224
1264
  }
1225
1265
  for (let i = this.scopes.length - 1;i >= 0; i--) {
1226
- if (this.scopes[i].has(name)) {
1227
- return this.scopes[i].get(name);
1266
+ const scope = this.scopes[i];
1267
+ if (name in scope) {
1268
+ return scope[name];
1228
1269
  }
1229
1270
  }
1230
- if (this.parent) {
1231
- return this.parent.get(name);
1232
- }
1233
- return;
1271
+ return this.parent ? this.parent.get(name) : undefined;
1234
1272
  }
1235
1273
  set(name, value) {
1236
- this.scopes[this.scopes.length - 1].set(name, value);
1274
+ this._currentScope[name] = value;
1237
1275
  }
1238
1276
  has(name) {
1239
1277
  for (let i = this.scopes.length - 1;i >= 0; i--) {
1240
- if (this.scopes[i].has(name))
1278
+ if (name in this.scopes[i])
1241
1279
  return true;
1242
1280
  }
1243
1281
  return this.parent ? this.parent.has(name) : false;
1244
1282
  }
1245
1283
  push(data = {}) {
1246
- this.scopes.push(new Map(Object.entries(data)));
1284
+ this._currentScope = Object.assign(Object.create(null), data);
1285
+ this.scopes.push(this._currentScope);
1247
1286
  }
1248
1287
  pop() {
1249
1288
  if (this.scopes.length > 1) {
1250
1289
  this.scopes.pop();
1290
+ this._currentScope = this.scopes[this.scopes.length - 1];
1251
1291
  }
1252
1292
  }
1253
1293
  derived(data = {}) {
1254
1294
  return new Context(data, this);
1255
1295
  }
1256
1296
  pushForLoop(items, index) {
1257
- const length = items.length;
1258
1297
  const depth = this._forloopStack.length + 1;
1259
- const forloop = {
1260
- counter: index + 1,
1261
- counter0: index,
1262
- revcounter: length - index,
1263
- revcounter0: length - index - 1,
1264
- first: index === 0,
1265
- last: index === length - 1,
1266
- length,
1267
- index: index + 1,
1268
- index0: index,
1269
- revindex: length - index,
1270
- revindex0: length - index - 1,
1271
- depth,
1272
- depth0: depth - 1,
1273
- previtem: index > 0 ? items[index - 1] : undefined,
1274
- nextitem: index < length - 1 ? items[index + 1] : undefined,
1275
- cycle: (...args) => args[index % args.length],
1276
- changed: (value) => {
1277
- const changed = value !== this._lastCycleValue;
1278
- this._lastCycleValue = value;
1279
- return changed;
1280
- }
1281
- };
1282
- if (this._forloopStack.length > 0) {
1283
- forloop.parentloop = this._forloopStack[this._forloopStack.length - 1];
1284
- }
1298
+ const parentloop = this._forloopStack.length > 0 ? this._forloopStack[this._forloopStack.length - 1] : undefined;
1299
+ const forloop = createForLoop(items, index, depth, this._lastCycleValue, parentloop);
1285
1300
  this._forloopStack.push(forloop);
1301
+ this._currentForloop = forloop;
1286
1302
  return forloop;
1287
1303
  }
1288
1304
  popForLoop() {
1289
1305
  this._forloopStack.pop();
1306
+ this._currentForloop = this._forloopStack.length > 0 ? this._forloopStack[this._forloopStack.length - 1] : null;
1290
1307
  }
1291
1308
  updateForLoop(index, items) {
1292
- const forloop = this._forloopStack[this._forloopStack.length - 1];
1309
+ const forloop = this._currentForloop;
1293
1310
  if (!forloop)
1294
1311
  return;
1295
1312
  const length = items.length;
1313
+ forloop._idx = index;
1314
+ forloop._items = items;
1296
1315
  forloop.counter = index + 1;
1297
1316
  forloop.counter0 = index;
1298
- forloop.revcounter = length - index;
1299
- forloop.revcounter0 = length - index - 1;
1300
1317
  forloop.first = index === 0;
1301
1318
  forloop.last = index === length - 1;
1302
1319
  forloop.index = index + 1;
1303
1320
  forloop.index0 = index;
1321
+ forloop.revcounter = length - index;
1322
+ forloop.revcounter0 = length - index - 1;
1304
1323
  forloop.revindex = length - index;
1305
1324
  forloop.revindex0 = length - index - 1;
1306
- forloop.previtem = index > 0 ? items[index - 1] : undefined;
1307
- forloop.nextitem = index < length - 1 ? items[index + 1] : undefined;
1325
+ forloop.cycle = (...args) => args[index % args.length];
1308
1326
  }
1309
1327
  toObject() {
1310
1328
  const result = {};
@@ -1312,15 +1330,28 @@ class Context {
1312
1330
  Object.assign(result, this.parent.toObject());
1313
1331
  }
1314
1332
  for (const scope of this.scopes) {
1315
- for (const [key, value] of scope) {
1316
- result[key] = value;
1317
- }
1333
+ Object.assign(result, scope);
1318
1334
  }
1319
1335
  return result;
1320
1336
  }
1321
1337
  }
1322
1338
 
1323
1339
  // src/filters/index.ts
1340
+ var TITLE_REGEX = /\b\w/g;
1341
+ var STRIPTAGS_REGEX = /<[^>]*>/g;
1342
+ var SLUGIFY_NON_WORD_REGEX = /[^\w\s-]/g;
1343
+ var SLUGIFY_SPACES_REGEX = /[\s_-]+/g;
1344
+ var SLUGIFY_TRIM_REGEX = /^-+|-+$/g;
1345
+ var URLIZE_REGEX = /(https?:\/\/[^\s]+)/g;
1346
+ var DATE_CHAR_REGEX = /[a-zA-Z]/g;
1347
+ var DOUBLE_NEWLINE_REGEX = /\n\n+/;
1348
+ var NEWLINE_REGEX = /\n/g;
1349
+ var WHITESPACE_REGEX = /\s+/;
1350
+ var DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1351
+ var DAY_NAMES_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1352
+ var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1353
+ var MONTH_NAMES_LONG = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
1354
+ var MONTH_NAMES_AP = ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."];
1324
1355
  var upper = (value) => String(value).toUpperCase();
1325
1356
  var lower = (value) => String(value).toLowerCase();
1326
1357
  var capitalize = (value) => {
@@ -1331,13 +1362,13 @@ var capfirst = (value) => {
1331
1362
  const str = String(value);
1332
1363
  return str.charAt(0).toUpperCase() + str.slice(1);
1333
1364
  };
1334
- var title = (value) => String(value).replace(/\b\w/g, (c) => c.toUpperCase());
1365
+ var title = (value) => String(value).replace(TITLE_REGEX, (c) => c.toUpperCase());
1335
1366
  var trim = (value) => String(value).trim();
1336
- var striptags = (value) => String(value).replace(/<[^>]*>/g, "");
1367
+ var striptags = (value) => String(value).replace(STRIPTAGS_REGEX, "");
1337
1368
  var escape = (value) => {
1338
1369
  if (value?.__safe__)
1339
1370
  return value;
1340
- const escaped = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
1371
+ const escaped = Bun.escapeHTML(String(value));
1341
1372
  const safeString = new String(escaped);
1342
1373
  safeString.__safe__ = true;
1343
1374
  return safeString;
@@ -1350,15 +1381,15 @@ var safe = (value) => {
1350
1381
  var escapejs = (value) => JSON.stringify(String(value)).slice(1, -1);
1351
1382
  var linebreaks = (value) => {
1352
1383
  const str = String(value);
1353
- const paragraphs = str.split(/\n\n+/);
1354
- const html = paragraphs.map((p) => `<p>${p.replace(/\n/g, "<br>")}</p>`).join(`
1384
+ const paragraphs = str.split(DOUBLE_NEWLINE_REGEX);
1385
+ const html = paragraphs.map((p) => `<p>${p.replace(NEWLINE_REGEX, "<br>")}</p>`).join(`
1355
1386
  `);
1356
1387
  const safeString = new String(html);
1357
1388
  safeString.__safe__ = true;
1358
1389
  return safeString;
1359
1390
  };
1360
1391
  var linebreaksbr = (value) => {
1361
- const html = String(value).replace(/\n/g, "<br>");
1392
+ const html = String(value).replace(NEWLINE_REGEX, "<br>");
1362
1393
  const safeString = new String(html);
1363
1394
  safeString.__safe__ = true;
1364
1395
  return safeString;
@@ -1370,12 +1401,12 @@ var truncatechars = (value, length = 30) => {
1370
1401
  return str.slice(0, length - 3) + "...";
1371
1402
  };
1372
1403
  var truncatewords = (value, count = 15) => {
1373
- const words = String(value).split(/\s+/);
1404
+ const words = String(value).split(WHITESPACE_REGEX);
1374
1405
  if (words.length <= count)
1375
1406
  return value;
1376
1407
  return words.slice(0, count).join(" ") + "...";
1377
1408
  };
1378
- var wordcount = (value) => String(value).split(/\s+/).filter(Boolean).length;
1409
+ var wordcount = (value) => String(value).split(WHITESPACE_REGEX).filter(Boolean).length;
1379
1410
  var center = (value, width = 80) => {
1380
1411
  const str = String(value);
1381
1412
  const padding = Math.max(0, width - str.length);
@@ -1386,7 +1417,7 @@ var center = (value, width = 80) => {
1386
1417
  var ljust = (value, width = 80) => String(value).padEnd(width);
1387
1418
  var rjust = (value, width = 80) => String(value).padStart(width);
1388
1419
  var cut = (value, arg = "") => String(value).split(arg).join("");
1389
- var slugify = (value) => String(value).toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1420
+ var slugify = (value) => String(value).toLowerCase().replace(SLUGIFY_NON_WORD_REGEX, "").replace(SLUGIFY_SPACES_REGEX, "-").replace(SLUGIFY_TRIM_REGEX, "");
1390
1421
  var abs = (value) => Math.abs(Number(value));
1391
1422
  var round = (value, precision = 0) => Number(Number(value).toFixed(precision));
1392
1423
  var int = (value) => parseInt(String(value), 10) || 0;
@@ -1426,8 +1457,12 @@ var length = (value) => {
1426
1457
  return 0;
1427
1458
  if (typeof value === "string" || Array.isArray(value))
1428
1459
  return value.length;
1429
- if (typeof value === "object")
1430
- return Object.keys(value).length;
1460
+ if (typeof value === "object") {
1461
+ let count = 0;
1462
+ for (const _ in value)
1463
+ count++;
1464
+ return count;
1465
+ }
1431
1466
  return 0;
1432
1467
  };
1433
1468
  var length_is = (value, len) => length(value) === Number(len);
@@ -1507,58 +1542,55 @@ var columns = (value, cols) => {
1507
1542
  }
1508
1543
  return result;
1509
1544
  };
1545
+ var formatDateChar = (d, char) => {
1546
+ switch (char) {
1547
+ case "d":
1548
+ return String(d.getDate()).padStart(2, "0");
1549
+ case "j":
1550
+ return String(d.getDate());
1551
+ case "D":
1552
+ return DAY_NAMES_SHORT[d.getDay()];
1553
+ case "l":
1554
+ return DAY_NAMES_LONG[d.getDay()];
1555
+ case "m":
1556
+ return String(d.getMonth() + 1).padStart(2, "0");
1557
+ case "n":
1558
+ return String(d.getMonth() + 1);
1559
+ case "M":
1560
+ return MONTH_NAMES_SHORT[d.getMonth()];
1561
+ case "F":
1562
+ return MONTH_NAMES_LONG[d.getMonth()];
1563
+ case "N":
1564
+ return MONTH_NAMES_AP[d.getMonth()];
1565
+ case "y":
1566
+ return String(d.getFullYear()).slice(-2);
1567
+ case "Y":
1568
+ return String(d.getFullYear());
1569
+ case "H":
1570
+ return String(d.getHours()).padStart(2, "0");
1571
+ case "G":
1572
+ return String(d.getHours());
1573
+ case "i":
1574
+ return String(d.getMinutes()).padStart(2, "0");
1575
+ case "s":
1576
+ return String(d.getSeconds()).padStart(2, "0");
1577
+ case "a":
1578
+ return d.getHours() < 12 ? "a.m." : "p.m.";
1579
+ case "A":
1580
+ return d.getHours() < 12 ? "AM" : "PM";
1581
+ case "g":
1582
+ return String(d.getHours() % 12 || 12);
1583
+ case "h":
1584
+ return String(d.getHours() % 12 || 12).padStart(2, "0");
1585
+ default:
1586
+ return char;
1587
+ }
1588
+ };
1510
1589
  var date = (value, format = "N j, Y") => {
1511
1590
  const d = value instanceof Date ? value : new Date(value);
1512
1591
  if (isNaN(d.getTime()))
1513
1592
  return "";
1514
- const formatMap = {
1515
- d: () => String(d.getDate()).padStart(2, "0"),
1516
- j: () => String(d.getDate()),
1517
- D: () => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()],
1518
- l: () => ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][d.getDay()],
1519
- m: () => String(d.getMonth() + 1).padStart(2, "0"),
1520
- n: () => String(d.getMonth() + 1),
1521
- M: () => ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()],
1522
- F: () => [
1523
- "January",
1524
- "February",
1525
- "March",
1526
- "April",
1527
- "May",
1528
- "June",
1529
- "July",
1530
- "August",
1531
- "September",
1532
- "October",
1533
- "November",
1534
- "December"
1535
- ][d.getMonth()],
1536
- N: () => [
1537
- "Jan.",
1538
- "Feb.",
1539
- "March",
1540
- "April",
1541
- "May",
1542
- "June",
1543
- "July",
1544
- "Aug.",
1545
- "Sept.",
1546
- "Oct.",
1547
- "Nov.",
1548
- "Dec."
1549
- ][d.getMonth()],
1550
- y: () => String(d.getFullYear()).slice(-2),
1551
- Y: () => String(d.getFullYear()),
1552
- H: () => String(d.getHours()).padStart(2, "0"),
1553
- G: () => String(d.getHours()),
1554
- i: () => String(d.getMinutes()).padStart(2, "0"),
1555
- s: () => String(d.getSeconds()).padStart(2, "0"),
1556
- a: () => d.getHours() < 12 ? "a.m." : "p.m.",
1557
- A: () => d.getHours() < 12 ? "AM" : "PM",
1558
- g: () => String(d.getHours() % 12 || 12),
1559
- h: () => String(d.getHours() % 12 || 12).padStart(2, "0")
1560
- };
1561
- return format.replace(/[a-zA-Z]/g, (char) => formatMap[char]?.() ?? char);
1593
+ return format.replace(DATE_CHAR_REGEX, (char) => formatDateChar(d, char));
1562
1594
  };
1563
1595
  var time = (value, format = "H:i") => date(value, format);
1564
1596
  var timesince = (value, now = new Date) => {
@@ -1621,8 +1653,7 @@ var pluralize = (value, arg = "s") => {
1621
1653
  };
1622
1654
  var urlencode = (value) => encodeURIComponent(String(value));
1623
1655
  var urlize = (value) => {
1624
- const urlRegex = /(https?:\/\/[^\s]+)/g;
1625
- const html = String(value).replace(urlRegex, '<a href="$1">$1</a>');
1656
+ const html = String(value).replace(URLIZE_REGEX, '<a href="$1">$1</a>');
1626
1657
  const safeString = new String(html);
1627
1658
  safeString.__safe__ = true;
1628
1659
  return safeString;
@@ -1809,8 +1840,11 @@ var empty = (value) => {
1809
1840
  return true;
1810
1841
  if (typeof value === "string" || Array.isArray(value))
1811
1842
  return value.length === 0;
1812
- if (typeof value === "object")
1813
- return Object.keys(value).length === 0;
1843
+ if (typeof value === "object") {
1844
+ for (const _ in value)
1845
+ return false;
1846
+ return true;
1847
+ }
1814
1848
  return false;
1815
1849
  };
1816
1850
  var in_ = (value, container) => {
@@ -1837,8 +1871,11 @@ var truthy = (value) => {
1837
1871
  return value.length > 0;
1838
1872
  if (Array.isArray(value))
1839
1873
  return value.length > 0;
1840
- if (typeof value === "object")
1841
- return Object.keys(value).length > 0;
1874
+ if (typeof value === "object") {
1875
+ for (const _ in value)
1876
+ return true;
1877
+ return false;
1878
+ }
1842
1879
  return true;
1843
1880
  };
1844
1881
  var falsy = (value) => !truthy(value);
@@ -1906,91 +1943,154 @@ class Runtime {
1906
1943
  const ctx = new Context({ ...this.options.globals, ...context });
1907
1944
  this.blocks.clear();
1908
1945
  this.parentTemplate = null;
1909
- await this.collectBlocks(ast, ctx);
1910
- if (this.parentTemplate) {
1911
- return this.renderTemplate(this.parentTemplate, ctx);
1946
+ const needsAsync = this.templateNeedsAsync(ast);
1947
+ if (needsAsync) {
1948
+ await this.collectBlocks(ast, ctx);
1949
+ if (this.parentTemplate) {
1950
+ return this.renderTemplateAsync(this.parentTemplate, ctx);
1951
+ }
1952
+ return this.renderTemplateAsync(ast, ctx);
1912
1953
  }
1913
- return this.renderTemplate(ast, ctx);
1954
+ this.collectBlocksSync(ast);
1955
+ return this.renderTemplateSync(ast, ctx);
1914
1956
  }
1915
- async collectBlocks(ast, ctx) {
1957
+ templateNeedsAsync(ast) {
1916
1958
  for (const node of ast.body) {
1917
- if (node.type === "Extends") {
1918
- const templateName = await this.evaluate(node.template, ctx);
1919
- this.parentTemplate = await this.options.templateLoader(String(templateName));
1920
- await this.collectBlocks(this.parentTemplate, ctx);
1921
- } else if (node.type === "Block") {
1959
+ if (node.type === "Extends" || node.type === "Include")
1960
+ return true;
1961
+ if (node.type === "If") {
1962
+ const ifNode = node;
1963
+ if (this.nodesNeedAsync(ifNode.body))
1964
+ return true;
1965
+ for (const elif of ifNode.elifs) {
1966
+ if (this.nodesNeedAsync(elif.body))
1967
+ return true;
1968
+ }
1969
+ if (this.nodesNeedAsync(ifNode.else_))
1970
+ return true;
1971
+ }
1972
+ if (node.type === "For") {
1973
+ const forNode = node;
1974
+ if (this.nodesNeedAsync(forNode.body))
1975
+ return true;
1976
+ if (this.nodesNeedAsync(forNode.else_))
1977
+ return true;
1978
+ }
1979
+ if (node.type === "Block") {
1980
+ if (this.nodesNeedAsync(node.body))
1981
+ return true;
1982
+ }
1983
+ if (node.type === "With") {
1984
+ if (this.nodesNeedAsync(node.body))
1985
+ return true;
1986
+ }
1987
+ }
1988
+ return false;
1989
+ }
1990
+ nodesNeedAsync(nodes2) {
1991
+ for (const node of nodes2) {
1992
+ if (node.type === "Extends" || node.type === "Include")
1993
+ return true;
1994
+ if (node.type === "If") {
1995
+ const ifNode = node;
1996
+ if (this.nodesNeedAsync(ifNode.body))
1997
+ return true;
1998
+ for (const elif of ifNode.elifs) {
1999
+ if (this.nodesNeedAsync(elif.body))
2000
+ return true;
2001
+ }
2002
+ if (this.nodesNeedAsync(ifNode.else_))
2003
+ return true;
2004
+ }
2005
+ if (node.type === "For") {
2006
+ const forNode = node;
2007
+ if (this.nodesNeedAsync(forNode.body))
2008
+ return true;
2009
+ if (this.nodesNeedAsync(forNode.else_))
2010
+ return true;
2011
+ }
2012
+ if (node.type === "Block") {
2013
+ if (this.nodesNeedAsync(node.body))
2014
+ return true;
2015
+ }
2016
+ if (node.type === "With") {
2017
+ if (this.nodesNeedAsync(node.body))
2018
+ return true;
2019
+ }
2020
+ }
2021
+ return false;
2022
+ }
2023
+ collectBlocksSync(ast) {
2024
+ for (const node of ast.body) {
2025
+ if (node.type === "Block") {
1922
2026
  this.blocks.set(node.name, node);
1923
2027
  }
1924
2028
  }
1925
2029
  }
1926
- async renderTemplate(ast, ctx) {
2030
+ renderTemplateSync(ast, ctx) {
1927
2031
  const parts = [];
1928
2032
  for (const node of ast.body) {
1929
- const result = await this.renderNode(node, ctx);
2033
+ const result = this.renderNodeSync(node, ctx);
1930
2034
  if (result !== null)
1931
2035
  parts.push(result);
1932
2036
  }
1933
2037
  return parts.join("");
1934
2038
  }
1935
- async renderNode(node, ctx) {
2039
+ renderNodeSync(node, ctx) {
1936
2040
  switch (node.type) {
1937
2041
  case "Text":
1938
2042
  return node.value;
1939
2043
  case "Output":
1940
- return this.renderOutput(node, ctx);
2044
+ return this.stringify(this.eval(node.expression, ctx));
1941
2045
  case "If":
1942
- return this.renderIf(node, ctx);
2046
+ return this.renderIfSync(node, ctx);
1943
2047
  case "For":
1944
- return this.renderFor(node, ctx);
2048
+ return this.renderForSync(node, ctx);
1945
2049
  case "Block":
1946
- return this.renderBlock(node, ctx);
1947
- case "Extends":
1948
- return null;
1949
- case "Include":
1950
- return this.renderInclude(node, ctx);
2050
+ return this.renderBlockSync(node, ctx);
1951
2051
  case "Set":
1952
- return this.renderSet(node, ctx);
2052
+ ctx.set(node.target, this.eval(node.value, ctx));
2053
+ return "";
1953
2054
  case "With":
1954
- return this.renderWith(node, ctx);
1955
- case "Load":
1956
- return null;
2055
+ return this.renderWithSync(node, ctx);
1957
2056
  case "Url":
1958
- return this.renderUrl(node, ctx);
2057
+ return this.renderUrlSync(node, ctx);
1959
2058
  case "Static":
1960
- return this.renderStatic(node, ctx);
2059
+ return this.renderStaticSync(node, ctx);
2060
+ case "Load":
2061
+ case "Extends":
2062
+ return null;
1961
2063
  default:
1962
2064
  return null;
1963
2065
  }
1964
2066
  }
1965
- async renderOutput(node, ctx) {
1966
- const value = await this.evaluate(node.expression, ctx);
1967
- return this.stringify(value);
1968
- }
1969
- async renderIf(node, ctx) {
1970
- if (this.isTruthy(await this.evaluate(node.test, ctx))) {
1971
- return this.renderNodes(node.body, ctx);
2067
+ renderIfSync(node, ctx) {
2068
+ if (this.isTruthy(this.eval(node.test, ctx))) {
2069
+ return this.renderNodesSync(node.body, ctx);
1972
2070
  }
1973
2071
  for (const elif of node.elifs) {
1974
- if (this.isTruthy(await this.evaluate(elif.test, ctx))) {
1975
- return this.renderNodes(elif.body, ctx);
2072
+ if (this.isTruthy(this.eval(elif.test, ctx))) {
2073
+ return this.renderNodesSync(elif.body, ctx);
1976
2074
  }
1977
2075
  }
1978
- if (node.else_.length > 0) {
1979
- return this.renderNodes(node.else_, ctx);
1980
- }
1981
- return "";
2076
+ return node.else_.length > 0 ? this.renderNodesSync(node.else_, ctx) : "";
1982
2077
  }
1983
- async renderFor(node, ctx) {
1984
- const iterable2 = await this.evaluate(node.iter, ctx);
2078
+ renderForSync(node, ctx) {
2079
+ const iterable2 = this.eval(node.iter, ctx);
1985
2080
  const items = this.toIterable(iterable2);
1986
- if (items.length === 0) {
1987
- return this.renderNodes(node.else_, ctx);
2081
+ const len = items.length;
2082
+ if (len === 0) {
2083
+ return this.renderNodesSync(node.else_, ctx);
1988
2084
  }
1989
- const parts = [];
2085
+ const parts = new Array(len);
2086
+ const isUnpacking = Array.isArray(node.target);
1990
2087
  ctx.push();
1991
- for (let i = 0;i < items.length; i++) {
2088
+ ctx.pushForLoop(items, 0);
2089
+ for (let i = 0;i < len; i++) {
1992
2090
  const item = items[i];
1993
- if (Array.isArray(node.target)) {
2091
+ if (i > 0)
2092
+ ctx.updateForLoop(i, items);
2093
+ if (isUnpacking) {
1994
2094
  let values;
1995
2095
  if (Array.isArray(item)) {
1996
2096
  values = item;
@@ -1999,68 +2099,42 @@ class Runtime {
1999
2099
  } else {
2000
2100
  values = [item, item];
2001
2101
  }
2002
- node.target.forEach((name, idx) => {
2003
- ctx.set(name, values[idx]);
2004
- });
2102
+ const targets = node.target;
2103
+ for (let j = 0;j < targets.length; j++) {
2104
+ ctx.set(targets[j], values[j]);
2105
+ }
2005
2106
  } else {
2006
2107
  ctx.set(node.target, item);
2007
2108
  }
2008
- ctx.pushForLoop(items, i);
2009
- const result = await this.renderNodes(node.body, ctx);
2010
- parts.push(result);
2011
- ctx.popForLoop();
2109
+ parts[i] = this.renderNodesSync(node.body, ctx);
2012
2110
  }
2111
+ ctx.popForLoop();
2013
2112
  ctx.pop();
2014
2113
  return parts.join("");
2015
2114
  }
2016
- async renderBlock(node, ctx) {
2115
+ renderBlockSync(node, ctx) {
2017
2116
  const blockToRender = this.blocks.get(node.name) || node;
2018
2117
  ctx.push();
2019
2118
  ctx.set("block", {
2020
- super: async () => {
2021
- return this.renderNodes(node.body, ctx);
2022
- }
2119
+ super: () => this.renderNodesSync(node.body, ctx)
2023
2120
  });
2024
- const result = await this.renderNodes(blockToRender.body, ctx);
2121
+ const result = this.renderNodesSync(blockToRender.body, ctx);
2025
2122
  ctx.pop();
2026
2123
  return result;
2027
2124
  }
2028
- async renderInclude(node, ctx) {
2029
- try {
2030
- const templateName = await this.evaluate(node.template, ctx);
2031
- const template = await this.options.templateLoader(String(templateName));
2032
- let includeCtx;
2033
- if (node.only) {
2034
- includeCtx = new Context(node.context ? await this.evaluateObject(node.context, ctx) : {});
2035
- } else {
2036
- const additional = node.context ? await this.evaluateObject(node.context, ctx) : {};
2037
- includeCtx = ctx.derived(additional);
2038
- }
2039
- return this.renderTemplate(template, includeCtx);
2040
- } catch (error) {
2041
- if (node.ignoreMissing)
2042
- return "";
2043
- throw error;
2044
- }
2045
- }
2046
- async renderSet(node, ctx) {
2047
- const value = await this.evaluate(node.value, ctx);
2048
- ctx.set(node.target, value);
2049
- return "";
2050
- }
2051
- async renderWith(node, ctx) {
2125
+ renderWithSync(node, ctx) {
2052
2126
  ctx.push();
2053
2127
  for (const { target, value } of node.assignments) {
2054
- ctx.set(target, await this.evaluate(value, ctx));
2128
+ ctx.set(target, this.eval(value, ctx));
2055
2129
  }
2056
- const result = await this.renderNodes(node.body, ctx);
2130
+ const result = this.renderNodesSync(node.body, ctx);
2057
2131
  ctx.pop();
2058
2132
  return result;
2059
2133
  }
2060
- async renderUrl(node, ctx) {
2061
- const name = await this.evaluate(node.name, ctx);
2062
- const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2063
- const kwargs = await this.evaluateObject(node.kwargs, ctx);
2134
+ renderUrlSync(node, ctx) {
2135
+ const name = this.eval(node.name, ctx);
2136
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2137
+ const kwargs = this.evalObjectSync(node.kwargs, ctx);
2064
2138
  const url = this.options.urlResolver(String(name), args, kwargs);
2065
2139
  if (node.asVar) {
2066
2140
  ctx.set(node.asVar, url);
@@ -2068,8 +2142,8 @@ class Runtime {
2068
2142
  }
2069
2143
  return url;
2070
2144
  }
2071
- async renderStatic(node, ctx) {
2072
- const path = await this.evaluate(node.path, ctx);
2145
+ renderStaticSync(node, ctx) {
2146
+ const path = this.eval(node.path, ctx);
2073
2147
  const url = this.options.staticResolver(String(path));
2074
2148
  if (node.asVar) {
2075
2149
  ctx.set(node.asVar, url);
@@ -2077,114 +2151,86 @@ class Runtime {
2077
2151
  }
2078
2152
  return url;
2079
2153
  }
2080
- async renderNodes(nodes2, ctx) {
2154
+ renderNodesSync(nodes2, ctx) {
2081
2155
  const parts = [];
2082
2156
  for (const node of nodes2) {
2083
- const result = await this.renderNode(node, ctx);
2157
+ const result = this.renderNodeSync(node, ctx);
2084
2158
  if (result !== null)
2085
2159
  parts.push(result);
2086
2160
  }
2087
2161
  return parts.join("");
2088
2162
  }
2089
- async evaluate(node, ctx) {
2163
+ eval(node, ctx) {
2090
2164
  switch (node.type) {
2091
2165
  case "Literal":
2092
2166
  return node.value;
2093
2167
  case "Name":
2094
2168
  return ctx.get(node.name);
2095
2169
  case "GetAttr":
2096
- return this.evaluateGetAttr(node, ctx);
2170
+ return this.evalGetAttr(node, ctx);
2097
2171
  case "GetItem":
2098
- return this.evaluateGetItem(node, ctx);
2172
+ return this.evalGetItem(node, ctx);
2099
2173
  case "FilterExpr":
2100
- return this.evaluateFilter(node, ctx);
2174
+ return this.evalFilter(node, ctx);
2101
2175
  case "BinaryOp":
2102
- return this.evaluateBinaryOp(node, ctx);
2176
+ return this.evalBinaryOp(node, ctx);
2103
2177
  case "UnaryOp":
2104
- return this.evaluateUnaryOp(node, ctx);
2178
+ return this.evalUnaryOp(node, ctx);
2105
2179
  case "Compare":
2106
- return this.evaluateCompare(node, ctx);
2180
+ return this.evalCompare(node, ctx);
2107
2181
  case "Conditional":
2108
- return this.evaluateConditional(node, ctx);
2182
+ return this.evalConditional(node, ctx);
2109
2183
  case "Array":
2110
- return Promise.all(node.elements.map((el) => this.evaluate(el, ctx)));
2184
+ return node.elements.map((el) => this.eval(el, ctx));
2111
2185
  case "Object":
2112
- return this.evaluateObjectLiteral(node, ctx);
2186
+ return this.evalObjectLiteral(node, ctx);
2113
2187
  case "FunctionCall":
2114
- return this.evaluateFunctionCall(node, ctx);
2188
+ return this.evalFunctionCall(node, ctx);
2115
2189
  case "TestExpr":
2116
- return this.evaluateTest(node, ctx);
2190
+ return this.evalTest(node, ctx);
2117
2191
  default:
2118
2192
  return;
2119
2193
  }
2120
2194
  }
2121
- async evaluateTest(node, ctx) {
2122
- if (node.test === "defined" || node.test === "undefined") {
2123
- let isDefined = false;
2124
- if (node.node.type === "Name") {
2125
- isDefined = ctx.has(node.node.name);
2126
- } else {
2127
- const value2 = await this.evaluate(node.node, ctx);
2128
- isDefined = value2 !== undefined;
2129
- }
2130
- const result2 = node.test === "defined" ? isDefined : !isDefined;
2131
- return node.negated ? !result2 : result2;
2132
- }
2133
- const value = await this.evaluate(node.node, ctx);
2134
- const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2135
- const test = this.tests[node.test];
2136
- if (!test) {
2137
- throw new Error(`Unknown test: ${node.test}`);
2138
- }
2139
- const result = test(value, ...args);
2140
- return node.negated ? !result : result;
2141
- }
2142
- async evaluateGetAttr(node, ctx) {
2143
- const obj = await this.evaluate(node.object, ctx);
2195
+ evalGetAttr(node, ctx) {
2196
+ const obj = this.eval(node.object, ctx);
2144
2197
  if (obj == null)
2145
2198
  return;
2146
2199
  const numIndex = parseInt(node.attribute, 10);
2147
- if (!isNaN(numIndex) && Array.isArray(obj)) {
2200
+ if (!isNaN(numIndex) && Array.isArray(obj))
2148
2201
  return obj[numIndex];
2149
- }
2150
2202
  if (typeof obj === "object" && node.attribute in obj) {
2151
2203
  const value = obj[node.attribute];
2152
- if (typeof value === "function") {
2153
- return value.call(obj);
2154
- }
2155
- return value;
2204
+ return typeof value === "function" ? value.call(obj) : value;
2156
2205
  }
2157
2206
  if (typeof obj[node.attribute] === "function") {
2158
2207
  return obj[node.attribute].bind(obj);
2159
2208
  }
2160
2209
  return;
2161
2210
  }
2162
- async evaluateGetItem(node, ctx) {
2163
- const obj = await this.evaluate(node.object, ctx);
2164
- const index = await this.evaluate(node.index, ctx);
2211
+ evalGetItem(node, ctx) {
2212
+ const obj = this.eval(node.object, ctx);
2213
+ const index = this.eval(node.index, ctx);
2165
2214
  if (obj == null)
2166
2215
  return;
2167
2216
  return obj[index];
2168
2217
  }
2169
- async evaluateFilter(node, ctx) {
2170
- const value = await this.evaluate(node.node, ctx);
2171
- const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2172
- const kwargs = await this.evaluateObject(node.kwargs, ctx);
2218
+ evalFilter(node, ctx) {
2219
+ const value = this.eval(node.node, ctx);
2220
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2221
+ const kwargs = this.evalObjectSync(node.kwargs, ctx);
2173
2222
  const filter = this.filters[node.filter];
2174
- if (!filter) {
2223
+ if (!filter)
2175
2224
  throw new Error(`Unknown filter: ${node.filter}`);
2176
- }
2177
2225
  return filter(value, ...args, ...Object.values(kwargs));
2178
2226
  }
2179
- async evaluateBinaryOp(node, ctx) {
2180
- const left = await this.evaluate(node.left, ctx);
2181
- if (node.operator === "and") {
2182
- return this.isTruthy(left) ? await this.evaluate(node.right, ctx) : left;
2183
- }
2184
- if (node.operator === "or") {
2185
- return this.isTruthy(left) ? left : await this.evaluate(node.right, ctx);
2186
- }
2187
- const right = await this.evaluate(node.right, ctx);
2227
+ evalBinaryOp(node, ctx) {
2228
+ const left = this.eval(node.left, ctx);
2229
+ if (node.operator === "and")
2230
+ return this.isTruthy(left) ? this.eval(node.right, ctx) : left;
2231
+ if (node.operator === "or")
2232
+ return this.isTruthy(left) ? left : this.eval(node.right, ctx);
2233
+ const right = this.eval(node.right, ctx);
2188
2234
  switch (node.operator) {
2189
2235
  case "+":
2190
2236
  return typeof left === "string" || typeof right === "string" ? String(left) + String(right) : Number(left) + Number(right);
@@ -2193,20 +2239,17 @@ class Runtime {
2193
2239
  case "*":
2194
2240
  return Number(left) * Number(right);
2195
2241
  case "/":
2196
- const divisor = Number(right);
2197
- if (divisor === 0)
2198
- return 0;
2199
- return Number(left) / divisor;
2242
+ return Number(left) / Number(right);
2200
2243
  case "%":
2201
- return Number(left) % Number(right);
2244
+ return Number(right) === 0 ? NaN : Number(left) % Number(right);
2202
2245
  case "~":
2203
2246
  return String(left) + String(right);
2204
2247
  default:
2205
2248
  return;
2206
2249
  }
2207
2250
  }
2208
- async evaluateUnaryOp(node, ctx) {
2209
- const operand = await this.evaluate(node.operand, ctx);
2251
+ evalUnaryOp(node, ctx) {
2252
+ const operand = this.eval(node.operand, ctx);
2210
2253
  switch (node.operator) {
2211
2254
  case "not":
2212
2255
  return !this.isTruthy(operand);
@@ -2218,10 +2261,10 @@ class Runtime {
2218
2261
  return operand;
2219
2262
  }
2220
2263
  }
2221
- async evaluateCompare(node, ctx) {
2222
- let left = await this.evaluate(node.left, ctx);
2264
+ evalCompare(node, ctx) {
2265
+ let left = this.eval(node.left, ctx);
2223
2266
  for (const { operator, right: rightNode } of node.ops) {
2224
- const right = await this.evaluate(rightNode, ctx);
2267
+ const right = this.eval(rightNode, ctx);
2225
2268
  let result;
2226
2269
  switch (operator) {
2227
2270
  case "==":
@@ -2263,34 +2306,197 @@ class Runtime {
2263
2306
  }
2264
2307
  return true;
2265
2308
  }
2266
- async evaluateConditional(node, ctx) {
2267
- const test = await this.evaluate(node.test, ctx);
2268
- return this.isTruthy(test) ? await this.evaluate(node.trueExpr, ctx) : await this.evaluate(node.falseExpr, ctx);
2309
+ evalConditional(node, ctx) {
2310
+ return this.isTruthy(this.eval(node.test, ctx)) ? this.eval(node.trueExpr, ctx) : this.eval(node.falseExpr, ctx);
2269
2311
  }
2270
- async evaluateObjectLiteral(node, ctx) {
2312
+ evalObjectLiteral(node, ctx) {
2271
2313
  const result = {};
2272
2314
  for (const { key, value } of node.pairs) {
2273
- const k = await this.evaluate(key, ctx);
2274
- result[String(k)] = await this.evaluate(value, ctx);
2315
+ result[String(this.eval(key, ctx))] = this.eval(value, ctx);
2275
2316
  }
2276
2317
  return result;
2277
2318
  }
2278
- async evaluateFunctionCall(node, ctx) {
2279
- const callee = await this.evaluate(node.callee, ctx);
2280
- const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2281
- const kwargs = await this.evaluateObject(node.kwargs, ctx);
2282
- if (typeof callee === "function") {
2283
- return callee(...args, kwargs);
2319
+ evalFunctionCall(node, ctx) {
2320
+ const callee = this.eval(node.callee, ctx);
2321
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2322
+ const kwargs = this.evalObjectSync(node.kwargs, ctx);
2323
+ return typeof callee === "function" ? callee(...args, kwargs) : undefined;
2324
+ }
2325
+ evalTest(node, ctx) {
2326
+ if (node.test === "defined" || node.test === "undefined") {
2327
+ let isDefined = false;
2328
+ if (node.node.type === "Name") {
2329
+ isDefined = ctx.has(node.node.name);
2330
+ } else {
2331
+ isDefined = this.eval(node.node, ctx) !== undefined;
2332
+ }
2333
+ const result2 = node.test === "defined" ? isDefined : !isDefined;
2334
+ return node.negated ? !result2 : result2;
2284
2335
  }
2285
- return;
2336
+ const value = this.eval(node.node, ctx);
2337
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2338
+ const test = this.tests[node.test];
2339
+ if (!test)
2340
+ throw new Error(`Unknown test: ${node.test}`);
2341
+ const result = test(value, ...args);
2342
+ return node.negated ? !result : result;
2286
2343
  }
2287
- async evaluateObject(obj, ctx) {
2344
+ evalObjectSync(obj, ctx) {
2288
2345
  const result = {};
2289
2346
  for (const [key, value] of Object.entries(obj)) {
2290
- result[key] = await this.evaluate(value, ctx);
2347
+ result[key] = this.eval(value, ctx);
2348
+ }
2349
+ return result;
2350
+ }
2351
+ async collectBlocks(ast, ctx) {
2352
+ for (const node of ast.body) {
2353
+ if (node.type === "Extends") {
2354
+ const templateName = this.eval(node.template, ctx);
2355
+ this.parentTemplate = await this.options.templateLoader(String(templateName));
2356
+ await this.collectBlocks(this.parentTemplate, ctx);
2357
+ } else if (node.type === "Block") {
2358
+ this.blocks.set(node.name, node);
2359
+ }
2360
+ }
2361
+ }
2362
+ async renderTemplateAsync(ast, ctx) {
2363
+ const parts = [];
2364
+ for (const node of ast.body) {
2365
+ const result = await this.renderNodeAsync(node, ctx);
2366
+ if (result !== null)
2367
+ parts.push(result);
2368
+ }
2369
+ return parts.join("");
2370
+ }
2371
+ async renderNodeAsync(node, ctx) {
2372
+ switch (node.type) {
2373
+ case "Text":
2374
+ return node.value;
2375
+ case "Output":
2376
+ return this.stringify(this.eval(node.expression, ctx));
2377
+ case "If":
2378
+ return this.renderIfAsync(node, ctx);
2379
+ case "For":
2380
+ return this.renderForAsync(node, ctx);
2381
+ case "Block":
2382
+ return this.renderBlockAsync(node, ctx);
2383
+ case "Extends":
2384
+ return null;
2385
+ case "Include":
2386
+ return this.renderInclude(node, ctx);
2387
+ case "Set":
2388
+ ctx.set(node.target, this.eval(node.value, ctx));
2389
+ return "";
2390
+ case "With":
2391
+ return this.renderWithAsync(node, ctx);
2392
+ case "Load":
2393
+ return null;
2394
+ case "Url":
2395
+ return this.renderUrlSync(node, ctx);
2396
+ case "Static":
2397
+ return this.renderStaticSync(node, ctx);
2398
+ default:
2399
+ return null;
2400
+ }
2401
+ }
2402
+ async renderIfAsync(node, ctx) {
2403
+ if (this.isTruthy(this.eval(node.test, ctx))) {
2404
+ return this.renderNodesAsync(node.body, ctx);
2405
+ }
2406
+ for (const elif of node.elifs) {
2407
+ if (this.isTruthy(this.eval(elif.test, ctx))) {
2408
+ return this.renderNodesAsync(elif.body, ctx);
2409
+ }
2410
+ }
2411
+ return node.else_.length > 0 ? this.renderNodesAsync(node.else_, ctx) : "";
2412
+ }
2413
+ async renderForAsync(node, ctx) {
2414
+ const iterable2 = this.eval(node.iter, ctx);
2415
+ const items = this.toIterable(iterable2);
2416
+ const len = items.length;
2417
+ if (len === 0) {
2418
+ return this.renderNodesAsync(node.else_, ctx);
2419
+ }
2420
+ const parts = new Array(len);
2421
+ const isUnpacking = Array.isArray(node.target);
2422
+ ctx.push();
2423
+ ctx.pushForLoop(items, 0);
2424
+ for (let i = 0;i < len; i++) {
2425
+ const item = items[i];
2426
+ if (i > 0)
2427
+ ctx.updateForLoop(i, items);
2428
+ if (isUnpacking) {
2429
+ let values;
2430
+ if (Array.isArray(item)) {
2431
+ values = item;
2432
+ } else if (item && typeof item === "object" && (("0" in item) || ("key" in item))) {
2433
+ values = [item[0] ?? item.key, item[1] ?? item.value];
2434
+ } else {
2435
+ values = [item, item];
2436
+ }
2437
+ const targets = node.target;
2438
+ for (let j = 0;j < targets.length; j++) {
2439
+ ctx.set(targets[j], values[j]);
2440
+ }
2441
+ } else {
2442
+ ctx.set(node.target, item);
2443
+ }
2444
+ parts[i] = await this.renderNodesAsync(node.body, ctx);
2445
+ }
2446
+ ctx.popForLoop();
2447
+ ctx.pop();
2448
+ return parts.join("");
2449
+ }
2450
+ async renderBlockAsync(node, ctx) {
2451
+ const blockToRender = this.blocks.get(node.name) || node;
2452
+ ctx.push();
2453
+ const parentContent = await this.renderNodesAsync(node.body, ctx);
2454
+ ctx.set("block", {
2455
+ super: () => parentContent
2456
+ });
2457
+ const result = await this.renderNodesAsync(blockToRender.body, ctx);
2458
+ ctx.pop();
2459
+ return result;
2460
+ }
2461
+ async renderInclude(node, ctx) {
2462
+ try {
2463
+ const templateName = this.eval(node.template, ctx);
2464
+ const template = await this.options.templateLoader(String(templateName));
2465
+ let includeCtx;
2466
+ if (node.only) {
2467
+ includeCtx = new Context(node.context ? this.evalObjectSync(node.context, ctx) : {});
2468
+ } else {
2469
+ const additional = node.context ? this.evalObjectSync(node.context, ctx) : {};
2470
+ includeCtx = ctx.derived(additional);
2471
+ }
2472
+ return this.renderTemplateAsync(template, includeCtx);
2473
+ } catch (error) {
2474
+ if (node.ignoreMissing)
2475
+ return "";
2476
+ throw error;
2291
2477
  }
2478
+ }
2479
+ async renderWithAsync(node, ctx) {
2480
+ ctx.push();
2481
+ for (const { target, value } of node.assignments) {
2482
+ ctx.set(target, this.eval(value, ctx));
2483
+ }
2484
+ const result = await this.renderNodesAsync(node.body, ctx);
2485
+ ctx.pop();
2292
2486
  return result;
2293
2487
  }
2488
+ async renderNodesAsync(nodes2, ctx) {
2489
+ const parts = [];
2490
+ for (const node of nodes2) {
2491
+ const result = await this.renderNodeAsync(node, ctx);
2492
+ if (result !== null)
2493
+ parts.push(result);
2494
+ }
2495
+ return parts.join("");
2496
+ }
2497
+ async evaluate(node, ctx) {
2498
+ return this.eval(node, ctx);
2499
+ }
2294
2500
  stringify(value) {
2295
2501
  if (value == null)
2296
2502
  return "";
@@ -2300,7 +2506,7 @@ class Runtime {
2300
2506
  if (value.__safe__)
2301
2507
  return str;
2302
2508
  if (this.options.autoescape) {
2303
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
2509
+ return Bun.escapeHTML(str);
2304
2510
  }
2305
2511
  return str;
2306
2512
  }
@@ -2315,8 +2521,11 @@ class Runtime {
2315
2521
  return value.length > 0;
2316
2522
  if (Array.isArray(value))
2317
2523
  return value.length > 0;
2318
- if (typeof value === "object")
2319
- return Object.keys(value).length > 0;
2524
+ if (typeof value === "object") {
2525
+ for (const _ in value)
2526
+ return true;
2527
+ return false;
2528
+ }
2320
2529
  return true;
2321
2530
  }
2322
2531
  isIn(needle, haystack) {
@@ -2354,9 +2563,441 @@ class Runtime {
2354
2563
  }
2355
2564
  }
2356
2565
 
2566
+ // src/compiler/index.ts
2567
+ function compileToString(ast, options = {}) {
2568
+ const compiler = new Compiler(options);
2569
+ return compiler.compile(ast);
2570
+ }
2571
+ function compileToFunction(ast, options = {}) {
2572
+ const code = compileToString(ast, options);
2573
+ const fn = new Function("__ctx", "__helpers", `
2574
+ const { escape, isTruthy, toArray, applyFilter, applyTest } = __helpers;
2575
+ ${code}
2576
+ return render(__ctx);
2577
+ `);
2578
+ return (ctx) => fn(ctx, runtimeHelpers);
2579
+ }
2580
+ var runtimeHelpers = {
2581
+ escape: (value) => {
2582
+ if (value == null)
2583
+ return "";
2584
+ if (typeof value === "object" && value.__safe)
2585
+ return String(value.value ?? "");
2586
+ if (value?.__safe__)
2587
+ return String(value);
2588
+ return Bun.escapeHTML(String(value));
2589
+ },
2590
+ isTruthy: (value) => {
2591
+ if (value == null)
2592
+ return false;
2593
+ if (typeof value === "boolean")
2594
+ return value;
2595
+ if (typeof value === "number")
2596
+ return value !== 0;
2597
+ if (typeof value === "string")
2598
+ return value.length > 0;
2599
+ if (Array.isArray(value))
2600
+ return value.length > 0;
2601
+ if (typeof value === "object") {
2602
+ for (const _ in value)
2603
+ return true;
2604
+ return false;
2605
+ }
2606
+ return true;
2607
+ },
2608
+ toArray: (value) => {
2609
+ if (value == null)
2610
+ return [];
2611
+ if (Array.isArray(value))
2612
+ return value;
2613
+ if (typeof value === "string")
2614
+ return value.split("");
2615
+ if (typeof value === "object") {
2616
+ if (typeof value[Symbol.iterator] === "function")
2617
+ return [...value];
2618
+ return Object.entries(value);
2619
+ }
2620
+ return [];
2621
+ },
2622
+ applyFilter: (name, value, ...args) => {
2623
+ const filter = builtinFilters[name];
2624
+ if (!filter)
2625
+ throw new Error(`Unknown filter: ${name}`);
2626
+ return filter(value, ...args);
2627
+ },
2628
+ applyTest: (name, value, ...args) => {
2629
+ const test = builtinTests[name];
2630
+ if (!test)
2631
+ throw new Error(`Unknown test: ${name}`);
2632
+ return test(value, ...args);
2633
+ }
2634
+ };
2635
+
2636
+ class Compiler {
2637
+ options;
2638
+ indent = 0;
2639
+ varCounter = 0;
2640
+ loopStack = [];
2641
+ constructor(options = {}) {
2642
+ this.options = {
2643
+ functionName: options.functionName ?? "render",
2644
+ inlineHelpers: options.inlineHelpers ?? true,
2645
+ minify: options.minify ?? false,
2646
+ autoescape: options.autoescape ?? true
2647
+ };
2648
+ }
2649
+ compile(ast) {
2650
+ const body = this.compileNodes(ast.body);
2651
+ const nl = this.options.minify ? "" : `
2652
+ `;
2653
+ return `function ${this.options.functionName}(__ctx) {${nl}` + ` let __out = '';${nl}` + body + ` return __out;${nl}` + `}`;
2654
+ }
2655
+ compileNodes(nodes2) {
2656
+ return nodes2.map((node) => this.compileNode(node)).join("");
2657
+ }
2658
+ compileNode(node) {
2659
+ switch (node.type) {
2660
+ case "Text":
2661
+ return this.compileText(node);
2662
+ case "Output":
2663
+ return this.compileOutput(node);
2664
+ case "If":
2665
+ return this.compileIf(node);
2666
+ case "For":
2667
+ return this.compileFor(node);
2668
+ case "Set":
2669
+ return this.compileSet(node);
2670
+ case "With":
2671
+ return this.compileWith(node);
2672
+ case "Comment":
2673
+ return "";
2674
+ case "Extends":
2675
+ case "Block":
2676
+ case "Include":
2677
+ throw new Error(`AOT compilation does not support '${node.type}' - use Environment.render() for templates with inheritance`);
2678
+ case "Url":
2679
+ case "Static":
2680
+ throw new Error(`AOT compilation does not support '${node.type}' tag - use Environment.render() with urlResolver/staticResolver`);
2681
+ default:
2682
+ throw new Error(`Unknown node type in AOT compiler: ${node.type}`);
2683
+ }
2684
+ }
2685
+ compileText(node) {
2686
+ const escaped = JSON.stringify(node.value);
2687
+ return ` __out += ${escaped};${this.nl()}`;
2688
+ }
2689
+ compileOutput(node) {
2690
+ const expr = this.compileExpr(node.expression);
2691
+ if (this.options.autoescape && !this.isMarkedSafe(node.expression)) {
2692
+ return ` __out += escape(${expr});${this.nl()}`;
2693
+ }
2694
+ return ` __out += (${expr}) ?? '';${this.nl()}`;
2695
+ }
2696
+ compileIf(node) {
2697
+ let code = "";
2698
+ const test = this.compileExpr(node.test);
2699
+ code += ` if (isTruthy(${test})) {${this.nl()}`;
2700
+ code += this.compileNodes(node.body);
2701
+ code += ` }`;
2702
+ for (const elif of node.elifs) {
2703
+ const elifTest = this.compileExpr(elif.test);
2704
+ code += ` else if (isTruthy(${elifTest})) {${this.nl()}`;
2705
+ code += this.compileNodes(elif.body);
2706
+ code += ` }`;
2707
+ }
2708
+ if (node.else_.length > 0) {
2709
+ code += ` else {${this.nl()}`;
2710
+ code += this.compileNodes(node.else_);
2711
+ code += ` }`;
2712
+ }
2713
+ code += this.nl();
2714
+ return code;
2715
+ }
2716
+ compileFor(node) {
2717
+ const iterVar = this.genVar("iter");
2718
+ const indexVar = this.genVar("i");
2719
+ const lenVar = this.genVar("len");
2720
+ const loopVar = this.genVar("loop");
2721
+ const itemVar = Array.isArray(node.target) ? node.target[0] : node.target;
2722
+ const valueVar = Array.isArray(node.target) && node.target[1] ? node.target[1] : null;
2723
+ const parentLoopVar = this.loopStack.length > 0 ? this.loopStack[this.loopStack.length - 1] : null;
2724
+ const iter = this.compileExpr(node.iter);
2725
+ let code = "";
2726
+ code += ` const ${iterVar} = toArray(${iter});${this.nl()}`;
2727
+ code += ` const ${lenVar} = ${iterVar}.length;${this.nl()}`;
2728
+ if (node.else_.length > 0) {
2729
+ code += ` if (${lenVar} === 0) {${this.nl()}`;
2730
+ code += this.compileNodes(node.else_);
2731
+ code += ` } else {${this.nl()}`;
2732
+ }
2733
+ code += ` for (let ${indexVar} = 0; ${indexVar} < ${lenVar}; ${indexVar}++) {${this.nl()}`;
2734
+ if (valueVar) {
2735
+ code += ` const ${itemVar} = ${iterVar}[${indexVar}][0];${this.nl()}`;
2736
+ code += ` const ${valueVar} = ${iterVar}[${indexVar}][1];${this.nl()}`;
2737
+ } else {
2738
+ code += ` const ${itemVar} = ${iterVar}[${indexVar}];${this.nl()}`;
2739
+ }
2740
+ code += ` const ${loopVar} = {${this.nl()}`;
2741
+ code += ` counter: ${indexVar} + 1,${this.nl()}`;
2742
+ code += ` counter0: ${indexVar},${this.nl()}`;
2743
+ code += ` revcounter: ${lenVar} - ${indexVar},${this.nl()}`;
2744
+ code += ` revcounter0: ${lenVar} - ${indexVar} - 1,${this.nl()}`;
2745
+ code += ` first: ${indexVar} === 0,${this.nl()}`;
2746
+ code += ` last: ${indexVar} === ${lenVar} - 1,${this.nl()}`;
2747
+ code += ` length: ${lenVar},${this.nl()}`;
2748
+ code += ` index: ${indexVar} + 1,${this.nl()}`;
2749
+ code += ` index0: ${indexVar},${this.nl()}`;
2750
+ if (parentLoopVar) {
2751
+ code += ` parentloop: ${parentLoopVar},${this.nl()}`;
2752
+ code += ` parent: ${parentLoopVar}${this.nl()}`;
2753
+ } else {
2754
+ code += ` parentloop: null,${this.nl()}`;
2755
+ code += ` parent: null${this.nl()}`;
2756
+ }
2757
+ code += ` };${this.nl()}`;
2758
+ code += ` const forloop = ${loopVar};${this.nl()}`;
2759
+ code += ` const loop = ${loopVar};${this.nl()}`;
2760
+ this.loopStack.push(loopVar);
2761
+ const bodyCode = this.compileNodes(node.body);
2762
+ code += bodyCode.replace(new RegExp(`__ctx\\.${itemVar}`, "g"), itemVar);
2763
+ this.loopStack.pop();
2764
+ code += ` }${this.nl()}`;
2765
+ if (node.else_.length > 0) {
2766
+ code += ` }${this.nl()}`;
2767
+ }
2768
+ return code;
2769
+ }
2770
+ compileSet(node) {
2771
+ const value = this.compileExpr(node.value);
2772
+ return ` const ${node.target} = ${value};${this.nl()}`;
2773
+ }
2774
+ compileWith(node) {
2775
+ let code = ` {${this.nl()}`;
2776
+ for (const { target, value } of node.assignments) {
2777
+ const valueExpr = this.compileExpr(value);
2778
+ code += ` const ${target} = ${valueExpr};${this.nl()}`;
2779
+ }
2780
+ code += this.compileNodes(node.body);
2781
+ code += ` }${this.nl()}`;
2782
+ return code;
2783
+ }
2784
+ compileExpr(node) {
2785
+ switch (node.type) {
2786
+ case "Name":
2787
+ return this.compileName(node);
2788
+ case "Literal":
2789
+ return this.compileLiteral(node);
2790
+ case "Array":
2791
+ return this.compileArray(node);
2792
+ case "Object":
2793
+ return this.compileObject(node);
2794
+ case "BinaryOp":
2795
+ return this.compileBinaryOp(node);
2796
+ case "UnaryOp":
2797
+ return this.compileUnaryOp(node);
2798
+ case "Compare":
2799
+ return this.compileCompare(node);
2800
+ case "GetAttr":
2801
+ return this.compileGetAttr(node);
2802
+ case "GetItem":
2803
+ return this.compileGetItem(node);
2804
+ case "FilterExpr":
2805
+ return this.compileFilter(node);
2806
+ case "TestExpr":
2807
+ return this.compileTest(node);
2808
+ case "Conditional":
2809
+ return this.compileConditional(node);
2810
+ default:
2811
+ return "undefined";
2812
+ }
2813
+ }
2814
+ compileName(node) {
2815
+ if (node.name === "true" || node.name === "True")
2816
+ return "true";
2817
+ if (node.name === "false" || node.name === "False")
2818
+ return "false";
2819
+ if (node.name === "none" || node.name === "None" || node.name === "null")
2820
+ return "null";
2821
+ if (node.name === "forloop" || node.name === "loop")
2822
+ return node.name;
2823
+ return `__ctx.${node.name}`;
2824
+ }
2825
+ compileLiteral(node) {
2826
+ if (typeof node.value === "string") {
2827
+ return JSON.stringify(node.value);
2828
+ }
2829
+ return String(node.value);
2830
+ }
2831
+ compileArray(node) {
2832
+ const elements = node.elements.map((el) => this.compileExpr(el)).join(", ");
2833
+ return `[${elements}]`;
2834
+ }
2835
+ compileObject(node) {
2836
+ const pairs = node.pairs.map(({ key, value }) => {
2837
+ const k = this.compileExpr(key);
2838
+ const v = this.compileExpr(value);
2839
+ return `[${k}]: ${v}`;
2840
+ }).join(", ");
2841
+ return `{${pairs}}`;
2842
+ }
2843
+ compileBinaryOp(node) {
2844
+ const left = this.compileExpr(node.left);
2845
+ const right = this.compileExpr(node.right);
2846
+ switch (node.operator) {
2847
+ case "and":
2848
+ return `(${left} && ${right})`;
2849
+ case "or":
2850
+ return `(${left} || ${right})`;
2851
+ case "~":
2852
+ return `(String(${left}) + String(${right}))`;
2853
+ case "in":
2854
+ return `(Array.isArray(${right}) ? ${right}.includes(${left}) : String(${right}).includes(String(${left})))`;
2855
+ case "not in":
2856
+ return `!(Array.isArray(${right}) ? ${right}.includes(${left}) : String(${right}).includes(String(${left})))`;
2857
+ default:
2858
+ return `(${left} ${node.operator} ${right})`;
2859
+ }
2860
+ }
2861
+ compileUnaryOp(node) {
2862
+ const operand = this.compileExpr(node.operand);
2863
+ switch (node.operator) {
2864
+ case "not":
2865
+ return `!isTruthy(${operand})`;
2866
+ case "-":
2867
+ return `-(${operand})`;
2868
+ case "+":
2869
+ return `+(${operand})`;
2870
+ default:
2871
+ return operand;
2872
+ }
2873
+ }
2874
+ compileCompare(node) {
2875
+ let result = this.compileExpr(node.left);
2876
+ for (const { operator, right } of node.ops) {
2877
+ const rightExpr = this.compileExpr(right);
2878
+ switch (operator) {
2879
+ case "==":
2880
+ case "===":
2881
+ result = `(${result} === ${rightExpr})`;
2882
+ break;
2883
+ case "!=":
2884
+ case "!==":
2885
+ result = `(${result} !== ${rightExpr})`;
2886
+ break;
2887
+ default:
2888
+ result = `(${result} ${operator} ${rightExpr})`;
2889
+ }
2890
+ }
2891
+ return result;
2892
+ }
2893
+ compileGetAttr(node) {
2894
+ const obj = this.compileExpr(node.object);
2895
+ return `${obj}?.${node.attribute}`;
2896
+ }
2897
+ compileGetItem(node) {
2898
+ const obj = this.compileExpr(node.object);
2899
+ const index = this.compileExpr(node.index);
2900
+ return `${obj}?.[${index}]`;
2901
+ }
2902
+ compileFilter(node) {
2903
+ const value = this.compileExpr(node.node);
2904
+ const args = node.args.map((arg) => this.compileExpr(arg));
2905
+ switch (node.filter) {
2906
+ case "upper":
2907
+ return `String(${value}).toUpperCase()`;
2908
+ case "lower":
2909
+ return `String(${value}).toLowerCase()`;
2910
+ case "title":
2911
+ return `String(${value}).replace(/\\b\\w/g, c => c.toUpperCase())`;
2912
+ case "trim":
2913
+ return `String(${value}).trim()`;
2914
+ case "length":
2915
+ return `(${value}?.length ?? Object.keys(${value} ?? {}).length)`;
2916
+ case "first":
2917
+ return `(${value})?.[0]`;
2918
+ case "last":
2919
+ return `(${value})?.[(${value})?.length - 1]`;
2920
+ case "default":
2921
+ return `((${value}) ?? ${args[0] ?? '""'})`;
2922
+ case "safe":
2923
+ return `{ __safe: true, value: String(${value}) }`;
2924
+ case "escape":
2925
+ case "e":
2926
+ return `escape(${value})`;
2927
+ case "join":
2928
+ return `(${value} ?? []).join(${args[0] ?? '""'})`;
2929
+ case "abs":
2930
+ return `Math.abs(${value})`;
2931
+ case "round":
2932
+ return args.length ? `Number(${value}).toFixed(${args[0]})` : `Math.round(${value})`;
2933
+ case "int":
2934
+ return `parseInt(${value}, 10)`;
2935
+ case "float":
2936
+ return `parseFloat(${value})`;
2937
+ case "floatformat":
2938
+ return `Number(${value}).toFixed(${args[0] ?? 1})`;
2939
+ case "filesizeformat":
2940
+ return `applyFilter('filesizeformat', ${value})`;
2941
+ default:
2942
+ const argsStr = args.length ? ", " + args.join(", ") : "";
2943
+ return `applyFilter('${node.filter}', ${value}${argsStr})`;
2944
+ }
2945
+ }
2946
+ compileTest(node) {
2947
+ const value = this.compileExpr(node.node);
2948
+ const args = node.args.map((arg) => this.compileExpr(arg));
2949
+ const negation = node.negated ? "!" : "";
2950
+ switch (node.test) {
2951
+ case "defined":
2952
+ return `${negation}(${value} !== undefined)`;
2953
+ case "undefined":
2954
+ return `${negation}(${value} === undefined)`;
2955
+ case "none":
2956
+ return `${negation}(${value} === null)`;
2957
+ case "even":
2958
+ return `${negation}(${value} % 2 === 0)`;
2959
+ case "odd":
2960
+ return `${negation}(${value} % 2 !== 0)`;
2961
+ case "divisibleby":
2962
+ return `${negation}(${value} % ${args[0]} === 0)`;
2963
+ case "empty":
2964
+ return `${negation}((${value} == null) || (${value}.length === 0) || (Object.keys(${value}).length === 0))`;
2965
+ case "iterable":
2966
+ return `${negation}(Array.isArray(${value}) || typeof ${value} === 'string')`;
2967
+ case "number":
2968
+ return `${negation}(typeof ${value} === 'number' && !isNaN(${value}))`;
2969
+ case "string":
2970
+ return `${negation}(typeof ${value} === 'string')`;
2971
+ default:
2972
+ const argsStr = args.length ? ", " + args.join(", ") : "";
2973
+ return `${negation}applyTest('${node.test}', ${value}${argsStr})`;
2974
+ }
2975
+ }
2976
+ compileConditional(node) {
2977
+ const test = this.compileExpr(node.test);
2978
+ const trueExpr = this.compileExpr(node.trueExpr);
2979
+ const falseExpr = this.compileExpr(node.falseExpr);
2980
+ return `(isTruthy(${test}) ? ${trueExpr} : ${falseExpr})`;
2981
+ }
2982
+ isMarkedSafe(node) {
2983
+ if (node.type === "FilterExpr") {
2984
+ const filter = node;
2985
+ return filter.filter === "safe";
2986
+ }
2987
+ return false;
2988
+ }
2989
+ genVar(prefix) {
2990
+ return `__${prefix}${this.varCounter++}`;
2991
+ }
2992
+ nl() {
2993
+ return this.options.minify ? "" : `
2994
+ `;
2995
+ }
2996
+ }
2997
+
2357
2998
  // src/index.ts
2358
- import * as fs from "fs";
2359
2999
  import * as path from "path";
3000
+ var URL_PARAM_REGEX = /<[^>]+>|:[a-zA-Z_]+|\(\?P<[^>]+>\[[^\]]+\]\)/g;
2360
3001
 
2361
3002
  class Environment {
2362
3003
  options;
@@ -2405,7 +3046,7 @@ class Environment {
2405
3046
  if (!templatePath) {
2406
3047
  throw new Error(`Template not found: ${templateName}`);
2407
3048
  }
2408
- const source = await fs.promises.readFile(templatePath, "utf-8");
3049
+ const source = await Bun.file(templatePath).text();
2409
3050
  const ast = this.compile(source);
2410
3051
  if (this.options.cache) {
2411
3052
  this.templateCache.set(templateName, ast);
@@ -2433,10 +3074,9 @@ class Environment {
2433
3074
  const basePath = path.resolve(this.options.templates, templateName);
2434
3075
  for (const ext of this.options.extensions) {
2435
3076
  const fullPath = basePath + ext;
2436
- try {
2437
- await fs.promises.access(fullPath, fs.constants.R_OK);
3077
+ if (await Bun.file(fullPath).exists()) {
2438
3078
  return fullPath;
2439
- } catch {}
3079
+ }
2440
3080
  }
2441
3081
  return null;
2442
3082
  }
@@ -2447,13 +3087,15 @@ class Environment {
2447
3087
  return `#${name}`;
2448
3088
  }
2449
3089
  let url = pattern;
2450
- for (const [key, value] of Object.entries(kwargs)) {
2451
- url = url.replace(`:${key}`, encodeURIComponent(String(value)));
2452
- url = url.replace(`<${key}>`, encodeURIComponent(String(value)));
2453
- url = url.replace(`(?P<${key}>[^/]+)`, encodeURIComponent(String(value)));
3090
+ for (const key in kwargs) {
3091
+ const encoded = encodeURIComponent(String(kwargs[key]));
3092
+ url = url.replaceAll(`:${key}`, encoded);
3093
+ url = url.replaceAll(`<${key}>`, encoded);
3094
+ url = url.replaceAll(`(?P<${key}>[^/]+)`, encoded);
2454
3095
  }
2455
3096
  let argIndex = 0;
2456
- url = url.replace(/<[^>]+>|:[a-zA-Z_]+|\(\?P<[^>]+>\[[^\]]+\]\)/g, () => {
3097
+ URL_PARAM_REGEX.lastIndex = 0;
3098
+ url = url.replace(URL_PARAM_REGEX, () => {
2457
3099
  if (argIndex < args.length) {
2458
3100
  return encodeURIComponent(String(args[argIndex++]));
2459
3101
  }
@@ -2472,22 +3114,39 @@ async function render(source, context = {}, options = {}) {
2472
3114
  function Template(source, options = {}) {
2473
3115
  const env = new Environment(options);
2474
3116
  const ast = env.compile(source);
3117
+ const runtime = new Runtime({
3118
+ autoescape: options.autoescape ?? true,
3119
+ filters: options.filters ?? {},
3120
+ globals: options.globals ?? {},
3121
+ urlResolver: options.urlResolver,
3122
+ staticResolver: options.staticResolver,
3123
+ templateLoader: async () => ast
3124
+ });
2475
3125
  return {
2476
3126
  async render(context = {}) {
2477
- const runtime = new Runtime({
2478
- autoescape: options.autoescape ?? true,
2479
- filters: options.filters ?? {},
2480
- globals: options.globals ?? {},
2481
- urlResolver: options.urlResolver,
2482
- staticResolver: options.staticResolver,
2483
- templateLoader: async () => ast
2484
- });
2485
3127
  return runtime.render(ast, context);
2486
3128
  }
2487
3129
  };
2488
3130
  }
3131
+ function compile(source, options = {}) {
3132
+ const lexer = new Lexer(source);
3133
+ const tokens = lexer.tokenize();
3134
+ const parser = new Parser(tokens);
3135
+ const ast = parser.parse();
3136
+ return compileToFunction(ast, options);
3137
+ }
3138
+ function compileToCode(source, options = {}) {
3139
+ const lexer = new Lexer(source);
3140
+ const tokens = lexer.tokenize();
3141
+ const parser = new Parser(tokens);
3142
+ const ast = parser.parse();
3143
+ return compileToString(ast, options);
3144
+ }
2489
3145
  export {
2490
3146
  render,
3147
+ compileToCode,
3148
+ compile,
3149
+ builtinTests,
2491
3150
  builtinFilters,
2492
3151
  TokenType,
2493
3152
  Template,