binja 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/lexer/tokens.ts
3
5
  var TokenType;
4
6
  ((TokenType2) => {
@@ -95,6 +97,14 @@ class Lexer {
95
97
  const wsControl = this.peek() === "-";
96
98
  if (wsControl)
97
99
  this.advance();
100
+ const savedPos = this.state.pos;
101
+ this.skipWhitespace();
102
+ if (this.checkWord("raw") || this.checkWord("verbatim")) {
103
+ const tagName = this.checkWord("raw") ? "raw" : "verbatim";
104
+ this.scanRawBlock(tagName, wsControl);
105
+ return;
106
+ }
107
+ this.state.pos = savedPos;
98
108
  this.addToken("BLOCK_START" /* BLOCK_START */, this.blockStart + (wsControl ? "-" : ""));
99
109
  this.scanExpression(this.blockEnd, "BLOCK_END" /* BLOCK_END */);
100
110
  return;
@@ -105,6 +115,73 @@ class Lexer {
105
115
  }
106
116
  this.scanText();
107
117
  }
118
+ checkWord(word) {
119
+ const start = this.state.pos;
120
+ for (let i = 0;i < word.length; i++) {
121
+ if (this.state.source[start + i]?.toLowerCase() !== word[i]) {
122
+ return false;
123
+ }
124
+ }
125
+ const nextChar = this.state.source[start + word.length];
126
+ return !nextChar || !this.isAlphaNumeric(nextChar);
127
+ }
128
+ scanRawBlock(tagName, wsControl) {
129
+ const startLine = this.state.line;
130
+ const startColumn = this.state.column;
131
+ for (let i = 0;i < tagName.length; i++) {
132
+ this.advance();
133
+ }
134
+ this.skipWhitespace();
135
+ if (this.peek() === "-")
136
+ this.advance();
137
+ if (!this.match(this.blockEnd)) {
138
+ throw new Error(`Expected ${this.blockEnd} after ${tagName} at line ${this.state.line}`);
139
+ }
140
+ const endTag = `end${tagName}`;
141
+ const contentStart = this.state.pos;
142
+ while (!this.isAtEnd()) {
143
+ if (this.check(this.blockStart)) {
144
+ const savedPos = this.state.pos;
145
+ const savedLine = this.state.line;
146
+ const savedColumn = this.state.column;
147
+ this.match(this.blockStart);
148
+ if (this.peek() === "-")
149
+ this.advance();
150
+ this.skipWhitespace();
151
+ if (this.checkWord(endTag)) {
152
+ const content = this.state.source.slice(contentStart, savedPos);
153
+ if (content.length > 0) {
154
+ this.state.tokens.push({
155
+ type: "TEXT" /* TEXT */,
156
+ value: content,
157
+ line: startLine,
158
+ column: startColumn
159
+ });
160
+ }
161
+ for (let i = 0;i < endTag.length; i++) {
162
+ this.advance();
163
+ }
164
+ this.skipWhitespace();
165
+ if (this.peek() === "-")
166
+ this.advance();
167
+ if (!this.match(this.blockEnd)) {
168
+ throw new Error(`Expected ${this.blockEnd} after ${endTag} at line ${this.state.line}`);
169
+ }
170
+ return;
171
+ }
172
+ this.state.pos = savedPos;
173
+ this.state.line = savedLine;
174
+ this.state.column = savedColumn;
175
+ }
176
+ if (this.peek() === `
177
+ `) {
178
+ this.state.line++;
179
+ this.state.column = 0;
180
+ }
181
+ this.advance();
182
+ }
183
+ throw new Error(`Unclosed ${tagName} block starting at line ${startLine}`);
184
+ }
108
185
  scanText() {
109
186
  const start = this.state.pos;
110
187
  const startLine = this.state.line;
@@ -1209,102 +1286,120 @@ class Parser {
1209
1286
  }
1210
1287
 
1211
1288
  // src/runtime/context.ts
1289
+ function createForLoop(items, index, depth, lastCycleValue, parentloop) {
1290
+ const length = items.length;
1291
+ const forloop = {
1292
+ _items: items,
1293
+ _idx: index,
1294
+ counter: index + 1,
1295
+ counter0: index,
1296
+ first: index === 0,
1297
+ last: index === length - 1,
1298
+ length,
1299
+ index: index + 1,
1300
+ index0: index,
1301
+ depth,
1302
+ depth0: depth - 1,
1303
+ revcounter: length - index,
1304
+ revcounter0: length - index - 1,
1305
+ revindex: length - index,
1306
+ revindex0: length - index - 1,
1307
+ get previtem() {
1308
+ return this._idx > 0 ? this._items[this._idx - 1] : undefined;
1309
+ },
1310
+ get nextitem() {
1311
+ return this._idx < this._items.length - 1 ? this._items[this._idx + 1] : undefined;
1312
+ },
1313
+ cycle: (...args) => args[index % args.length],
1314
+ changed: (value) => {
1315
+ const changed = value !== lastCycleValue.value;
1316
+ lastCycleValue.value = value;
1317
+ return changed;
1318
+ }
1319
+ };
1320
+ if (parentloop) {
1321
+ forloop.parentloop = parentloop;
1322
+ }
1323
+ return forloop;
1324
+ }
1325
+
1212
1326
  class Context {
1213
1327
  scopes = [];
1214
1328
  parent = null;
1215
1329
  _forloopStack = [];
1216
- _lastCycleValue = null;
1330
+ _lastCycleValue = { value: null };
1331
+ _currentForloop = null;
1332
+ _currentScope;
1217
1333
  constructor(data = {}, parent = null) {
1218
1334
  this.parent = parent;
1219
- this.scopes.push(new Map(Object.entries(data)));
1335
+ this._currentScope = Object.assign(Object.create(null), data);
1336
+ this.scopes.push(this._currentScope);
1220
1337
  }
1221
1338
  get(name) {
1222
1339
  if (name === "forloop" || name === "loop") {
1223
- return this._forloopStack[this._forloopStack.length - 1] || null;
1340
+ return this._currentForloop;
1224
1341
  }
1225
1342
  for (let i = this.scopes.length - 1;i >= 0; i--) {
1226
- if (this.scopes[i].has(name)) {
1227
- return this.scopes[i].get(name);
1343
+ const scope = this.scopes[i];
1344
+ if (name in scope) {
1345
+ return scope[name];
1228
1346
  }
1229
1347
  }
1230
- if (this.parent) {
1231
- return this.parent.get(name);
1232
- }
1233
- return;
1348
+ return this.parent ? this.parent.get(name) : undefined;
1234
1349
  }
1235
1350
  set(name, value) {
1236
- this.scopes[this.scopes.length - 1].set(name, value);
1351
+ this._currentScope[name] = value;
1237
1352
  }
1238
1353
  has(name) {
1239
1354
  for (let i = this.scopes.length - 1;i >= 0; i--) {
1240
- if (this.scopes[i].has(name))
1355
+ if (name in this.scopes[i])
1241
1356
  return true;
1242
1357
  }
1243
1358
  return this.parent ? this.parent.has(name) : false;
1244
1359
  }
1245
1360
  push(data = {}) {
1246
- this.scopes.push(new Map(Object.entries(data)));
1361
+ this._currentScope = Object.assign(Object.create(null), data);
1362
+ this.scopes.push(this._currentScope);
1247
1363
  }
1248
1364
  pop() {
1249
1365
  if (this.scopes.length > 1) {
1250
1366
  this.scopes.pop();
1367
+ this._currentScope = this.scopes[this.scopes.length - 1];
1251
1368
  }
1252
1369
  }
1253
1370
  derived(data = {}) {
1254
1371
  return new Context(data, this);
1255
1372
  }
1256
1373
  pushForLoop(items, index) {
1257
- const length = items.length;
1258
1374
  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
- }
1375
+ const parentloop = this._forloopStack.length > 0 ? this._forloopStack[this._forloopStack.length - 1] : undefined;
1376
+ const forloop = createForLoop(items, index, depth, this._lastCycleValue, parentloop);
1285
1377
  this._forloopStack.push(forloop);
1378
+ this._currentForloop = forloop;
1286
1379
  return forloop;
1287
1380
  }
1288
1381
  popForLoop() {
1289
1382
  this._forloopStack.pop();
1383
+ this._currentForloop = this._forloopStack.length > 0 ? this._forloopStack[this._forloopStack.length - 1] : null;
1290
1384
  }
1291
1385
  updateForLoop(index, items) {
1292
- const forloop = this._forloopStack[this._forloopStack.length - 1];
1386
+ const forloop = this._currentForloop;
1293
1387
  if (!forloop)
1294
1388
  return;
1295
1389
  const length = items.length;
1390
+ forloop._idx = index;
1391
+ forloop._items = items;
1296
1392
  forloop.counter = index + 1;
1297
1393
  forloop.counter0 = index;
1298
- forloop.revcounter = length - index;
1299
- forloop.revcounter0 = length - index - 1;
1300
1394
  forloop.first = index === 0;
1301
1395
  forloop.last = index === length - 1;
1302
1396
  forloop.index = index + 1;
1303
1397
  forloop.index0 = index;
1398
+ forloop.revcounter = length - index;
1399
+ forloop.revcounter0 = length - index - 1;
1304
1400
  forloop.revindex = length - index;
1305
1401
  forloop.revindex0 = length - index - 1;
1306
- forloop.previtem = index > 0 ? items[index - 1] : undefined;
1307
- forloop.nextitem = index < length - 1 ? items[index + 1] : undefined;
1402
+ forloop.cycle = (...args) => args[index % args.length];
1308
1403
  }
1309
1404
  toObject() {
1310
1405
  const result = {};
@@ -1312,15 +1407,28 @@ class Context {
1312
1407
  Object.assign(result, this.parent.toObject());
1313
1408
  }
1314
1409
  for (const scope of this.scopes) {
1315
- for (const [key, value] of scope) {
1316
- result[key] = value;
1317
- }
1410
+ Object.assign(result, scope);
1318
1411
  }
1319
1412
  return result;
1320
1413
  }
1321
1414
  }
1322
1415
 
1323
1416
  // src/filters/index.ts
1417
+ var TITLE_REGEX = /\b\w/g;
1418
+ var STRIPTAGS_REGEX = /<[^>]*>/g;
1419
+ var SLUGIFY_NON_WORD_REGEX = /[^\w\s-]/g;
1420
+ var SLUGIFY_SPACES_REGEX = /[\s_-]+/g;
1421
+ var SLUGIFY_TRIM_REGEX = /^-+|-+$/g;
1422
+ var URLIZE_REGEX = /(https?:\/\/[^\s]+)/g;
1423
+ var DATE_CHAR_REGEX = /[a-zA-Z]/g;
1424
+ var DOUBLE_NEWLINE_REGEX = /\n\n+/;
1425
+ var NEWLINE_REGEX = /\n/g;
1426
+ var WHITESPACE_REGEX = /\s+/;
1427
+ var DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1428
+ var DAY_NAMES_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1429
+ var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1430
+ var MONTH_NAMES_LONG = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
1431
+ var MONTH_NAMES_AP = ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."];
1324
1432
  var upper = (value) => String(value).toUpperCase();
1325
1433
  var lower = (value) => String(value).toLowerCase();
1326
1434
  var capitalize = (value) => {
@@ -1331,13 +1439,13 @@ var capfirst = (value) => {
1331
1439
  const str = String(value);
1332
1440
  return str.charAt(0).toUpperCase() + str.slice(1);
1333
1441
  };
1334
- var title = (value) => String(value).replace(/\b\w/g, (c) => c.toUpperCase());
1442
+ var title = (value) => String(value).replace(TITLE_REGEX, (c) => c.toUpperCase());
1335
1443
  var trim = (value) => String(value).trim();
1336
- var striptags = (value) => String(value).replace(/<[^>]*>/g, "");
1444
+ var striptags = (value) => String(value).replace(STRIPTAGS_REGEX, "");
1337
1445
  var escape = (value) => {
1338
1446
  if (value?.__safe__)
1339
1447
  return value;
1340
- const escaped = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
1448
+ const escaped = Bun.escapeHTML(String(value));
1341
1449
  const safeString = new String(escaped);
1342
1450
  safeString.__safe__ = true;
1343
1451
  return safeString;
@@ -1350,15 +1458,15 @@ var safe = (value) => {
1350
1458
  var escapejs = (value) => JSON.stringify(String(value)).slice(1, -1);
1351
1459
  var linebreaks = (value) => {
1352
1460
  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(`
1461
+ const paragraphs = str.split(DOUBLE_NEWLINE_REGEX);
1462
+ const html = paragraphs.map((p) => `<p>${p.replace(NEWLINE_REGEX, "<br>")}</p>`).join(`
1355
1463
  `);
1356
1464
  const safeString = new String(html);
1357
1465
  safeString.__safe__ = true;
1358
1466
  return safeString;
1359
1467
  };
1360
1468
  var linebreaksbr = (value) => {
1361
- const html = String(value).replace(/\n/g, "<br>");
1469
+ const html = String(value).replace(NEWLINE_REGEX, "<br>");
1362
1470
  const safeString = new String(html);
1363
1471
  safeString.__safe__ = true;
1364
1472
  return safeString;
@@ -1370,12 +1478,12 @@ var truncatechars = (value, length = 30) => {
1370
1478
  return str.slice(0, length - 3) + "...";
1371
1479
  };
1372
1480
  var truncatewords = (value, count = 15) => {
1373
- const words = String(value).split(/\s+/);
1481
+ const words = String(value).split(WHITESPACE_REGEX);
1374
1482
  if (words.length <= count)
1375
1483
  return value;
1376
1484
  return words.slice(0, count).join(" ") + "...";
1377
1485
  };
1378
- var wordcount = (value) => String(value).split(/\s+/).filter(Boolean).length;
1486
+ var wordcount = (value) => String(value).split(WHITESPACE_REGEX).filter(Boolean).length;
1379
1487
  var center = (value, width = 80) => {
1380
1488
  const str = String(value);
1381
1489
  const padding = Math.max(0, width - str.length);
@@ -1386,7 +1494,7 @@ var center = (value, width = 80) => {
1386
1494
  var ljust = (value, width = 80) => String(value).padEnd(width);
1387
1495
  var rjust = (value, width = 80) => String(value).padStart(width);
1388
1496
  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, "");
1497
+ var slugify = (value) => String(value).toLowerCase().replace(SLUGIFY_NON_WORD_REGEX, "").replace(SLUGIFY_SPACES_REGEX, "-").replace(SLUGIFY_TRIM_REGEX, "");
1390
1498
  var abs = (value) => Math.abs(Number(value));
1391
1499
  var round = (value, precision = 0) => Number(Number(value).toFixed(precision));
1392
1500
  var int = (value) => parseInt(String(value), 10) || 0;
@@ -1426,8 +1534,12 @@ var length = (value) => {
1426
1534
  return 0;
1427
1535
  if (typeof value === "string" || Array.isArray(value))
1428
1536
  return value.length;
1429
- if (typeof value === "object")
1430
- return Object.keys(value).length;
1537
+ if (typeof value === "object") {
1538
+ let count = 0;
1539
+ for (const _ in value)
1540
+ count++;
1541
+ return count;
1542
+ }
1431
1543
  return 0;
1432
1544
  };
1433
1545
  var length_is = (value, len) => length(value) === Number(len);
@@ -1507,58 +1619,55 @@ var columns = (value, cols) => {
1507
1619
  }
1508
1620
  return result;
1509
1621
  };
1622
+ var formatDateChar = (d, char) => {
1623
+ switch (char) {
1624
+ case "d":
1625
+ return String(d.getDate()).padStart(2, "0");
1626
+ case "j":
1627
+ return String(d.getDate());
1628
+ case "D":
1629
+ return DAY_NAMES_SHORT[d.getDay()];
1630
+ case "l":
1631
+ return DAY_NAMES_LONG[d.getDay()];
1632
+ case "m":
1633
+ return String(d.getMonth() + 1).padStart(2, "0");
1634
+ case "n":
1635
+ return String(d.getMonth() + 1);
1636
+ case "M":
1637
+ return MONTH_NAMES_SHORT[d.getMonth()];
1638
+ case "F":
1639
+ return MONTH_NAMES_LONG[d.getMonth()];
1640
+ case "N":
1641
+ return MONTH_NAMES_AP[d.getMonth()];
1642
+ case "y":
1643
+ return String(d.getFullYear()).slice(-2);
1644
+ case "Y":
1645
+ return String(d.getFullYear());
1646
+ case "H":
1647
+ return String(d.getHours()).padStart(2, "0");
1648
+ case "G":
1649
+ return String(d.getHours());
1650
+ case "i":
1651
+ return String(d.getMinutes()).padStart(2, "0");
1652
+ case "s":
1653
+ return String(d.getSeconds()).padStart(2, "0");
1654
+ case "a":
1655
+ return d.getHours() < 12 ? "a.m." : "p.m.";
1656
+ case "A":
1657
+ return d.getHours() < 12 ? "AM" : "PM";
1658
+ case "g":
1659
+ return String(d.getHours() % 12 || 12);
1660
+ case "h":
1661
+ return String(d.getHours() % 12 || 12).padStart(2, "0");
1662
+ default:
1663
+ return char;
1664
+ }
1665
+ };
1510
1666
  var date = (value, format = "N j, Y") => {
1511
1667
  const d = value instanceof Date ? value : new Date(value);
1512
1668
  if (isNaN(d.getTime()))
1513
1669
  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);
1670
+ return format.replace(DATE_CHAR_REGEX, (char) => formatDateChar(d, char));
1562
1671
  };
1563
1672
  var time = (value, format = "H:i") => date(value, format);
1564
1673
  var timesince = (value, now = new Date) => {
@@ -1621,8 +1730,7 @@ var pluralize = (value, arg = "s") => {
1621
1730
  };
1622
1731
  var urlencode = (value) => encodeURIComponent(String(value));
1623
1732
  var urlize = (value) => {
1624
- const urlRegex = /(https?:\/\/[^\s]+)/g;
1625
- const html = String(value).replace(urlRegex, '<a href="$1">$1</a>');
1733
+ const html = String(value).replace(URLIZE_REGEX, '<a href="$1">$1</a>');
1626
1734
  const safeString = new String(html);
1627
1735
  safeString.__safe__ = true;
1628
1736
  return safeString;
@@ -1673,6 +1781,263 @@ var groupby = (value, attribute) => {
1673
1781
  list
1674
1782
  }));
1675
1783
  };
1784
+ var wordwrap = (value, width = 79, breakLongWords = true, wrapString = `
1785
+ `) => {
1786
+ const str = String(value);
1787
+ if (str.length <= width)
1788
+ return str;
1789
+ const words = str.split(" ");
1790
+ const lines = [];
1791
+ let currentLine = "";
1792
+ for (const word of words) {
1793
+ if (currentLine.length + word.length + 1 <= width) {
1794
+ currentLine += (currentLine ? " " : "") + word;
1795
+ } else {
1796
+ if (currentLine)
1797
+ lines.push(currentLine);
1798
+ if (breakLongWords && word.length > width) {
1799
+ let remaining = word;
1800
+ while (remaining.length > width) {
1801
+ lines.push(remaining.slice(0, width));
1802
+ remaining = remaining.slice(width);
1803
+ }
1804
+ currentLine = remaining;
1805
+ } else {
1806
+ currentLine = word;
1807
+ }
1808
+ }
1809
+ }
1810
+ if (currentLine)
1811
+ lines.push(currentLine);
1812
+ return lines.join(wrapString);
1813
+ };
1814
+ var indent = (value, width = 4, first2 = false, blank = false) => {
1815
+ const str = String(value);
1816
+ const indentStr = typeof width === "string" ? width : " ".repeat(Number(width));
1817
+ const lines = str.split(`
1818
+ `);
1819
+ return lines.map((line, i) => {
1820
+ if (i === 0 && !first2)
1821
+ return line;
1822
+ if (!blank && line.trim() === "")
1823
+ return line;
1824
+ return indentStr + line;
1825
+ }).join(`
1826
+ `);
1827
+ };
1828
+ var replace = (value, old, newStr, count) => {
1829
+ const str = String(value);
1830
+ if (count === undefined) {
1831
+ return str.replaceAll(String(old), String(newStr));
1832
+ }
1833
+ let result = str;
1834
+ let remaining = Number(count);
1835
+ while (remaining > 0 && result.includes(String(old))) {
1836
+ result = result.replace(String(old), String(newStr));
1837
+ remaining--;
1838
+ }
1839
+ return result;
1840
+ };
1841
+ var format = (value, ...args) => {
1842
+ let str = String(value);
1843
+ args.forEach((arg, i) => {
1844
+ str = str.replace(/%s/, String(arg));
1845
+ str = str.replace(new RegExp(`%${i + 1}`, "g"), String(arg));
1846
+ });
1847
+ return str;
1848
+ };
1849
+ var string = (value) => String(value);
1850
+ var list = (value) => {
1851
+ if (Array.isArray(value))
1852
+ return value;
1853
+ if (typeof value === "string")
1854
+ return value.split("");
1855
+ if (value && typeof value[Symbol.iterator] === "function")
1856
+ return [...value];
1857
+ if (typeof value === "object" && value !== null)
1858
+ return Object.values(value);
1859
+ return [value];
1860
+ };
1861
+ var map = (value, attribute) => {
1862
+ if (!Array.isArray(value))
1863
+ return [];
1864
+ if (typeof attribute === "string") {
1865
+ return value.map((item) => item?.[attribute]);
1866
+ }
1867
+ return value;
1868
+ };
1869
+ var select = (value, attribute) => {
1870
+ if (!Array.isArray(value))
1871
+ return [];
1872
+ if (attribute === undefined) {
1873
+ return value.filter((item) => !!item);
1874
+ }
1875
+ return value.filter((item) => !!item?.[attribute]);
1876
+ };
1877
+ var reject = (value, attribute) => {
1878
+ if (!Array.isArray(value))
1879
+ return [];
1880
+ if (attribute === undefined) {
1881
+ return value.filter((item) => !item);
1882
+ }
1883
+ return value.filter((item) => !item?.[attribute]);
1884
+ };
1885
+ var selectattr = (value, attribute, test, testValue) => {
1886
+ if (!Array.isArray(value))
1887
+ return [];
1888
+ return value.filter((item) => {
1889
+ const attrValue = item?.[attribute];
1890
+ if (test === undefined)
1891
+ return !!attrValue;
1892
+ if (test === "eq" || test === "equalto")
1893
+ return attrValue === testValue;
1894
+ if (test === "ne")
1895
+ return attrValue !== testValue;
1896
+ if (test === "gt")
1897
+ return attrValue > testValue;
1898
+ if (test === "lt")
1899
+ return attrValue < testValue;
1900
+ if (test === "ge" || test === "gte")
1901
+ return attrValue >= testValue;
1902
+ if (test === "le" || test === "lte")
1903
+ return attrValue <= testValue;
1904
+ if (test === "in")
1905
+ return testValue?.includes?.(attrValue);
1906
+ if (test === "defined")
1907
+ return attrValue !== undefined;
1908
+ if (test === "undefined")
1909
+ return attrValue === undefined;
1910
+ if (test === "none")
1911
+ return attrValue === null;
1912
+ if (test === "true")
1913
+ return attrValue === true;
1914
+ if (test === "false")
1915
+ return attrValue === false;
1916
+ return !!attrValue;
1917
+ });
1918
+ };
1919
+ var rejectattr = (value, attribute, test, testValue) => {
1920
+ if (!Array.isArray(value))
1921
+ return [];
1922
+ const selected = selectattr(value, attribute, test, testValue);
1923
+ return value.filter((item) => !selected.includes(item));
1924
+ };
1925
+ var attr = (value, name) => {
1926
+ if (value == null)
1927
+ return;
1928
+ return value[name];
1929
+ };
1930
+ var max = (value, attribute, defaultValue) => {
1931
+ if (!Array.isArray(value) || value.length === 0)
1932
+ return defaultValue;
1933
+ if (attribute) {
1934
+ return value.reduce((max2, item) => item[attribute] > max2[attribute] ? item : max2);
1935
+ }
1936
+ return Math.max(...value);
1937
+ };
1938
+ var min = (value, attribute, defaultValue) => {
1939
+ if (!Array.isArray(value) || value.length === 0)
1940
+ return defaultValue;
1941
+ if (attribute) {
1942
+ return value.reduce((min2, item) => item[attribute] < min2[attribute] ? item : min2);
1943
+ }
1944
+ return Math.min(...value);
1945
+ };
1946
+ var sum = (value, attribute, start = 0) => {
1947
+ if (!Array.isArray(value))
1948
+ return start;
1949
+ return value.reduce((total, item) => {
1950
+ const val = attribute ? item[attribute] : item;
1951
+ return total + (Number(val) || 0);
1952
+ }, Number(start));
1953
+ };
1954
+ var pprint = (value) => {
1955
+ try {
1956
+ const result = JSON.stringify(value, null, 2);
1957
+ const safeString = new String(result);
1958
+ safeString.__safe__ = true;
1959
+ return safeString;
1960
+ } catch {
1961
+ return String(value);
1962
+ }
1963
+ };
1964
+ var forceescape = (value) => {
1965
+ const escaped = Bun.escapeHTML(String(value));
1966
+ const safeString = new String(escaped);
1967
+ safeString.__safe__ = true;
1968
+ return safeString;
1969
+ };
1970
+ var PHONE_MAP = {
1971
+ a: "2",
1972
+ b: "2",
1973
+ c: "2",
1974
+ d: "3",
1975
+ e: "3",
1976
+ f: "3",
1977
+ g: "4",
1978
+ h: "4",
1979
+ i: "4",
1980
+ j: "5",
1981
+ k: "5",
1982
+ l: "5",
1983
+ m: "6",
1984
+ n: "6",
1985
+ o: "6",
1986
+ p: "7",
1987
+ q: "7",
1988
+ r: "7",
1989
+ s: "7",
1990
+ t: "8",
1991
+ u: "8",
1992
+ v: "8",
1993
+ w: "9",
1994
+ x: "9",
1995
+ y: "9",
1996
+ z: "9"
1997
+ };
1998
+ var phone2numeric = (value) => {
1999
+ return String(value).toLowerCase().split("").map((char) => PHONE_MAP[char] ?? char).join("");
2000
+ };
2001
+ var linenumbers = (value) => {
2002
+ const lines = String(value).split(`
2003
+ `);
2004
+ const width = String(lines.length).length;
2005
+ return lines.map((line, i) => `${String(i + 1).padStart(width, " ")}. ${line}`).join(`
2006
+ `);
2007
+ };
2008
+ var unordered_list = (value) => {
2009
+ if (!Array.isArray(value))
2010
+ return String(value);
2011
+ const renderList = (items, depth = 0) => {
2012
+ const indent2 = " ".repeat(depth);
2013
+ let html2 = "";
2014
+ for (let i = 0;i < items.length; i++) {
2015
+ const item = items[i];
2016
+ if (Array.isArray(item)) {
2017
+ html2 += `
2018
+ ${indent2}<ul>
2019
+ ${renderList(item, depth + 1)}${indent2}</ul>
2020
+ `;
2021
+ } else {
2022
+ html2 += `${indent2}<li>${item}`;
2023
+ if (i + 1 < items.length && Array.isArray(items[i + 1])) {
2024
+ html2 += `
2025
+ ${indent2}<ul>
2026
+ ${renderList(items[i + 1], depth + 1)}${indent2}</ul>
2027
+ ${indent2}`;
2028
+ i++;
2029
+ }
2030
+ html2 += `</li>
2031
+ `;
2032
+ }
2033
+ }
2034
+ return html2;
2035
+ };
2036
+ const html = renderList(value);
2037
+ const safeString = new String(html);
2038
+ safeString.__safe__ = true;
2039
+ return safeString;
2040
+ };
1676
2041
  var builtinFilters = {
1677
2042
  upper,
1678
2043
  lower,
@@ -1731,7 +2096,27 @@ var builtinFilters = {
1731
2096
  tojson: json,
1732
2097
  random,
1733
2098
  batch,
1734
- groupby
2099
+ groupby,
2100
+ wordwrap,
2101
+ indent,
2102
+ replace,
2103
+ format,
2104
+ string,
2105
+ list,
2106
+ map,
2107
+ select,
2108
+ reject,
2109
+ selectattr,
2110
+ rejectattr,
2111
+ attr,
2112
+ max,
2113
+ min,
2114
+ sum,
2115
+ pprint,
2116
+ forceescape,
2117
+ phone2numeric,
2118
+ linenumbers,
2119
+ unordered_list
1735
2120
  };
1736
2121
 
1737
2122
  // src/tests/index.ts
@@ -1777,7 +2162,7 @@ var none = (value) => {
1777
2162
  var boolean = (value) => {
1778
2163
  return typeof value === "boolean";
1779
2164
  };
1780
- var string = (value) => {
2165
+ var string2 = (value) => {
1781
2166
  return typeof value === "string";
1782
2167
  };
1783
2168
  var mapping = (value) => {
@@ -1809,8 +2194,11 @@ var empty = (value) => {
1809
2194
  return true;
1810
2195
  if (typeof value === "string" || Array.isArray(value))
1811
2196
  return value.length === 0;
1812
- if (typeof value === "object")
1813
- return Object.keys(value).length === 0;
2197
+ if (typeof value === "object") {
2198
+ for (const _ in value)
2199
+ return false;
2200
+ return true;
2201
+ }
1814
2202
  return false;
1815
2203
  };
1816
2204
  var in_ = (value, container) => {
@@ -1837,8 +2225,11 @@ var truthy = (value) => {
1837
2225
  return value.length > 0;
1838
2226
  if (Array.isArray(value))
1839
2227
  return value.length > 0;
1840
- if (typeof value === "object")
1841
- return Object.keys(value).length > 0;
2228
+ if (typeof value === "object") {
2229
+ for (const _ in value)
2230
+ return true;
2231
+ return false;
2232
+ }
1842
2233
  return true;
1843
2234
  };
1844
2235
  var falsy = (value) => !truthy(value);
@@ -1861,7 +2252,7 @@ var builtinTests = {
1861
2252
  undefined: undefined2,
1862
2253
  none,
1863
2254
  boolean,
1864
- string,
2255
+ string: string2,
1865
2256
  mapping,
1866
2257
  iterable,
1867
2258
  sequence,
@@ -1906,91 +2297,154 @@ class Runtime {
1906
2297
  const ctx = new Context({ ...this.options.globals, ...context });
1907
2298
  this.blocks.clear();
1908
2299
  this.parentTemplate = null;
1909
- await this.collectBlocks(ast, ctx);
1910
- if (this.parentTemplate) {
1911
- return this.renderTemplate(this.parentTemplate, ctx);
2300
+ const needsAsync = this.templateNeedsAsync(ast);
2301
+ if (needsAsync) {
2302
+ await this.collectBlocks(ast, ctx);
2303
+ if (this.parentTemplate) {
2304
+ return this.renderTemplateAsync(this.parentTemplate, ctx);
2305
+ }
2306
+ return this.renderTemplateAsync(ast, ctx);
1912
2307
  }
1913
- return this.renderTemplate(ast, ctx);
2308
+ this.collectBlocksSync(ast);
2309
+ return this.renderTemplateSync(ast, ctx);
1914
2310
  }
1915
- async collectBlocks(ast, ctx) {
2311
+ templateNeedsAsync(ast) {
1916
2312
  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") {
2313
+ if (node.type === "Extends" || node.type === "Include")
2314
+ return true;
2315
+ if (node.type === "If") {
2316
+ const ifNode = node;
2317
+ if (this.nodesNeedAsync(ifNode.body))
2318
+ return true;
2319
+ for (const elif of ifNode.elifs) {
2320
+ if (this.nodesNeedAsync(elif.body))
2321
+ return true;
2322
+ }
2323
+ if (this.nodesNeedAsync(ifNode.else_))
2324
+ return true;
2325
+ }
2326
+ if (node.type === "For") {
2327
+ const forNode = node;
2328
+ if (this.nodesNeedAsync(forNode.body))
2329
+ return true;
2330
+ if (this.nodesNeedAsync(forNode.else_))
2331
+ return true;
2332
+ }
2333
+ if (node.type === "Block") {
2334
+ if (this.nodesNeedAsync(node.body))
2335
+ return true;
2336
+ }
2337
+ if (node.type === "With") {
2338
+ if (this.nodesNeedAsync(node.body))
2339
+ return true;
2340
+ }
2341
+ }
2342
+ return false;
2343
+ }
2344
+ nodesNeedAsync(nodes2) {
2345
+ for (const node of nodes2) {
2346
+ if (node.type === "Extends" || node.type === "Include")
2347
+ return true;
2348
+ if (node.type === "If") {
2349
+ const ifNode = node;
2350
+ if (this.nodesNeedAsync(ifNode.body))
2351
+ return true;
2352
+ for (const elif of ifNode.elifs) {
2353
+ if (this.nodesNeedAsync(elif.body))
2354
+ return true;
2355
+ }
2356
+ if (this.nodesNeedAsync(ifNode.else_))
2357
+ return true;
2358
+ }
2359
+ if (node.type === "For") {
2360
+ const forNode = node;
2361
+ if (this.nodesNeedAsync(forNode.body))
2362
+ return true;
2363
+ if (this.nodesNeedAsync(forNode.else_))
2364
+ return true;
2365
+ }
2366
+ if (node.type === "Block") {
2367
+ if (this.nodesNeedAsync(node.body))
2368
+ return true;
2369
+ }
2370
+ if (node.type === "With") {
2371
+ if (this.nodesNeedAsync(node.body))
2372
+ return true;
2373
+ }
2374
+ }
2375
+ return false;
2376
+ }
2377
+ collectBlocksSync(ast) {
2378
+ for (const node of ast.body) {
2379
+ if (node.type === "Block") {
1922
2380
  this.blocks.set(node.name, node);
1923
2381
  }
1924
2382
  }
1925
2383
  }
1926
- async renderTemplate(ast, ctx) {
2384
+ renderTemplateSync(ast, ctx) {
1927
2385
  const parts = [];
1928
2386
  for (const node of ast.body) {
1929
- const result = await this.renderNode(node, ctx);
2387
+ const result = this.renderNodeSync(node, ctx);
1930
2388
  if (result !== null)
1931
2389
  parts.push(result);
1932
2390
  }
1933
2391
  return parts.join("");
1934
2392
  }
1935
- async renderNode(node, ctx) {
2393
+ renderNodeSync(node, ctx) {
1936
2394
  switch (node.type) {
1937
2395
  case "Text":
1938
2396
  return node.value;
1939
2397
  case "Output":
1940
- return this.renderOutput(node, ctx);
2398
+ return this.stringify(this.eval(node.expression, ctx));
1941
2399
  case "If":
1942
- return this.renderIf(node, ctx);
2400
+ return this.renderIfSync(node, ctx);
1943
2401
  case "For":
1944
- return this.renderFor(node, ctx);
2402
+ return this.renderForSync(node, ctx);
1945
2403
  case "Block":
1946
- return this.renderBlock(node, ctx);
1947
- case "Extends":
1948
- return null;
1949
- case "Include":
1950
- return this.renderInclude(node, ctx);
2404
+ return this.renderBlockSync(node, ctx);
1951
2405
  case "Set":
1952
- return this.renderSet(node, ctx);
2406
+ ctx.set(node.target, this.eval(node.value, ctx));
2407
+ return "";
1953
2408
  case "With":
1954
- return this.renderWith(node, ctx);
1955
- case "Load":
1956
- return null;
2409
+ return this.renderWithSync(node, ctx);
1957
2410
  case "Url":
1958
- return this.renderUrl(node, ctx);
2411
+ return this.renderUrlSync(node, ctx);
1959
2412
  case "Static":
1960
- return this.renderStatic(node, ctx);
2413
+ return this.renderStaticSync(node, ctx);
2414
+ case "Load":
2415
+ case "Extends":
2416
+ return null;
1961
2417
  default:
1962
2418
  return null;
1963
2419
  }
1964
2420
  }
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);
2421
+ renderIfSync(node, ctx) {
2422
+ if (this.isTruthy(this.eval(node.test, ctx))) {
2423
+ return this.renderNodesSync(node.body, ctx);
1972
2424
  }
1973
2425
  for (const elif of node.elifs) {
1974
- if (this.isTruthy(await this.evaluate(elif.test, ctx))) {
1975
- return this.renderNodes(elif.body, ctx);
2426
+ if (this.isTruthy(this.eval(elif.test, ctx))) {
2427
+ return this.renderNodesSync(elif.body, ctx);
1976
2428
  }
1977
2429
  }
1978
- if (node.else_.length > 0) {
1979
- return this.renderNodes(node.else_, ctx);
1980
- }
1981
- return "";
2430
+ return node.else_.length > 0 ? this.renderNodesSync(node.else_, ctx) : "";
1982
2431
  }
1983
- async renderFor(node, ctx) {
1984
- const iterable2 = await this.evaluate(node.iter, ctx);
2432
+ renderForSync(node, ctx) {
2433
+ const iterable2 = this.eval(node.iter, ctx);
1985
2434
  const items = this.toIterable(iterable2);
1986
- if (items.length === 0) {
1987
- return this.renderNodes(node.else_, ctx);
2435
+ const len = items.length;
2436
+ if (len === 0) {
2437
+ return this.renderNodesSync(node.else_, ctx);
1988
2438
  }
1989
- const parts = [];
2439
+ const parts = new Array(len);
2440
+ const isUnpacking = Array.isArray(node.target);
1990
2441
  ctx.push();
1991
- for (let i = 0;i < items.length; i++) {
2442
+ ctx.pushForLoop(items, 0);
2443
+ for (let i = 0;i < len; i++) {
1992
2444
  const item = items[i];
1993
- if (Array.isArray(node.target)) {
2445
+ if (i > 0)
2446
+ ctx.updateForLoop(i, items);
2447
+ if (isUnpacking) {
1994
2448
  let values;
1995
2449
  if (Array.isArray(item)) {
1996
2450
  values = item;
@@ -1999,68 +2453,42 @@ class Runtime {
1999
2453
  } else {
2000
2454
  values = [item, item];
2001
2455
  }
2002
- node.target.forEach((name, idx) => {
2003
- ctx.set(name, values[idx]);
2004
- });
2456
+ const targets = node.target;
2457
+ for (let j = 0;j < targets.length; j++) {
2458
+ ctx.set(targets[j], values[j]);
2459
+ }
2005
2460
  } else {
2006
2461
  ctx.set(node.target, item);
2007
2462
  }
2008
- ctx.pushForLoop(items, i);
2009
- const result = await this.renderNodes(node.body, ctx);
2010
- parts.push(result);
2011
- ctx.popForLoop();
2463
+ parts[i] = this.renderNodesSync(node.body, ctx);
2012
2464
  }
2465
+ ctx.popForLoop();
2013
2466
  ctx.pop();
2014
2467
  return parts.join("");
2015
2468
  }
2016
- async renderBlock(node, ctx) {
2469
+ renderBlockSync(node, ctx) {
2017
2470
  const blockToRender = this.blocks.get(node.name) || node;
2018
2471
  ctx.push();
2019
2472
  ctx.set("block", {
2020
- super: async () => {
2021
- return this.renderNodes(node.body, ctx);
2022
- }
2473
+ super: () => this.renderNodesSync(node.body, ctx)
2023
2474
  });
2024
- const result = await this.renderNodes(blockToRender.body, ctx);
2475
+ const result = this.renderNodesSync(blockToRender.body, ctx);
2025
2476
  ctx.pop();
2026
2477
  return result;
2027
2478
  }
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) {
2479
+ renderWithSync(node, ctx) {
2052
2480
  ctx.push();
2053
2481
  for (const { target, value } of node.assignments) {
2054
- ctx.set(target, await this.evaluate(value, ctx));
2482
+ ctx.set(target, this.eval(value, ctx));
2055
2483
  }
2056
- const result = await this.renderNodes(node.body, ctx);
2484
+ const result = this.renderNodesSync(node.body, ctx);
2057
2485
  ctx.pop();
2058
2486
  return result;
2059
2487
  }
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);
2488
+ renderUrlSync(node, ctx) {
2489
+ const name = this.eval(node.name, ctx);
2490
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2491
+ const kwargs = this.evalObjectSync(node.kwargs, ctx);
2064
2492
  const url = this.options.urlResolver(String(name), args, kwargs);
2065
2493
  if (node.asVar) {
2066
2494
  ctx.set(node.asVar, url);
@@ -2068,8 +2496,8 @@ class Runtime {
2068
2496
  }
2069
2497
  return url;
2070
2498
  }
2071
- async renderStatic(node, ctx) {
2072
- const path = await this.evaluate(node.path, ctx);
2499
+ renderStaticSync(node, ctx) {
2500
+ const path = this.eval(node.path, ctx);
2073
2501
  const url = this.options.staticResolver(String(path));
2074
2502
  if (node.asVar) {
2075
2503
  ctx.set(node.asVar, url);
@@ -2077,114 +2505,86 @@ class Runtime {
2077
2505
  }
2078
2506
  return url;
2079
2507
  }
2080
- async renderNodes(nodes2, ctx) {
2508
+ renderNodesSync(nodes2, ctx) {
2081
2509
  const parts = [];
2082
2510
  for (const node of nodes2) {
2083
- const result = await this.renderNode(node, ctx);
2511
+ const result = this.renderNodeSync(node, ctx);
2084
2512
  if (result !== null)
2085
2513
  parts.push(result);
2086
2514
  }
2087
2515
  return parts.join("");
2088
2516
  }
2089
- async evaluate(node, ctx) {
2517
+ eval(node, ctx) {
2090
2518
  switch (node.type) {
2091
2519
  case "Literal":
2092
2520
  return node.value;
2093
2521
  case "Name":
2094
2522
  return ctx.get(node.name);
2095
2523
  case "GetAttr":
2096
- return this.evaluateGetAttr(node, ctx);
2524
+ return this.evalGetAttr(node, ctx);
2097
2525
  case "GetItem":
2098
- return this.evaluateGetItem(node, ctx);
2526
+ return this.evalGetItem(node, ctx);
2099
2527
  case "FilterExpr":
2100
- return this.evaluateFilter(node, ctx);
2528
+ return this.evalFilter(node, ctx);
2101
2529
  case "BinaryOp":
2102
- return this.evaluateBinaryOp(node, ctx);
2530
+ return this.evalBinaryOp(node, ctx);
2103
2531
  case "UnaryOp":
2104
- return this.evaluateUnaryOp(node, ctx);
2532
+ return this.evalUnaryOp(node, ctx);
2105
2533
  case "Compare":
2106
- return this.evaluateCompare(node, ctx);
2534
+ return this.evalCompare(node, ctx);
2107
2535
  case "Conditional":
2108
- return this.evaluateConditional(node, ctx);
2536
+ return this.evalConditional(node, ctx);
2109
2537
  case "Array":
2110
- return Promise.all(node.elements.map((el) => this.evaluate(el, ctx)));
2538
+ return node.elements.map((el) => this.eval(el, ctx));
2111
2539
  case "Object":
2112
- return this.evaluateObjectLiteral(node, ctx);
2540
+ return this.evalObjectLiteral(node, ctx);
2113
2541
  case "FunctionCall":
2114
- return this.evaluateFunctionCall(node, ctx);
2542
+ return this.evalFunctionCall(node, ctx);
2115
2543
  case "TestExpr":
2116
- return this.evaluateTest(node, ctx);
2544
+ return this.evalTest(node, ctx);
2117
2545
  default:
2118
2546
  return;
2119
2547
  }
2120
2548
  }
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);
2549
+ evalGetAttr(node, ctx) {
2550
+ const obj = this.eval(node.object, ctx);
2144
2551
  if (obj == null)
2145
2552
  return;
2146
2553
  const numIndex = parseInt(node.attribute, 10);
2147
- if (!isNaN(numIndex) && Array.isArray(obj)) {
2554
+ if (!isNaN(numIndex) && Array.isArray(obj))
2148
2555
  return obj[numIndex];
2149
- }
2150
2556
  if (typeof obj === "object" && node.attribute in obj) {
2151
2557
  const value = obj[node.attribute];
2152
- if (typeof value === "function") {
2153
- return value.call(obj);
2154
- }
2155
- return value;
2558
+ return typeof value === "function" ? value.call(obj) : value;
2156
2559
  }
2157
2560
  if (typeof obj[node.attribute] === "function") {
2158
2561
  return obj[node.attribute].bind(obj);
2159
2562
  }
2160
2563
  return;
2161
2564
  }
2162
- async evaluateGetItem(node, ctx) {
2163
- const obj = await this.evaluate(node.object, ctx);
2164
- const index = await this.evaluate(node.index, ctx);
2565
+ evalGetItem(node, ctx) {
2566
+ const obj = this.eval(node.object, ctx);
2567
+ const index = this.eval(node.index, ctx);
2165
2568
  if (obj == null)
2166
2569
  return;
2167
2570
  return obj[index];
2168
2571
  }
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);
2572
+ evalFilter(node, ctx) {
2573
+ const value = this.eval(node.node, ctx);
2574
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2575
+ const kwargs = this.evalObjectSync(node.kwargs, ctx);
2173
2576
  const filter = this.filters[node.filter];
2174
- if (!filter) {
2577
+ if (!filter)
2175
2578
  throw new Error(`Unknown filter: ${node.filter}`);
2176
- }
2177
2579
  return filter(value, ...args, ...Object.values(kwargs));
2178
2580
  }
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);
2581
+ evalBinaryOp(node, ctx) {
2582
+ const left = this.eval(node.left, ctx);
2583
+ if (node.operator === "and")
2584
+ return this.isTruthy(left) ? this.eval(node.right, ctx) : left;
2585
+ if (node.operator === "or")
2586
+ return this.isTruthy(left) ? left : this.eval(node.right, ctx);
2587
+ const right = this.eval(node.right, ctx);
2188
2588
  switch (node.operator) {
2189
2589
  case "+":
2190
2590
  return typeof left === "string" || typeof right === "string" ? String(left) + String(right) : Number(left) + Number(right);
@@ -2193,20 +2593,17 @@ class Runtime {
2193
2593
  case "*":
2194
2594
  return Number(left) * Number(right);
2195
2595
  case "/":
2196
- const divisor = Number(right);
2197
- if (divisor === 0)
2198
- return 0;
2199
- return Number(left) / divisor;
2596
+ return Number(left) / Number(right);
2200
2597
  case "%":
2201
- return Number(left) % Number(right);
2598
+ return Number(right) === 0 ? NaN : Number(left) % Number(right);
2202
2599
  case "~":
2203
2600
  return String(left) + String(right);
2204
2601
  default:
2205
2602
  return;
2206
2603
  }
2207
2604
  }
2208
- async evaluateUnaryOp(node, ctx) {
2209
- const operand = await this.evaluate(node.operand, ctx);
2605
+ evalUnaryOp(node, ctx) {
2606
+ const operand = this.eval(node.operand, ctx);
2210
2607
  switch (node.operator) {
2211
2608
  case "not":
2212
2609
  return !this.isTruthy(operand);
@@ -2218,10 +2615,10 @@ class Runtime {
2218
2615
  return operand;
2219
2616
  }
2220
2617
  }
2221
- async evaluateCompare(node, ctx) {
2222
- let left = await this.evaluate(node.left, ctx);
2618
+ evalCompare(node, ctx) {
2619
+ let left = this.eval(node.left, ctx);
2223
2620
  for (const { operator, right: rightNode } of node.ops) {
2224
- const right = await this.evaluate(rightNode, ctx);
2621
+ const right = this.eval(rightNode, ctx);
2225
2622
  let result;
2226
2623
  switch (operator) {
2227
2624
  case "==":
@@ -2263,49 +2660,212 @@ class Runtime {
2263
2660
  }
2264
2661
  return true;
2265
2662
  }
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);
2663
+ evalConditional(node, ctx) {
2664
+ return this.isTruthy(this.eval(node.test, ctx)) ? this.eval(node.trueExpr, ctx) : this.eval(node.falseExpr, ctx);
2269
2665
  }
2270
- async evaluateObjectLiteral(node, ctx) {
2666
+ evalObjectLiteral(node, ctx) {
2271
2667
  const result = {};
2272
2668
  for (const { key, value } of node.pairs) {
2273
- const k = await this.evaluate(key, ctx);
2274
- result[String(k)] = await this.evaluate(value, ctx);
2669
+ result[String(this.eval(key, ctx))] = this.eval(value, ctx);
2275
2670
  }
2276
2671
  return result;
2277
2672
  }
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);
2673
+ evalFunctionCall(node, ctx) {
2674
+ const callee = this.eval(node.callee, ctx);
2675
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2676
+ const kwargs = this.evalObjectSync(node.kwargs, ctx);
2677
+ return typeof callee === "function" ? callee(...args, kwargs) : undefined;
2678
+ }
2679
+ evalTest(node, ctx) {
2680
+ if (node.test === "defined" || node.test === "undefined") {
2681
+ let isDefined = false;
2682
+ if (node.node.type === "Name") {
2683
+ isDefined = ctx.has(node.node.name);
2684
+ } else {
2685
+ isDefined = this.eval(node.node, ctx) !== undefined;
2686
+ }
2687
+ const result2 = node.test === "defined" ? isDefined : !isDefined;
2688
+ return node.negated ? !result2 : result2;
2284
2689
  }
2285
- return;
2690
+ const value = this.eval(node.node, ctx);
2691
+ const args = node.args.map((arg) => this.eval(arg, ctx));
2692
+ const test = this.tests[node.test];
2693
+ if (!test)
2694
+ throw new Error(`Unknown test: ${node.test}`);
2695
+ const result = test(value, ...args);
2696
+ return node.negated ? !result : result;
2286
2697
  }
2287
- async evaluateObject(obj, ctx) {
2698
+ evalObjectSync(obj, ctx) {
2288
2699
  const result = {};
2289
2700
  for (const [key, value] of Object.entries(obj)) {
2290
- result[key] = await this.evaluate(value, ctx);
2701
+ result[key] = this.eval(value, ctx);
2291
2702
  }
2292
2703
  return result;
2293
2704
  }
2294
- stringify(value) {
2295
- if (value == null)
2296
- return "";
2297
- if (typeof value === "boolean")
2298
- return value ? "True" : "False";
2299
- const str = String(value);
2300
- if (value.__safe__)
2301
- return str;
2302
- if (this.options.autoescape) {
2303
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
2304
- }
2305
- return str;
2306
- }
2307
- isTruthy(value) {
2308
- if (value == null)
2705
+ async collectBlocks(ast, ctx) {
2706
+ for (const node of ast.body) {
2707
+ if (node.type === "Extends") {
2708
+ const templateName = this.eval(node.template, ctx);
2709
+ this.parentTemplate = await this.options.templateLoader(String(templateName));
2710
+ await this.collectBlocks(this.parentTemplate, ctx);
2711
+ } else if (node.type === "Block") {
2712
+ this.blocks.set(node.name, node);
2713
+ }
2714
+ }
2715
+ }
2716
+ async renderTemplateAsync(ast, ctx) {
2717
+ const parts = [];
2718
+ for (const node of ast.body) {
2719
+ const result = await this.renderNodeAsync(node, ctx);
2720
+ if (result !== null)
2721
+ parts.push(result);
2722
+ }
2723
+ return parts.join("");
2724
+ }
2725
+ async renderNodeAsync(node, ctx) {
2726
+ switch (node.type) {
2727
+ case "Text":
2728
+ return node.value;
2729
+ case "Output":
2730
+ return this.stringify(this.eval(node.expression, ctx));
2731
+ case "If":
2732
+ return this.renderIfAsync(node, ctx);
2733
+ case "For":
2734
+ return this.renderForAsync(node, ctx);
2735
+ case "Block":
2736
+ return this.renderBlockAsync(node, ctx);
2737
+ case "Extends":
2738
+ return null;
2739
+ case "Include":
2740
+ return this.renderInclude(node, ctx);
2741
+ case "Set":
2742
+ ctx.set(node.target, this.eval(node.value, ctx));
2743
+ return "";
2744
+ case "With":
2745
+ return this.renderWithAsync(node, ctx);
2746
+ case "Load":
2747
+ return null;
2748
+ case "Url":
2749
+ return this.renderUrlSync(node, ctx);
2750
+ case "Static":
2751
+ return this.renderStaticSync(node, ctx);
2752
+ default:
2753
+ return null;
2754
+ }
2755
+ }
2756
+ async renderIfAsync(node, ctx) {
2757
+ if (this.isTruthy(this.eval(node.test, ctx))) {
2758
+ return this.renderNodesAsync(node.body, ctx);
2759
+ }
2760
+ for (const elif of node.elifs) {
2761
+ if (this.isTruthy(this.eval(elif.test, ctx))) {
2762
+ return this.renderNodesAsync(elif.body, ctx);
2763
+ }
2764
+ }
2765
+ return node.else_.length > 0 ? this.renderNodesAsync(node.else_, ctx) : "";
2766
+ }
2767
+ async renderForAsync(node, ctx) {
2768
+ const iterable2 = this.eval(node.iter, ctx);
2769
+ const items = this.toIterable(iterable2);
2770
+ const len = items.length;
2771
+ if (len === 0) {
2772
+ return this.renderNodesAsync(node.else_, ctx);
2773
+ }
2774
+ const parts = new Array(len);
2775
+ const isUnpacking = Array.isArray(node.target);
2776
+ ctx.push();
2777
+ ctx.pushForLoop(items, 0);
2778
+ for (let i = 0;i < len; i++) {
2779
+ const item = items[i];
2780
+ if (i > 0)
2781
+ ctx.updateForLoop(i, items);
2782
+ if (isUnpacking) {
2783
+ let values;
2784
+ if (Array.isArray(item)) {
2785
+ values = item;
2786
+ } else if (item && typeof item === "object" && (("0" in item) || ("key" in item))) {
2787
+ values = [item[0] ?? item.key, item[1] ?? item.value];
2788
+ } else {
2789
+ values = [item, item];
2790
+ }
2791
+ const targets = node.target;
2792
+ for (let j = 0;j < targets.length; j++) {
2793
+ ctx.set(targets[j], values[j]);
2794
+ }
2795
+ } else {
2796
+ ctx.set(node.target, item);
2797
+ }
2798
+ parts[i] = await this.renderNodesAsync(node.body, ctx);
2799
+ }
2800
+ ctx.popForLoop();
2801
+ ctx.pop();
2802
+ return parts.join("");
2803
+ }
2804
+ async renderBlockAsync(node, ctx) {
2805
+ const blockToRender = this.blocks.get(node.name) || node;
2806
+ ctx.push();
2807
+ const parentContent = await this.renderNodesAsync(node.body, ctx);
2808
+ ctx.set("block", {
2809
+ super: () => parentContent
2810
+ });
2811
+ const result = await this.renderNodesAsync(blockToRender.body, ctx);
2812
+ ctx.pop();
2813
+ return result;
2814
+ }
2815
+ async renderInclude(node, ctx) {
2816
+ try {
2817
+ const templateName = this.eval(node.template, ctx);
2818
+ const template = await this.options.templateLoader(String(templateName));
2819
+ let includeCtx;
2820
+ if (node.only) {
2821
+ includeCtx = new Context(node.context ? this.evalObjectSync(node.context, ctx) : {});
2822
+ } else {
2823
+ const additional = node.context ? this.evalObjectSync(node.context, ctx) : {};
2824
+ includeCtx = ctx.derived(additional);
2825
+ }
2826
+ return this.renderTemplateAsync(template, includeCtx);
2827
+ } catch (error) {
2828
+ if (node.ignoreMissing)
2829
+ return "";
2830
+ throw error;
2831
+ }
2832
+ }
2833
+ async renderWithAsync(node, ctx) {
2834
+ ctx.push();
2835
+ for (const { target, value } of node.assignments) {
2836
+ ctx.set(target, this.eval(value, ctx));
2837
+ }
2838
+ const result = await this.renderNodesAsync(node.body, ctx);
2839
+ ctx.pop();
2840
+ return result;
2841
+ }
2842
+ async renderNodesAsync(nodes2, ctx) {
2843
+ const parts = [];
2844
+ for (const node of nodes2) {
2845
+ const result = await this.renderNodeAsync(node, ctx);
2846
+ if (result !== null)
2847
+ parts.push(result);
2848
+ }
2849
+ return parts.join("");
2850
+ }
2851
+ async evaluate(node, ctx) {
2852
+ return this.eval(node, ctx);
2853
+ }
2854
+ stringify(value) {
2855
+ if (value == null)
2856
+ return "";
2857
+ if (typeof value === "boolean")
2858
+ return value ? "True" : "False";
2859
+ const str = String(value);
2860
+ if (value.__safe__)
2861
+ return str;
2862
+ if (this.options.autoescape) {
2863
+ return Bun.escapeHTML(str);
2864
+ }
2865
+ return str;
2866
+ }
2867
+ isTruthy(value) {
2868
+ if (value == null)
2309
2869
  return false;
2310
2870
  if (typeof value === "boolean")
2311
2871
  return value;
@@ -2315,8 +2875,11 @@ class Runtime {
2315
2875
  return value.length > 0;
2316
2876
  if (Array.isArray(value))
2317
2877
  return value.length > 0;
2318
- if (typeof value === "object")
2319
- return Object.keys(value).length > 0;
2878
+ if (typeof value === "object") {
2879
+ for (const _ in value)
2880
+ return true;
2881
+ return false;
2882
+ }
2320
2883
  return true;
2321
2884
  }
2322
2885
  isIn(needle, haystack) {
@@ -2354,9 +2917,1416 @@ class Runtime {
2354
2917
  }
2355
2918
  }
2356
2919
 
2920
+ // src/compiler/index.ts
2921
+ function compileToString(ast, options = {}) {
2922
+ const compiler = new Compiler(options);
2923
+ return compiler.compile(ast);
2924
+ }
2925
+ function compileToFunction(ast, options = {}) {
2926
+ const code = compileToString(ast, options);
2927
+ const fn = new Function("__ctx", "__helpers", `
2928
+ const { escape, isTruthy, toArray, applyFilter, applyTest } = __helpers;
2929
+ ${code}
2930
+ return render(__ctx);
2931
+ `);
2932
+ return (ctx) => fn(ctx, runtimeHelpers);
2933
+ }
2934
+ var runtimeHelpers = {
2935
+ escape: (value) => {
2936
+ if (value == null)
2937
+ return "";
2938
+ if (typeof value === "object" && value.__safe)
2939
+ return String(value.value ?? "");
2940
+ if (value?.__safe__)
2941
+ return String(value);
2942
+ return Bun.escapeHTML(String(value));
2943
+ },
2944
+ isTruthy: (value) => {
2945
+ if (value == null)
2946
+ return false;
2947
+ if (typeof value === "boolean")
2948
+ return value;
2949
+ if (typeof value === "number")
2950
+ return value !== 0;
2951
+ if (typeof value === "string")
2952
+ return value.length > 0;
2953
+ if (Array.isArray(value))
2954
+ return value.length > 0;
2955
+ if (typeof value === "object") {
2956
+ for (const _ in value)
2957
+ return true;
2958
+ return false;
2959
+ }
2960
+ return true;
2961
+ },
2962
+ toArray: (value) => {
2963
+ if (value == null)
2964
+ return [];
2965
+ if (Array.isArray(value))
2966
+ return value;
2967
+ if (typeof value === "string")
2968
+ return value.split("");
2969
+ if (typeof value === "object") {
2970
+ if (typeof value[Symbol.iterator] === "function")
2971
+ return [...value];
2972
+ return Object.entries(value);
2973
+ }
2974
+ return [];
2975
+ },
2976
+ applyFilter: (name, value, ...args) => {
2977
+ const filter = builtinFilters[name];
2978
+ if (!filter)
2979
+ throw new Error(`Unknown filter: ${name}`);
2980
+ return filter(value, ...args);
2981
+ },
2982
+ applyTest: (name, value, ...args) => {
2983
+ const test = builtinTests[name];
2984
+ if (!test)
2985
+ throw new Error(`Unknown test: ${name}`);
2986
+ return test(value, ...args);
2987
+ }
2988
+ };
2989
+
2990
+ class Compiler {
2991
+ options;
2992
+ indent = 0;
2993
+ varCounter = 0;
2994
+ loopStack = [];
2995
+ localVars = [];
2996
+ constructor(options = {}) {
2997
+ this.options = {
2998
+ functionName: options.functionName ?? "render",
2999
+ inlineHelpers: options.inlineHelpers ?? true,
3000
+ minify: options.minify ?? false,
3001
+ autoescape: options.autoescape ?? true
3002
+ };
3003
+ }
3004
+ pushScope() {
3005
+ this.localVars.push(new Set);
3006
+ }
3007
+ popScope() {
3008
+ this.localVars.pop();
3009
+ }
3010
+ addLocalVar(name) {
3011
+ if (this.localVars.length > 0) {
3012
+ this.localVars[this.localVars.length - 1].add(name);
3013
+ }
3014
+ }
3015
+ isLocalVar(name) {
3016
+ for (let i = this.localVars.length - 1;i >= 0; i--) {
3017
+ if (this.localVars[i].has(name))
3018
+ return true;
3019
+ }
3020
+ return false;
3021
+ }
3022
+ compile(ast) {
3023
+ const body = this.compileNodes(ast.body);
3024
+ const nl = this.options.minify ? "" : `
3025
+ `;
3026
+ return `function ${this.options.functionName}(__ctx) {${nl}` + ` let __out = '';${nl}` + body + ` return __out;${nl}` + `}`;
3027
+ }
3028
+ compileNodes(nodes2) {
3029
+ return nodes2.map((node) => this.compileNode(node)).join("");
3030
+ }
3031
+ compileNode(node) {
3032
+ switch (node.type) {
3033
+ case "Text":
3034
+ return this.compileText(node);
3035
+ case "Output":
3036
+ return this.compileOutput(node);
3037
+ case "If":
3038
+ return this.compileIf(node);
3039
+ case "For":
3040
+ return this.compileFor(node);
3041
+ case "Set":
3042
+ return this.compileSet(node);
3043
+ case "With":
3044
+ return this.compileWith(node);
3045
+ case "Comment":
3046
+ return "";
3047
+ case "Extends":
3048
+ case "Block":
3049
+ case "Include":
3050
+ throw new Error(`AOT compilation does not support '${node.type}' - use Environment.render() for templates with inheritance`);
3051
+ case "Url":
3052
+ case "Static":
3053
+ throw new Error(`AOT compilation does not support '${node.type}' tag - use Environment.render() with urlResolver/staticResolver`);
3054
+ default:
3055
+ throw new Error(`Unknown node type in AOT compiler: ${node.type}`);
3056
+ }
3057
+ }
3058
+ compileText(node) {
3059
+ const escaped = JSON.stringify(node.value);
3060
+ return ` __out += ${escaped};${this.nl()}`;
3061
+ }
3062
+ compileOutput(node) {
3063
+ const expr = this.compileExpr(node.expression);
3064
+ if (this.options.autoescape && !this.isMarkedSafe(node.expression)) {
3065
+ return ` __out += escape(${expr});${this.nl()}`;
3066
+ }
3067
+ return ` __out += (${expr}) ?? '';${this.nl()}`;
3068
+ }
3069
+ compileIf(node) {
3070
+ let code = "";
3071
+ const test = this.compileExpr(node.test);
3072
+ code += ` if (isTruthy(${test})) {${this.nl()}`;
3073
+ code += this.compileNodes(node.body);
3074
+ code += ` }`;
3075
+ for (const elif of node.elifs) {
3076
+ const elifTest = this.compileExpr(elif.test);
3077
+ code += ` else if (isTruthy(${elifTest})) {${this.nl()}`;
3078
+ code += this.compileNodes(elif.body);
3079
+ code += ` }`;
3080
+ }
3081
+ if (node.else_.length > 0) {
3082
+ code += ` else {${this.nl()}`;
3083
+ code += this.compileNodes(node.else_);
3084
+ code += ` }`;
3085
+ }
3086
+ code += this.nl();
3087
+ return code;
3088
+ }
3089
+ compileFor(node) {
3090
+ const iterVar = this.genVar("iter");
3091
+ const indexVar = this.genVar("i");
3092
+ const lenVar = this.genVar("len");
3093
+ const loopVar = this.genVar("loop");
3094
+ const itemVar = Array.isArray(node.target) ? node.target[0] : node.target;
3095
+ const valueVar = Array.isArray(node.target) && node.target[1] ? node.target[1] : null;
3096
+ const parentLoopVar = this.loopStack.length > 0 ? this.loopStack[this.loopStack.length - 1] : null;
3097
+ const iter = this.compileExpr(node.iter);
3098
+ let code = "";
3099
+ code += ` const ${iterVar} = toArray(${iter});${this.nl()}`;
3100
+ code += ` const ${lenVar} = ${iterVar}.length;${this.nl()}`;
3101
+ if (node.else_.length > 0) {
3102
+ code += ` if (${lenVar} === 0) {${this.nl()}`;
3103
+ code += this.compileNodes(node.else_);
3104
+ code += ` } else {${this.nl()}`;
3105
+ }
3106
+ code += ` for (let ${indexVar} = 0; ${indexVar} < ${lenVar}; ${indexVar}++) {${this.nl()}`;
3107
+ if (valueVar) {
3108
+ code += ` const ${itemVar} = ${iterVar}[${indexVar}][0];${this.nl()}`;
3109
+ code += ` const ${valueVar} = ${iterVar}[${indexVar}][1];${this.nl()}`;
3110
+ } else {
3111
+ code += ` const ${itemVar} = ${iterVar}[${indexVar}];${this.nl()}`;
3112
+ }
3113
+ code += ` const ${loopVar} = {${this.nl()}`;
3114
+ code += ` counter: ${indexVar} + 1,${this.nl()}`;
3115
+ code += ` counter0: ${indexVar},${this.nl()}`;
3116
+ code += ` revcounter: ${lenVar} - ${indexVar},${this.nl()}`;
3117
+ code += ` revcounter0: ${lenVar} - ${indexVar} - 1,${this.nl()}`;
3118
+ code += ` first: ${indexVar} === 0,${this.nl()}`;
3119
+ code += ` last: ${indexVar} === ${lenVar} - 1,${this.nl()}`;
3120
+ code += ` length: ${lenVar},${this.nl()}`;
3121
+ code += ` index: ${indexVar} + 1,${this.nl()}`;
3122
+ code += ` index0: ${indexVar},${this.nl()}`;
3123
+ if (parentLoopVar) {
3124
+ code += ` parentloop: ${parentLoopVar},${this.nl()}`;
3125
+ code += ` parent: ${parentLoopVar}${this.nl()}`;
3126
+ } else {
3127
+ code += ` parentloop: null,${this.nl()}`;
3128
+ code += ` parent: null${this.nl()}`;
3129
+ }
3130
+ code += ` };${this.nl()}`;
3131
+ code += ` const forloop = ${loopVar};${this.nl()}`;
3132
+ code += ` const loop = ${loopVar};${this.nl()}`;
3133
+ this.loopStack.push(loopVar);
3134
+ const bodyCode = this.compileNodes(node.body);
3135
+ code += bodyCode.replace(new RegExp(`__ctx\\.${itemVar}`, "g"), itemVar);
3136
+ this.loopStack.pop();
3137
+ code += ` }${this.nl()}`;
3138
+ if (node.else_.length > 0) {
3139
+ code += ` }${this.nl()}`;
3140
+ }
3141
+ return code;
3142
+ }
3143
+ compileSet(node) {
3144
+ const value = this.compileExpr(node.value);
3145
+ return ` const ${node.target} = ${value};${this.nl()}`;
3146
+ }
3147
+ compileWith(node) {
3148
+ let code = ` {${this.nl()}`;
3149
+ this.pushScope();
3150
+ for (const { target, value } of node.assignments) {
3151
+ const valueExpr = this.compileExpr(value);
3152
+ code += ` const ${target} = ${valueExpr};${this.nl()}`;
3153
+ this.addLocalVar(target);
3154
+ }
3155
+ code += this.compileNodes(node.body);
3156
+ code += ` }${this.nl()}`;
3157
+ this.popScope();
3158
+ return code;
3159
+ }
3160
+ compileExpr(node) {
3161
+ switch (node.type) {
3162
+ case "Name":
3163
+ return this.compileName(node);
3164
+ case "Literal":
3165
+ return this.compileLiteral(node);
3166
+ case "Array":
3167
+ return this.compileArray(node);
3168
+ case "Object":
3169
+ return this.compileObject(node);
3170
+ case "BinaryOp":
3171
+ return this.compileBinaryOp(node);
3172
+ case "UnaryOp":
3173
+ return this.compileUnaryOp(node);
3174
+ case "Compare":
3175
+ return this.compileCompare(node);
3176
+ case "GetAttr":
3177
+ return this.compileGetAttr(node);
3178
+ case "GetItem":
3179
+ return this.compileGetItem(node);
3180
+ case "FilterExpr":
3181
+ return this.compileFilter(node);
3182
+ case "TestExpr":
3183
+ return this.compileTest(node);
3184
+ case "Conditional":
3185
+ return this.compileConditional(node);
3186
+ default:
3187
+ return "undefined";
3188
+ }
3189
+ }
3190
+ compileName(node) {
3191
+ if (node.name === "true" || node.name === "True")
3192
+ return "true";
3193
+ if (node.name === "false" || node.name === "False")
3194
+ return "false";
3195
+ if (node.name === "none" || node.name === "None" || node.name === "null")
3196
+ return "null";
3197
+ if (node.name === "forloop" || node.name === "loop")
3198
+ return node.name;
3199
+ if (this.isLocalVar(node.name)) {
3200
+ return node.name;
3201
+ }
3202
+ return `__ctx.${node.name}`;
3203
+ }
3204
+ compileLiteral(node) {
3205
+ if (typeof node.value === "string") {
3206
+ return JSON.stringify(node.value);
3207
+ }
3208
+ return String(node.value);
3209
+ }
3210
+ compileArray(node) {
3211
+ const elements = node.elements.map((el) => this.compileExpr(el)).join(", ");
3212
+ return `[${elements}]`;
3213
+ }
3214
+ compileObject(node) {
3215
+ const pairs = node.pairs.map(({ key, value }) => {
3216
+ const k = this.compileExpr(key);
3217
+ const v = this.compileExpr(value);
3218
+ return `[${k}]: ${v}`;
3219
+ }).join(", ");
3220
+ return `{${pairs}}`;
3221
+ }
3222
+ compileBinaryOp(node) {
3223
+ const left = this.compileExpr(node.left);
3224
+ const right = this.compileExpr(node.right);
3225
+ switch (node.operator) {
3226
+ case "and":
3227
+ return `(${left} && ${right})`;
3228
+ case "or":
3229
+ return `(${left} || ${right})`;
3230
+ case "~":
3231
+ return `(String(${left}) + String(${right}))`;
3232
+ case "in":
3233
+ return `(Array.isArray(${right}) ? ${right}.includes(${left}) : String(${right}).includes(String(${left})))`;
3234
+ case "not in":
3235
+ return `!(Array.isArray(${right}) ? ${right}.includes(${left}) : String(${right}).includes(String(${left})))`;
3236
+ default:
3237
+ return `(${left} ${node.operator} ${right})`;
3238
+ }
3239
+ }
3240
+ compileUnaryOp(node) {
3241
+ const operand = this.compileExpr(node.operand);
3242
+ switch (node.operator) {
3243
+ case "not":
3244
+ return `!isTruthy(${operand})`;
3245
+ case "-":
3246
+ return `-(${operand})`;
3247
+ case "+":
3248
+ return `+(${operand})`;
3249
+ default:
3250
+ return operand;
3251
+ }
3252
+ }
3253
+ compileCompare(node) {
3254
+ let result = this.compileExpr(node.left);
3255
+ for (const { operator, right } of node.ops) {
3256
+ const rightExpr = this.compileExpr(right);
3257
+ switch (operator) {
3258
+ case "==":
3259
+ case "===":
3260
+ result = `(${result} === ${rightExpr})`;
3261
+ break;
3262
+ case "!=":
3263
+ case "!==":
3264
+ result = `(${result} !== ${rightExpr})`;
3265
+ break;
3266
+ default:
3267
+ result = `(${result} ${operator} ${rightExpr})`;
3268
+ }
3269
+ }
3270
+ return result;
3271
+ }
3272
+ compileGetAttr(node) {
3273
+ const obj = this.compileExpr(node.object);
3274
+ return `${obj}?.${node.attribute}`;
3275
+ }
3276
+ compileGetItem(node) {
3277
+ const obj = this.compileExpr(node.object);
3278
+ const index = this.compileExpr(node.index);
3279
+ return `${obj}?.[${index}]`;
3280
+ }
3281
+ compileFilter(node) {
3282
+ const value = this.compileExpr(node.node);
3283
+ const args = node.args.map((arg) => this.compileExpr(arg));
3284
+ switch (node.filter) {
3285
+ case "upper":
3286
+ return `String(${value}).toUpperCase()`;
3287
+ case "lower":
3288
+ return `String(${value}).toLowerCase()`;
3289
+ case "title":
3290
+ return `String(${value}).replace(/\\b\\w/g, c => c.toUpperCase())`;
3291
+ case "trim":
3292
+ return `String(${value}).trim()`;
3293
+ case "length":
3294
+ return `(${value}?.length ?? Object.keys(${value} ?? {}).length)`;
3295
+ case "first":
3296
+ return `(${value})?.[0]`;
3297
+ case "last":
3298
+ return `(${value})?.[(${value})?.length - 1]`;
3299
+ case "default":
3300
+ return `((${value}) ?? ${args[0] ?? '""'})`;
3301
+ case "safe":
3302
+ return `{ __safe: true, value: String(${value}) }`;
3303
+ case "escape":
3304
+ case "e":
3305
+ return `escape(${value})`;
3306
+ case "join":
3307
+ return `(${value} ?? []).join(${args[0] ?? '""'})`;
3308
+ case "abs":
3309
+ return `Math.abs(${value})`;
3310
+ case "round":
3311
+ return args.length ? `Number(${value}).toFixed(${args[0]})` : `Math.round(${value})`;
3312
+ case "int":
3313
+ return `parseInt(${value}, 10)`;
3314
+ case "float":
3315
+ return `parseFloat(${value})`;
3316
+ case "floatformat":
3317
+ return `Number(${value}).toFixed(${args[0] ?? 1})`;
3318
+ case "filesizeformat":
3319
+ return `applyFilter('filesizeformat', ${value})`;
3320
+ default:
3321
+ const argsStr = args.length ? ", " + args.join(", ") : "";
3322
+ return `applyFilter('${node.filter}', ${value}${argsStr})`;
3323
+ }
3324
+ }
3325
+ compileTest(node) {
3326
+ const value = this.compileExpr(node.node);
3327
+ const args = node.args.map((arg) => this.compileExpr(arg));
3328
+ const negation = node.negated ? "!" : "";
3329
+ switch (node.test) {
3330
+ case "defined":
3331
+ return `${negation}(${value} !== undefined)`;
3332
+ case "undefined":
3333
+ return `${negation}(${value} === undefined)`;
3334
+ case "none":
3335
+ return `${negation}(${value} === null)`;
3336
+ case "even":
3337
+ return `${negation}(${value} % 2 === 0)`;
3338
+ case "odd":
3339
+ return `${negation}(${value} % 2 !== 0)`;
3340
+ case "divisibleby":
3341
+ return `${negation}(${value} % ${args[0]} === 0)`;
3342
+ case "empty":
3343
+ return `${negation}((${value} == null) || (${value}.length === 0) || (Object.keys(${value}).length === 0))`;
3344
+ case "iterable":
3345
+ return `${negation}(Array.isArray(${value}) || typeof ${value} === 'string')`;
3346
+ case "number":
3347
+ return `${negation}(typeof ${value} === 'number' && !isNaN(${value}))`;
3348
+ case "string":
3349
+ return `${negation}(typeof ${value} === 'string')`;
3350
+ default:
3351
+ const argsStr = args.length ? ", " + args.join(", ") : "";
3352
+ return `${negation}applyTest('${node.test}', ${value}${argsStr})`;
3353
+ }
3354
+ }
3355
+ compileConditional(node) {
3356
+ const test = this.compileExpr(node.test);
3357
+ const trueExpr = this.compileExpr(node.trueExpr);
3358
+ const falseExpr = this.compileExpr(node.falseExpr);
3359
+ return `(isTruthy(${test}) ? ${trueExpr} : ${falseExpr})`;
3360
+ }
3361
+ isMarkedSafe(node) {
3362
+ if (node.type === "FilterExpr") {
3363
+ const filter = node;
3364
+ return filter.filter === "safe";
3365
+ }
3366
+ return false;
3367
+ }
3368
+ genVar(prefix) {
3369
+ return `__${prefix}${this.varCounter++}`;
3370
+ }
3371
+ nl() {
3372
+ return this.options.minify ? "" : `
3373
+ `;
3374
+ }
3375
+ }
3376
+
3377
+ // src/compiler/flattener.ts
3378
+ function flattenTemplate(ast, options) {
3379
+ const flattener = new TemplateFlattener(options);
3380
+ return flattener.flatten(ast);
3381
+ }
3382
+ function canFlatten(ast) {
3383
+ const checker = new StaticChecker;
3384
+ return checker.check(ast);
3385
+ }
3386
+
3387
+ class TemplateFlattener {
3388
+ loader;
3389
+ maxDepth;
3390
+ blocks = new Map;
3391
+ depth = 0;
3392
+ constructor(options) {
3393
+ this.loader = options.loader;
3394
+ this.maxDepth = options.maxDepth ?? 10;
3395
+ }
3396
+ flatten(ast) {
3397
+ this.blocks.clear();
3398
+ this.depth = 0;
3399
+ return this.processTemplate(ast);
3400
+ }
3401
+ processTemplate(ast, isChild = true) {
3402
+ if (this.depth > this.maxDepth) {
3403
+ throw new Error(`Maximum template inheritance depth (${this.maxDepth}) exceeded`);
3404
+ }
3405
+ this.collectBlocks(ast.body, isChild);
3406
+ const extendsNode = this.findExtends(ast.body);
3407
+ if (extendsNode) {
3408
+ const parentName = this.getStaticTemplateName(extendsNode.template);
3409
+ const parentSource = this.loader.load(parentName);
3410
+ const parentAst = this.loader.parse(parentSource);
3411
+ this.depth++;
3412
+ const flattenedParent = this.processTemplate(parentAst, false);
3413
+ this.depth--;
3414
+ return {
3415
+ type: "Template",
3416
+ body: this.replaceBlocks(flattenedParent.body),
3417
+ line: ast.line,
3418
+ column: ast.column
3419
+ };
3420
+ }
3421
+ return {
3422
+ type: "Template",
3423
+ body: this.processNodes(ast.body),
3424
+ line: ast.line,
3425
+ column: ast.column
3426
+ };
3427
+ }
3428
+ collectBlocks(nodes2, override = true) {
3429
+ for (const node of nodes2) {
3430
+ if (node.type === "Block") {
3431
+ const block = node;
3432
+ if (override || !this.blocks.has(block.name)) {
3433
+ this.blocks.set(block.name, block);
3434
+ }
3435
+ }
3436
+ this.collectBlocksFromNode(node, override);
3437
+ }
3438
+ }
3439
+ collectBlocksFromNode(node, override = true) {
3440
+ switch (node.type) {
3441
+ case "If": {
3442
+ const ifNode = node;
3443
+ this.collectBlocks(ifNode.body, override);
3444
+ for (const elif of ifNode.elifs) {
3445
+ this.collectBlocks(elif.body, override);
3446
+ }
3447
+ this.collectBlocks(ifNode.else_, override);
3448
+ break;
3449
+ }
3450
+ case "For": {
3451
+ const forNode = node;
3452
+ this.collectBlocks(forNode.body, override);
3453
+ this.collectBlocks(forNode.else_, override);
3454
+ break;
3455
+ }
3456
+ case "With": {
3457
+ const withNode = node;
3458
+ this.collectBlocks(withNode.body, override);
3459
+ break;
3460
+ }
3461
+ case "Block": {
3462
+ const blockNode = node;
3463
+ this.collectBlocks(blockNode.body, override);
3464
+ break;
3465
+ }
3466
+ }
3467
+ }
3468
+ findExtends(nodes2) {
3469
+ for (const node of nodes2) {
3470
+ if (node.type === "Extends") {
3471
+ return node;
3472
+ }
3473
+ }
3474
+ return null;
3475
+ }
3476
+ processNodes(nodes2) {
3477
+ const result = [];
3478
+ for (const node of nodes2) {
3479
+ if (node.type === "Extends") {
3480
+ continue;
3481
+ }
3482
+ if (node.type === "Include") {
3483
+ const includeNode = node;
3484
+ const inlined = this.inlineInclude(includeNode);
3485
+ result.push(...inlined);
3486
+ continue;
3487
+ }
3488
+ if (node.type === "Block") {
3489
+ const block = node;
3490
+ const childBlock = this.blocks.get(block.name);
3491
+ if (childBlock && childBlock !== block) {
3492
+ result.push(...this.processNodes(childBlock.body));
3493
+ } else {
3494
+ result.push(...this.processNodes(block.body));
3495
+ }
3496
+ continue;
3497
+ }
3498
+ const processed = this.processNode(node);
3499
+ if (processed) {
3500
+ result.push(processed);
3501
+ }
3502
+ }
3503
+ return result;
3504
+ }
3505
+ processNode(node) {
3506
+ switch (node.type) {
3507
+ case "If": {
3508
+ const ifNode = node;
3509
+ return {
3510
+ ...ifNode,
3511
+ body: this.processNodes(ifNode.body),
3512
+ elifs: ifNode.elifs.map((elif) => ({
3513
+ test: elif.test,
3514
+ body: this.processNodes(elif.body)
3515
+ })),
3516
+ else_: this.processNodes(ifNode.else_)
3517
+ };
3518
+ }
3519
+ case "For": {
3520
+ const forNode = node;
3521
+ return {
3522
+ ...forNode,
3523
+ body: this.processNodes(forNode.body),
3524
+ else_: this.processNodes(forNode.else_)
3525
+ };
3526
+ }
3527
+ case "With": {
3528
+ const withNode = node;
3529
+ return {
3530
+ ...withNode,
3531
+ body: this.processNodes(withNode.body)
3532
+ };
3533
+ }
3534
+ default:
3535
+ return node;
3536
+ }
3537
+ }
3538
+ replaceBlocks(nodes2) {
3539
+ return this.processNodes(nodes2);
3540
+ }
3541
+ inlineInclude(node) {
3542
+ const templateName = this.getStaticTemplateName(node.template);
3543
+ try {
3544
+ const source = this.loader.load(templateName);
3545
+ const ast = this.loader.parse(source);
3546
+ this.depth++;
3547
+ const flattened = this.processTemplate(ast);
3548
+ this.depth--;
3549
+ if (node.context && Object.keys(node.context).length > 0) {
3550
+ const withNode = {
3551
+ type: "With",
3552
+ assignments: Object.entries(node.context).map(([target, value]) => ({
3553
+ target,
3554
+ value
3555
+ })),
3556
+ body: flattened.body,
3557
+ line: node.line,
3558
+ column: node.column
3559
+ };
3560
+ return [withNode];
3561
+ }
3562
+ return flattened.body;
3563
+ } catch (error) {
3564
+ if (node.ignoreMissing) {
3565
+ return [];
3566
+ }
3567
+ throw error;
3568
+ }
3569
+ }
3570
+ getStaticTemplateName(expr) {
3571
+ if (expr.type === "Literal") {
3572
+ const literal = expr;
3573
+ if (typeof literal.value === "string") {
3574
+ return literal.value;
3575
+ }
3576
+ }
3577
+ throw new Error(`AOT compilation requires static template names. ` + `Found dynamic expression at line ${expr.line}. ` + `Use Environment.render() for dynamic template names.`);
3578
+ }
3579
+ }
3580
+
3581
+ class StaticChecker {
3582
+ check(ast) {
3583
+ return this.checkNodes(ast.body);
3584
+ }
3585
+ checkNodes(nodes2) {
3586
+ for (const node of nodes2) {
3587
+ const result = this.checkNode(node);
3588
+ if (!result.canFlatten) {
3589
+ return result;
3590
+ }
3591
+ }
3592
+ return { canFlatten: true };
3593
+ }
3594
+ checkNode(node) {
3595
+ switch (node.type) {
3596
+ case "Extends": {
3597
+ const extendsNode = node;
3598
+ if (!this.isStaticName(extendsNode.template)) {
3599
+ return {
3600
+ canFlatten: false,
3601
+ reason: `Dynamic extends at line ${node.line} - use static string literal`
3602
+ };
3603
+ }
3604
+ break;
3605
+ }
3606
+ case "Include": {
3607
+ const includeNode = node;
3608
+ if (!this.isStaticName(includeNode.template)) {
3609
+ return {
3610
+ canFlatten: false,
3611
+ reason: `Dynamic include at line ${node.line} - use static string literal`
3612
+ };
3613
+ }
3614
+ break;
3615
+ }
3616
+ case "If": {
3617
+ const ifNode = node;
3618
+ let result = this.checkNodes(ifNode.body);
3619
+ if (!result.canFlatten)
3620
+ return result;
3621
+ for (const elif of ifNode.elifs) {
3622
+ result = this.checkNodes(elif.body);
3623
+ if (!result.canFlatten)
3624
+ return result;
3625
+ }
3626
+ result = this.checkNodes(ifNode.else_);
3627
+ if (!result.canFlatten)
3628
+ return result;
3629
+ break;
3630
+ }
3631
+ case "For": {
3632
+ const forNode = node;
3633
+ let result = this.checkNodes(forNode.body);
3634
+ if (!result.canFlatten)
3635
+ return result;
3636
+ result = this.checkNodes(forNode.else_);
3637
+ if (!result.canFlatten)
3638
+ return result;
3639
+ break;
3640
+ }
3641
+ case "With": {
3642
+ const withNode = node;
3643
+ const result = this.checkNodes(withNode.body);
3644
+ if (!result.canFlatten)
3645
+ return result;
3646
+ break;
3647
+ }
3648
+ case "Block": {
3649
+ const blockNode = node;
3650
+ const result = this.checkNodes(blockNode.body);
3651
+ if (!result.canFlatten)
3652
+ return result;
3653
+ break;
3654
+ }
3655
+ }
3656
+ return { canFlatten: true };
3657
+ }
3658
+ isStaticName(expr) {
3659
+ return expr.type === "Literal" && typeof expr.value === "string";
3660
+ }
3661
+ }
3662
+
3663
+ // src/debug/collector.ts
3664
+ class DebugCollector {
3665
+ data;
3666
+ constructor() {
3667
+ this.data = {
3668
+ startTime: performance.now(),
3669
+ templateChain: [],
3670
+ mode: "runtime",
3671
+ isAsync: false,
3672
+ contextKeys: [],
3673
+ contextSnapshot: {},
3674
+ filtersUsed: new Map,
3675
+ testsUsed: new Map,
3676
+ cacheHits: 0,
3677
+ cacheMisses: 0,
3678
+ warnings: []
3679
+ };
3680
+ }
3681
+ startLexer() {
3682
+ this.data._lexerStart = performance.now();
3683
+ }
3684
+ endLexer() {
3685
+ if (this.data._lexerStart) {
3686
+ this.data.lexerTime = performance.now() - this.data._lexerStart;
3687
+ }
3688
+ }
3689
+ startParser() {
3690
+ this.data._parserStart = performance.now();
3691
+ }
3692
+ endParser() {
3693
+ if (this.data._parserStart) {
3694
+ this.data.parserTime = performance.now() - this.data._parserStart;
3695
+ }
3696
+ }
3697
+ startRender() {
3698
+ this.data._renderStart = performance.now();
3699
+ }
3700
+ endRender() {
3701
+ if (this.data._renderStart) {
3702
+ this.data.renderTime = performance.now() - this.data._renderStart;
3703
+ }
3704
+ this.data.endTime = performance.now();
3705
+ this.data.totalTime = this.data.endTime - this.data.startTime;
3706
+ }
3707
+ addTemplate(name, type, parent) {
3708
+ this.data.templateChain.push({ name, type, parent });
3709
+ if (type === "root") {
3710
+ this.data.rootTemplate = name;
3711
+ }
3712
+ }
3713
+ setMode(mode) {
3714
+ this.data.mode = mode;
3715
+ }
3716
+ setAsync(isAsync) {
3717
+ this.data.isAsync = isAsync;
3718
+ }
3719
+ captureContext(context) {
3720
+ this.data.contextKeys = Object.keys(context);
3721
+ for (const [key, value] of Object.entries(context)) {
3722
+ this.data.contextSnapshot[key] = this.captureValue(value);
3723
+ }
3724
+ }
3725
+ captureValue(value, depth = 0) {
3726
+ const type = this.getType(value);
3727
+ const preview = this.getPreview(value);
3728
+ const expandable = this.isExpandable(value);
3729
+ const result = { type, preview, value, expandable };
3730
+ if (expandable && depth < 3) {
3731
+ result.children = {};
3732
+ if (Array.isArray(value)) {
3733
+ value.forEach((item, i) => {
3734
+ result.children[String(i)] = this.captureValue(item, depth + 1);
3735
+ });
3736
+ } else if (typeof value === "object" && value !== null) {
3737
+ for (const [k, v] of Object.entries(value)) {
3738
+ result.children[k] = this.captureValue(v, depth + 1);
3739
+ }
3740
+ }
3741
+ }
3742
+ return result;
3743
+ }
3744
+ isExpandable(value) {
3745
+ if (value === null || value === undefined)
3746
+ return false;
3747
+ if (Array.isArray(value))
3748
+ return value.length > 0;
3749
+ if (typeof value === "object")
3750
+ return Object.keys(value).length > 0;
3751
+ return false;
3752
+ }
3753
+ getType(value) {
3754
+ if (value === null)
3755
+ return "null";
3756
+ if (value === undefined)
3757
+ return "undefined";
3758
+ if (Array.isArray(value))
3759
+ return `Array(${value.length})`;
3760
+ if (value instanceof Date)
3761
+ return "Date";
3762
+ if (typeof value === "object")
3763
+ return "Object";
3764
+ return typeof value;
3765
+ }
3766
+ getPreview(value, maxLen = 50) {
3767
+ if (value === null)
3768
+ return "null";
3769
+ if (value === undefined)
3770
+ return "undefined";
3771
+ if (typeof value === "string") {
3772
+ return value.length > maxLen ? `"${value.slice(0, maxLen)}..."` : `"${value}"`;
3773
+ }
3774
+ if (typeof value === "number" || typeof value === "boolean") {
3775
+ return String(value);
3776
+ }
3777
+ if (Array.isArray(value)) {
3778
+ if (value.length === 0)
3779
+ return "[]";
3780
+ if (value.length <= 3) {
3781
+ const items = value.map((v) => this.getPreview(v, 15)).join(", ");
3782
+ return `[${items}]`;
3783
+ }
3784
+ return `[${this.getPreview(value[0], 15)}, ... +${value.length - 1}]`;
3785
+ }
3786
+ if (value instanceof Date) {
3787
+ return value.toISOString();
3788
+ }
3789
+ if (typeof value === "object") {
3790
+ const keys = Object.keys(value);
3791
+ if (keys.length === 0)
3792
+ return "{}";
3793
+ if (keys.length <= 2) {
3794
+ return `{ ${keys.join(", ")} }`;
3795
+ }
3796
+ return `{ ${keys.slice(0, 2).join(", ")}, ... +${keys.length - 2} }`;
3797
+ }
3798
+ if (typeof value === "function") {
3799
+ return "function()";
3800
+ }
3801
+ return String(value);
3802
+ }
3803
+ recordFilter(name) {
3804
+ this.data.filtersUsed.set(name, (this.data.filtersUsed.get(name) || 0) + 1);
3805
+ }
3806
+ recordTest(name) {
3807
+ this.data.testsUsed.set(name, (this.data.testsUsed.get(name) || 0) + 1);
3808
+ }
3809
+ recordCacheHit() {
3810
+ this.data.cacheHits++;
3811
+ }
3812
+ recordCacheMiss() {
3813
+ this.data.cacheMisses++;
3814
+ }
3815
+ addWarning(message) {
3816
+ this.data.warnings.push(message);
3817
+ }
3818
+ getData() {
3819
+ return this.data;
3820
+ }
3821
+ getSummary() {
3822
+ return {
3823
+ totalTime: this.data.totalTime || 0,
3824
+ templateCount: this.data.templateChain.length,
3825
+ filterCount: this.data.filtersUsed.size,
3826
+ mode: this.data.mode
3827
+ };
3828
+ }
3829
+ }
3830
+ var currentCollector = null;
3831
+ function startDebugCollection() {
3832
+ currentCollector = new DebugCollector;
3833
+ return currentCollector;
3834
+ }
3835
+ function endDebugCollection() {
3836
+ if (currentCollector) {
3837
+ const data = currentCollector.getData();
3838
+ currentCollector = null;
3839
+ return data;
3840
+ }
3841
+ return null;
3842
+ }
3843
+
3844
+ // src/debug/panel.ts
3845
+ var DEFAULT_OPTIONS = {
3846
+ position: "bottom-right",
3847
+ collapsed: true,
3848
+ dark: true,
3849
+ width: 420
3850
+ };
3851
+ function generateDebugPanel(data, options = {}) {
3852
+ const opts = { ...DEFAULT_OPTIONS, ...options };
3853
+ const id = `binja-debug-${Date.now()}`;
3854
+ const colors = opts.dark ? darkTheme : lightTheme;
3855
+ return `
3856
+ <!-- Binja Debug Panel -->
3857
+ <div id="${id}" class="binja-debugger" data-theme="${opts.dark ? "dark" : "light"}">
3858
+ <style>${generateStyles(id, colors, opts)}</style>
3859
+ ${generateToggle(id, data, colors)}
3860
+ ${generatePanel(id, data, colors, opts)}
3861
+ <script>${generateScript(id)}</script>
3862
+ </div>
3863
+ <!-- /Binja Debug Panel -->
3864
+ `;
3865
+ }
3866
+ var darkTheme = {
3867
+ bg: "#0f0f0f",
3868
+ bgSecondary: "#1a1a1a",
3869
+ bgTertiary: "#242424",
3870
+ border: "#2a2a2a",
3871
+ borderLight: "#333",
3872
+ text: "#e5e5e5",
3873
+ textSecondary: "#a0a0a0",
3874
+ textMuted: "#666",
3875
+ accent: "#3b82f6",
3876
+ accentHover: "#2563eb",
3877
+ success: "#22c55e",
3878
+ warning: "#eab308",
3879
+ error: "#ef4444",
3880
+ info: "#06b6d4"
3881
+ };
3882
+ var lightTheme = {
3883
+ bg: "#ffffff",
3884
+ bgSecondary: "#f8f9fa",
3885
+ bgTertiary: "#f1f3f4",
3886
+ border: "#e5e7eb",
3887
+ borderLight: "#d1d5db",
3888
+ text: "#111827",
3889
+ textSecondary: "#4b5563",
3890
+ textMuted: "#9ca3af",
3891
+ accent: "#2563eb",
3892
+ accentHover: "#1d4ed8",
3893
+ success: "#16a34a",
3894
+ warning: "#ca8a04",
3895
+ error: "#dc2626",
3896
+ info: "#0891b2"
3897
+ };
3898
+ function generateStyles(id, c, opts) {
3899
+ const pos = getPosition(opts.position);
3900
+ return `
3901
+ #${id} { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; font-size: 13px; line-height: 1.5; position: fixed; ${pos} z-index: 2147483647; }
3902
+ #${id} * { box-sizing: border-box; margin: 0; padding: 0; }
3903
+ #${id} .dbg-toggle { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; background: ${c.bg}; border: 1px solid ${c.border}; border-radius: 8px; color: ${c.text}; cursor: pointer; font-size: 12px; font-weight: 500; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: all 0.2s ease; }
3904
+ #${id} .dbg-toggle:hover { border-color: ${c.accent}; box-shadow: 0 4px 16px rgba(0,0,0,0.2); }
3905
+ #${id} .dbg-toggle svg { width: 16px; height: 16px; }
3906
+ #${id} .dbg-toggle .dbg-time { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 11px; padding: 2px 8px; background: ${c.bgTertiary}; border-radius: 4px; color: ${c.success}; }
3907
+ #${id} .dbg-panel { display: none; width: ${opts.width}px; max-height: 85vh; background: ${c.bg}; border: 1px solid ${c.border}; border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.24); overflow: hidden; margin-top: 8px; }
3908
+ #${id} .dbg-panel.open { display: block; }
3909
+ #${id} .dbg-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${c.bgSecondary}; border-bottom: 1px solid ${c.border}; }
3910
+ #${id} .dbg-logo { display: flex; align-items: center; gap: 10px; font-weight: 600; color: ${c.text}; }
3911
+ #${id} .dbg-logo svg { width: 20px; height: 20px; color: ${c.accent}; }
3912
+ #${id} .dbg-meta { display: flex; align-items: center; gap: 12px; }
3913
+ #${id} .dbg-badge { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; padding: 3px 10px; border-radius: 4px; font-weight: 500; }
3914
+ #${id} .dbg-badge.time { background: rgba(34,197,94,0.1); color: ${c.success}; }
3915
+ #${id} .dbg-badge.mode { background: rgba(59,130,246,0.1); color: ${c.accent}; }
3916
+ #${id} .dbg-close { background: none; border: none; color: ${c.textMuted}; cursor: pointer; padding: 4px; border-radius: 4px; display: flex; }
3917
+ #${id} .dbg-close:hover { background: ${c.bgTertiary}; color: ${c.text}; }
3918
+ #${id} .dbg-close svg { width: 18px; height: 18px; }
3919
+ #${id} .dbg-body { max-height: calc(85vh - 52px); overflow-y: auto; }
3920
+ #${id} .dbg-section { border-bottom: 1px solid ${c.border}; }
3921
+ #${id} .dbg-section:last-child { border-bottom: none; }
3922
+ #${id} .dbg-section-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; cursor: pointer; user-select: none; transition: background 0.15s; }
3923
+ #${id} .dbg-section-header:hover { background: ${c.bgSecondary}; }
3924
+ #${id} .dbg-section-title { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: ${c.textSecondary}; }
3925
+ #${id} .dbg-section-title svg { width: 14px; height: 14px; opacity: 0.7; }
3926
+ #${id} .dbg-section-meta { font-size: 11px; color: ${c.textMuted}; font-family: 'SF Mono', Monaco, monospace; }
3927
+ #${id} .dbg-section-content { display: none; padding: 12px 16px; background: ${c.bgSecondary}; }
3928
+ #${id} .dbg-section.open .dbg-section-content { display: block; }
3929
+ #${id} .dbg-section .dbg-chevron { transition: transform 0.2s; color: ${c.textMuted}; }
3930
+ #${id} .dbg-section.open .dbg-chevron { transform: rotate(90deg); }
3931
+ #${id} .dbg-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid ${c.border}; }
3932
+ #${id} .dbg-row:last-child { border-bottom: none; }
3933
+ #${id} .dbg-label { color: ${c.textSecondary}; font-size: 12px; }
3934
+ #${id} .dbg-value { color: ${c.text}; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; text-align: right; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3935
+ #${id} .dbg-bar { height: 3px; background: ${c.bgTertiary}; border-radius: 2px; margin-top: 4px; overflow: hidden; }
3936
+ #${id} .dbg-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s ease; }
3937
+ #${id} .dbg-bar-fill.lexer { background: ${c.info}; }
3938
+ #${id} .dbg-bar-fill.parser { background: ${c.warning}; }
3939
+ #${id} .dbg-bar-fill.render { background: ${c.success}; }
3940
+ #${id} .dbg-templates { display: flex; flex-direction: column; gap: 6px; }
3941
+ #${id} .dbg-template { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: ${c.bg}; border-radius: 6px; font-size: 12px; }
3942
+ #${id} .dbg-template-icon { width: 16px; height: 16px; color: ${c.textMuted}; flex-shrink: 0; }
3943
+ #${id} .dbg-template-name { color: ${c.text}; font-family: 'SF Mono', Monaco, monospace; }
3944
+ #${id} .dbg-template-tag { font-size: 10px; padding: 2px 6px; border-radius: 3px; font-weight: 500; text-transform: uppercase; }
3945
+ #${id} .dbg-template-tag.root { background: rgba(59,130,246,0.15); color: ${c.accent}; }
3946
+ #${id} .dbg-template-tag.extends { background: rgba(168,85,247,0.15); color: #a855f7; }
3947
+ #${id} .dbg-template-tag.include { background: rgba(34,197,94,0.15); color: ${c.success}; }
3948
+ #${id} .dbg-ctx-grid { display: flex; flex-direction: column; gap: 4px; }
3949
+ #${id} .dbg-ctx-item { background: ${c.bg}; border-radius: 6px; overflow: hidden; }
3950
+ #${id} .dbg-ctx-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; cursor: default; }
3951
+ #${id} .dbg-ctx-row.expandable { cursor: pointer; }
3952
+ #${id} .dbg-ctx-row.expandable:hover { background: ${c.bgTertiary}; }
3953
+ #${id} .dbg-ctx-key { display: flex; align-items: center; gap: 6px; }
3954
+ #${id} .dbg-ctx-arrow { width: 12px; height: 12px; color: ${c.textMuted}; transition: transform 0.15s; flex-shrink: 0; }
3955
+ #${id} .dbg-ctx-item.open > .dbg-ctx-row .dbg-ctx-arrow { transform: rotate(90deg); }
3956
+ #${id} .dbg-ctx-name { color: ${c.text}; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; }
3957
+ #${id} .dbg-ctx-type { font-size: 10px; color: ${c.accent}; background: rgba(59,130,246,0.1); padding: 1px 5px; border-radius: 3px; }
3958
+ #${id} .dbg-ctx-preview { color: ${c.textSecondary}; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3959
+ #${id} .dbg-ctx-children { display: none; padding-left: 16px; border-left: 1px solid ${c.border}; margin-left: 10px; }
3960
+ #${id} .dbg-ctx-item.open > .dbg-ctx-children { display: block; }
3961
+ #${id} .dbg-ctx-children .dbg-ctx-item { background: transparent; }
3962
+ #${id} .dbg-ctx-children .dbg-ctx-row { padding: 4px 8px; }
3963
+ #${id} .dbg-filters { display: flex; flex-wrap: wrap; gap: 6px; }
3964
+ #${id} .dbg-filter { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; background: ${c.bg}; border-radius: 5px; font-size: 12px; font-family: 'SF Mono', Monaco, monospace; color: ${c.text}; }
3965
+ #${id} .dbg-filter-count { font-size: 10px; color: ${c.accent}; font-weight: 600; }
3966
+ #${id} .dbg-cache { display: flex; gap: 16px; }
3967
+ #${id} .dbg-cache-stat { flex: 1; padding: 12px; background: ${c.bg}; border-radius: 6px; text-align: center; }
3968
+ #${id} .dbg-cache-num { font-size: 24px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; }
3969
+ #${id} .dbg-cache-num.hit { color: ${c.success}; }
3970
+ #${id} .dbg-cache-num.miss { color: ${c.error}; }
3971
+ #${id} .dbg-cache-label { font-size: 11px; color: ${c.textMuted}; margin-top: 4px; }
3972
+ #${id} .dbg-warnings { display: flex; flex-direction: column; gap: 6px; }
3973
+ #${id} .dbg-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(234,179,8,0.1); border-radius: 6px; border-left: 3px solid ${c.warning}; }
3974
+ #${id} .dbg-warning-icon { color: ${c.warning}; flex-shrink: 0; margin-top: 1px; }
3975
+ #${id} .dbg-warning-text { color: ${c.text}; font-size: 12px; }
3976
+ `;
3977
+ }
3978
+ function getPosition(pos) {
3979
+ switch (pos) {
3980
+ case "bottom-left":
3981
+ return "bottom: 16px; left: 16px;";
3982
+ case "top-right":
3983
+ return "top: 16px; right: 16px;";
3984
+ case "top-left":
3985
+ return "top: 16px; left: 16px;";
3986
+ default:
3987
+ return "bottom: 16px; right: 16px;";
3988
+ }
3989
+ }
3990
+ var icons = {
3991
+ logo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`,
3992
+ close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
3993
+ chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
3994
+ perf: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>`,
3995
+ template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>`,
3996
+ context: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>`,
3997
+ filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
3998
+ cache: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1010 10H12V2z"/><path d="M12 2a10 10 0 00-8.66 15"/></svg>`,
3999
+ warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
4000
+ file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg>`
4001
+ };
4002
+ function generateToggle(id, data, c) {
4003
+ const time2 = (data.totalTime || 0).toFixed(1);
4004
+ return `
4005
+ <button class="dbg-toggle" onclick="document.querySelector('#${id} .dbg-panel').classList.add('open');this.style.display='none'">
4006
+ ${icons.logo}
4007
+ <span>Binja</span>
4008
+ <span class="dbg-time">${time2}ms</span>
4009
+ </button>`;
4010
+ }
4011
+ function generatePanel(id, data, c, opts) {
4012
+ const time2 = (data.totalTime || 0).toFixed(2);
4013
+ const mode = data.mode === "aot" ? "AOT" : "Runtime";
4014
+ return `
4015
+ <div class="dbg-panel">
4016
+ <div class="dbg-header">
4017
+ <div class="dbg-logo">${icons.logo} Binja Debugger</div>
4018
+ <div class="dbg-meta">
4019
+ <span class="dbg-badge mode">${mode}</span>
4020
+ <span class="dbg-badge time">${time2}ms</span>
4021
+ <button class="dbg-close" onclick="document.querySelector('#${id} .dbg-panel').classList.remove('open');document.querySelector('#${id} .dbg-toggle').style.display='inline-flex'">${icons.close}</button>
4022
+ </div>
4023
+ </div>
4024
+ <div class="dbg-body">
4025
+ ${generatePerfSection(data)}
4026
+ ${generateTemplatesSection(data)}
4027
+ ${generateContextSection(data)}
4028
+ ${generateFiltersSection(data)}
4029
+ ${generateCacheSection(data)}
4030
+ ${generateWarningsSection(data)}
4031
+ </div>
4032
+ </div>`;
4033
+ }
4034
+ function generatePerfSection(data) {
4035
+ const total = data.totalTime || 0.01;
4036
+ const lexer = data.lexerTime || 0;
4037
+ const parser = data.parserTime || 0;
4038
+ const render = data.renderTime || 0;
4039
+ return `
4040
+ <div class="dbg-section open">
4041
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4042
+ <span class="dbg-section-title">${icons.perf} Performance</span>
4043
+ <span class="dbg-chevron">${icons.chevron}</span>
4044
+ </div>
4045
+ <div class="dbg-section-content">
4046
+ <div class="dbg-row">
4047
+ <span class="dbg-label">Lexer</span>
4048
+ <span class="dbg-value">${lexer.toFixed(2)}ms</span>
4049
+ </div>
4050
+ <div class="dbg-bar"><div class="dbg-bar-fill lexer" style="width:${lexer / total * 100}%"></div></div>
4051
+ <div class="dbg-row">
4052
+ <span class="dbg-label">Parser</span>
4053
+ <span class="dbg-value">${parser.toFixed(2)}ms</span>
4054
+ </div>
4055
+ <div class="dbg-bar"><div class="dbg-bar-fill parser" style="width:${parser / total * 100}%"></div></div>
4056
+ <div class="dbg-row">
4057
+ <span class="dbg-label">Render</span>
4058
+ <span class="dbg-value">${render.toFixed(2)}ms</span>
4059
+ </div>
4060
+ <div class="dbg-bar"><div class="dbg-bar-fill render" style="width:${render / total * 100}%"></div></div>
4061
+ <div class="dbg-row" style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.1)">
4062
+ <span class="dbg-label" style="font-weight:600">Total</span>
4063
+ <span class="dbg-value" style="font-weight:600">${total.toFixed(2)}ms</span>
4064
+ </div>
4065
+ </div>
4066
+ </div>`;
4067
+ }
4068
+ function generateTemplatesSection(data) {
4069
+ if (data.templateChain.length === 0)
4070
+ return "";
4071
+ const templates = data.templateChain.map((t) => `
4072
+ <div class="dbg-template">
4073
+ ${icons.file}
4074
+ <span class="dbg-template-name">${t.name}</span>
4075
+ <span class="dbg-template-tag ${t.type}">${t.type}</span>
4076
+ </div>
4077
+ `).join("");
4078
+ return `
4079
+ <div class="dbg-section">
4080
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4081
+ <span class="dbg-section-title">${icons.template} Templates</span>
4082
+ <span class="dbg-section-meta">${data.templateChain.length}</span>
4083
+ <span class="dbg-chevron">${icons.chevron}</span>
4084
+ </div>
4085
+ <div class="dbg-section-content">
4086
+ <div class="dbg-templates">${templates}</div>
4087
+ </div>
4088
+ </div>`;
4089
+ }
4090
+ function generateContextSection(data) {
4091
+ const keys = Object.keys(data.contextSnapshot);
4092
+ if (keys.length === 0)
4093
+ return "";
4094
+ const items = keys.map((key) => renderContextValue(key, data.contextSnapshot[key])).join("");
4095
+ return `
4096
+ <div class="dbg-section">
4097
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4098
+ <span class="dbg-section-title">${icons.context} Context</span>
4099
+ <span class="dbg-section-meta">${keys.length} vars</span>
4100
+ <span class="dbg-chevron">${icons.chevron}</span>
4101
+ </div>
4102
+ <div class="dbg-section-content">
4103
+ <div class="dbg-ctx-grid">${items}</div>
4104
+ </div>
4105
+ </div>`;
4106
+ }
4107
+ function renderContextValue(key, ctx) {
4108
+ const arrow = ctx.expandable ? `<svg class="dbg-ctx-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>` : "";
4109
+ const expandableClass = ctx.expandable ? "expandable" : "";
4110
+ const onClick = ctx.expandable ? `onclick="this.parentElement.classList.toggle('open')"` : "";
4111
+ let children = "";
4112
+ if (ctx.expandable && ctx.children) {
4113
+ const childItems = Object.entries(ctx.children).map(([k, v]) => renderContextValue(k, v)).join("");
4114
+ children = `<div class="dbg-ctx-children">${childItems}</div>`;
4115
+ }
4116
+ return `
4117
+ <div class="dbg-ctx-item">
4118
+ <div class="dbg-ctx-row ${expandableClass}" ${onClick}>
4119
+ <div class="dbg-ctx-key">
4120
+ ${arrow}
4121
+ <span class="dbg-ctx-name">${escapeHtml(key)}</span>
4122
+ <span class="dbg-ctx-type">${ctx.type}</span>
4123
+ </div>
4124
+ <span class="dbg-ctx-preview">${escapeHtml(ctx.preview)}</span>
4125
+ </div>
4126
+ ${children}
4127
+ </div>`;
4128
+ }
4129
+ function generateFiltersSection(data) {
4130
+ const filters = Array.from(data.filtersUsed.entries());
4131
+ if (filters.length === 0)
4132
+ return "";
4133
+ const items = filters.map(([name, count]) => `<span class="dbg-filter">${name}<span class="dbg-filter-count">\xD7${count}</span></span>`).join("");
4134
+ return `
4135
+ <div class="dbg-section">
4136
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4137
+ <span class="dbg-section-title">${icons.filter} Filters</span>
4138
+ <span class="dbg-section-meta">${filters.length}</span>
4139
+ <span class="dbg-chevron">${icons.chevron}</span>
4140
+ </div>
4141
+ <div class="dbg-section-content">
4142
+ <div class="dbg-filters">${items}</div>
4143
+ </div>
4144
+ </div>`;
4145
+ }
4146
+ function generateCacheSection(data) {
4147
+ const total = data.cacheHits + data.cacheMisses;
4148
+ if (total === 0)
4149
+ return "";
4150
+ return `
4151
+ <div class="dbg-section">
4152
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4153
+ <span class="dbg-section-title">${icons.cache} Cache</span>
4154
+ <span class="dbg-section-meta">${(data.cacheHits / total * 100).toFixed(0)}% hit</span>
4155
+ <span class="dbg-chevron">${icons.chevron}</span>
4156
+ </div>
4157
+ <div class="dbg-section-content">
4158
+ <div class="dbg-cache">
4159
+ <div class="dbg-cache-stat">
4160
+ <div class="dbg-cache-num hit">${data.cacheHits}</div>
4161
+ <div class="dbg-cache-label">Cache Hits</div>
4162
+ </div>
4163
+ <div class="dbg-cache-stat">
4164
+ <div class="dbg-cache-num miss">${data.cacheMisses}</div>
4165
+ <div class="dbg-cache-label">Cache Misses</div>
4166
+ </div>
4167
+ </div>
4168
+ </div>
4169
+ </div>`;
4170
+ }
4171
+ function generateWarningsSection(data) {
4172
+ if (data.warnings.length === 0)
4173
+ return "";
4174
+ const items = data.warnings.map((w) => `
4175
+ <div class="dbg-warning">
4176
+ ${icons.warning}
4177
+ <span class="dbg-warning-text">${escapeHtml(w)}</span>
4178
+ </div>
4179
+ `).join("");
4180
+ return `
4181
+ <div class="dbg-section open">
4182
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4183
+ <span class="dbg-section-title">${icons.warning} Warnings</span>
4184
+ <span class="dbg-section-meta" style="color:#eab308">${data.warnings.length}</span>
4185
+ <span class="dbg-chevron">${icons.chevron}</span>
4186
+ </div>
4187
+ <div class="dbg-section-content">
4188
+ <div class="dbg-warnings">${items}</div>
4189
+ </div>
4190
+ </div>`;
4191
+ }
4192
+ function generateScript(id) {
4193
+ return `
4194
+ (function(){
4195
+ var panel = document.getElementById('${id}');
4196
+ if (!panel) return;
4197
+ var header = panel.querySelector('.dbg-header');
4198
+ if (!header) return;
4199
+ var isDrag = false, startX, startY, startL, startT;
4200
+ header.style.cursor = 'grab';
4201
+ header.onmousedown = function(e) {
4202
+ if (e.target.closest('.dbg-close')) return;
4203
+ isDrag = true;
4204
+ header.style.cursor = 'grabbing';
4205
+ startX = e.clientX;
4206
+ startY = e.clientY;
4207
+ var r = panel.getBoundingClientRect();
4208
+ startL = r.left;
4209
+ startT = r.top;
4210
+ panel.style.right = 'auto';
4211
+ panel.style.bottom = 'auto';
4212
+ panel.style.left = startL + 'px';
4213
+ panel.style.top = startT + 'px';
4214
+ };
4215
+ document.onmousemove = function(e) {
4216
+ if (!isDrag) return;
4217
+ panel.style.left = (startL + e.clientX - startX) + 'px';
4218
+ panel.style.top = (startT + e.clientY - startY) + 'px';
4219
+ };
4220
+ document.onmouseup = function() {
4221
+ isDrag = false;
4222
+ header.style.cursor = 'grab';
4223
+ };
4224
+ })();`;
4225
+ }
4226
+ function escapeHtml(str) {
4227
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4228
+ }
4229
+
4230
+ // src/debug/index.ts
4231
+ async function renderWithDebug(env, templateName, context = {}, options = {}) {
4232
+ const collector = startDebugCollection();
4233
+ collector.captureContext(context);
4234
+ collector.addTemplate(templateName, "root");
4235
+ collector.setMode("runtime");
4236
+ collector.startRender();
4237
+ const html = await env.render(templateName, context);
4238
+ collector.endRender();
4239
+ const data = endDebugCollection();
4240
+ if (options.htmlOnly !== false) {
4241
+ const isHtml = html.includes("<html") || html.includes("<body") || html.includes("<!DOCTYPE");
4242
+ if (!isHtml) {
4243
+ return html;
4244
+ }
4245
+ }
4246
+ const panel = generateDebugPanel(data, options.panel);
4247
+ if (html.includes("</body>")) {
4248
+ return html.replace("</body>", `${panel}</body>`);
4249
+ }
4250
+ return html + panel;
4251
+ }
4252
+ async function renderStringWithDebug(env, source, context = {}, options = {}) {
4253
+ const collector = startDebugCollection();
4254
+ collector.captureContext(context);
4255
+ collector.setMode("runtime");
4256
+ collector.startRender();
4257
+ const html = await env.renderString(source, context);
4258
+ collector.endRender();
4259
+ const data = endDebugCollection();
4260
+ if (options.htmlOnly !== false) {
4261
+ const isHtml = html.includes("<html") || html.includes("<body");
4262
+ if (!isHtml) {
4263
+ return html;
4264
+ }
4265
+ }
4266
+ const panel = generateDebugPanel(data, options.panel);
4267
+ if (html.includes("</body>")) {
4268
+ return html.replace("</body>", `${panel}</body>`);
4269
+ }
4270
+ return html + panel;
4271
+ }
4272
+ function createDebugRenderer(env, options = {}) {
4273
+ return {
4274
+ async render(templateName, context = {}) {
4275
+ return renderWithDebug(env, templateName, context, options);
4276
+ },
4277
+ async renderString(source, context = {}) {
4278
+ return renderStringWithDebug(env, source, context, options);
4279
+ }
4280
+ };
4281
+ }
4282
+ function debugMiddleware(env, options = {}) {
4283
+ return {
4284
+ hono() {
4285
+ return async (c, next) => {
4286
+ await next();
4287
+ const contentType = c.res.headers.get("content-type") || "";
4288
+ if (!contentType.includes("text/html"))
4289
+ return;
4290
+ const body = await c.res.text();
4291
+ const collector = startDebugCollection();
4292
+ collector.captureContext({});
4293
+ collector.setMode("runtime");
4294
+ collector.endRender();
4295
+ const data = endDebugCollection();
4296
+ const panel = generateDebugPanel(data, options.panel);
4297
+ const newBody = body.includes("</body>") ? body.replace("</body>", `${panel}</body>`) : body + panel;
4298
+ c.res = new Response(newBody, {
4299
+ status: c.res.status,
4300
+ headers: c.res.headers
4301
+ });
4302
+ };
4303
+ },
4304
+ express() {
4305
+ return (req, res, next) => {
4306
+ const originalSend = res.send.bind(res);
4307
+ res.send = (body) => {
4308
+ const contentType = res.get("Content-Type") || "";
4309
+ if (!contentType.includes("text/html") || typeof body !== "string") {
4310
+ return originalSend(body);
4311
+ }
4312
+ const collector = startDebugCollection();
4313
+ collector.captureContext({});
4314
+ collector.setMode("runtime");
4315
+ collector.endRender();
4316
+ const data = endDebugCollection();
4317
+ const panel = generateDebugPanel(data, options.panel);
4318
+ const newBody = body.includes("</body>") ? body.replace("</body>", `${panel}</body>`) : body + panel;
4319
+ return originalSend(newBody);
4320
+ };
4321
+ next();
4322
+ };
4323
+ }
4324
+ };
4325
+ }
4326
+
2357
4327
  // src/index.ts
2358
- import * as fs from "fs";
2359
4328
  import * as path from "path";
4329
+ var URL_PARAM_REGEX = /<[^>]+>|:[a-zA-Z_]+|\(\?P<[^>]+>\[[^\]]+\]\)/g;
2360
4330
 
2361
4331
  class Environment {
2362
4332
  options;
@@ -2372,7 +4342,9 @@ class Environment {
2372
4342
  urlResolver: options.urlResolver ?? this.defaultUrlResolver.bind(this),
2373
4343
  staticResolver: options.staticResolver ?? this.defaultStaticResolver.bind(this),
2374
4344
  cache: options.cache ?? true,
2375
- extensions: options.extensions ?? [".html", ".jinja", ".jinja2", ""]
4345
+ extensions: options.extensions ?? [".html", ".jinja", ".jinja2", ""],
4346
+ debug: options.debug ?? false,
4347
+ debugOptions: options.debugOptions ?? {}
2376
4348
  };
2377
4349
  this.runtime = new Runtime({
2378
4350
  autoescape: this.options.autoescape,
@@ -2384,13 +4356,60 @@ class Environment {
2384
4356
  });
2385
4357
  }
2386
4358
  async render(templateName, context = {}) {
4359
+ if (this.options.debug) {
4360
+ return this.renderWithDebug(templateName, context);
4361
+ }
2387
4362
  const ast = await this.loadTemplate(templateName);
2388
4363
  return this.runtime.render(ast, context);
2389
4364
  }
2390
4365
  async renderString(source, context = {}) {
4366
+ if (this.options.debug) {
4367
+ return this.renderStringWithDebug(source, context);
4368
+ }
2391
4369
  const ast = this.compile(source);
2392
4370
  return this.runtime.render(ast, context);
2393
4371
  }
4372
+ async renderWithDebug(templateName, context) {
4373
+ const collector = startDebugCollection();
4374
+ collector.captureContext(context);
4375
+ collector.addTemplate(templateName, "root");
4376
+ collector.setMode("runtime");
4377
+ collector.startRender();
4378
+ const ast = await this.loadTemplate(templateName);
4379
+ let html = this.runtime.render(ast, context);
4380
+ if (html && typeof html.then === "function") {
4381
+ html = await html;
4382
+ }
4383
+ collector.endRender();
4384
+ const data = endDebugCollection();
4385
+ return this.injectDebugPanel(String(html || ""), data);
4386
+ }
4387
+ async renderStringWithDebug(source, context) {
4388
+ const collector = startDebugCollection();
4389
+ collector.captureContext(context);
4390
+ collector.setMode("runtime");
4391
+ collector.startRender();
4392
+ const ast = this.compile(source);
4393
+ let html = this.runtime.render(ast, context);
4394
+ if (html && typeof html.then === "function") {
4395
+ html = await html;
4396
+ }
4397
+ collector.endRender();
4398
+ const data = endDebugCollection();
4399
+ return this.injectDebugPanel(String(html || ""), data);
4400
+ }
4401
+ injectDebugPanel(html, data) {
4402
+ if (!html || typeof html !== "string")
4403
+ return html || "";
4404
+ const isHtml = html.includes("<html") || html.includes("<body") || html.includes("<!DOCTYPE");
4405
+ if (!isHtml)
4406
+ return html;
4407
+ const panel = generateDebugPanel(data, this.options.debugOptions);
4408
+ if (html.includes("</body>")) {
4409
+ return html.replace("</body>", `${panel}</body>`);
4410
+ }
4411
+ return html + panel;
4412
+ }
2394
4413
  compile(source) {
2395
4414
  const lexer = new Lexer(source);
2396
4415
  const tokens = lexer.tokenize();
@@ -2405,7 +4424,7 @@ class Environment {
2405
4424
  if (!templatePath) {
2406
4425
  throw new Error(`Template not found: ${templateName}`);
2407
4426
  }
2408
- const source = await fs.promises.readFile(templatePath, "utf-8");
4427
+ const source = await Bun.file(templatePath).text();
2409
4428
  const ast = this.compile(source);
2410
4429
  if (this.options.cache) {
2411
4430
  this.templateCache.set(templateName, ast);
@@ -2433,10 +4452,9 @@ class Environment {
2433
4452
  const basePath = path.resolve(this.options.templates, templateName);
2434
4453
  for (const ext of this.options.extensions) {
2435
4454
  const fullPath = basePath + ext;
2436
- try {
2437
- await fs.promises.access(fullPath, fs.constants.R_OK);
4455
+ if (await Bun.file(fullPath).exists()) {
2438
4456
  return fullPath;
2439
- } catch {}
4457
+ }
2440
4458
  }
2441
4459
  return null;
2442
4460
  }
@@ -2447,13 +4465,15 @@ class Environment {
2447
4465
  return `#${name}`;
2448
4466
  }
2449
4467
  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)));
4468
+ for (const key in kwargs) {
4469
+ const encoded = encodeURIComponent(String(kwargs[key]));
4470
+ url = url.replaceAll(`:${key}`, encoded);
4471
+ url = url.replaceAll(`<${key}>`, encoded);
4472
+ url = url.replaceAll(`(?P<${key}>[^/]+)`, encoded);
2454
4473
  }
2455
4474
  let argIndex = 0;
2456
- url = url.replace(/<[^>]+>|:[a-zA-Z_]+|\(\?P<[^>]+>\[[^\]]+\]\)/g, () => {
4475
+ URL_PARAM_REGEX.lastIndex = 0;
4476
+ url = url.replace(URL_PARAM_REGEX, () => {
2457
4477
  if (argIndex < args.length) {
2458
4478
  return encodeURIComponent(String(args[argIndex++]));
2459
4479
  }
@@ -2472,22 +4492,112 @@ async function render(source, context = {}, options = {}) {
2472
4492
  function Template(source, options = {}) {
2473
4493
  const env = new Environment(options);
2474
4494
  const ast = env.compile(source);
4495
+ const runtime = new Runtime({
4496
+ autoescape: options.autoescape ?? true,
4497
+ filters: options.filters ?? {},
4498
+ globals: options.globals ?? {},
4499
+ urlResolver: options.urlResolver,
4500
+ staticResolver: options.staticResolver,
4501
+ templateLoader: async () => ast
4502
+ });
2475
4503
  return {
2476
4504
  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
4505
  return runtime.render(ast, context);
2486
4506
  }
2487
4507
  };
2488
4508
  }
4509
+ function compile(source, options = {}) {
4510
+ const lexer = new Lexer(source);
4511
+ const tokens = lexer.tokenize();
4512
+ const parser = new Parser(tokens);
4513
+ const ast = parser.parse();
4514
+ return compileToFunction(ast, options);
4515
+ }
4516
+ async function compileWithInheritance(templateName, options) {
4517
+ const extensions = options.extensions ?? [".html", ".jinja", ".jinja2", ""];
4518
+ const templatesDir = path.resolve(options.templates);
4519
+ const loader = {
4520
+ load(name) {
4521
+ const basePath = path.resolve(templatesDir, name);
4522
+ for (const ext of extensions) {
4523
+ const fullPath = basePath + ext;
4524
+ const file = Bun.file(fullPath);
4525
+ const fs = __require("fs");
4526
+ if (fs.existsSync(fullPath)) {
4527
+ return fs.readFileSync(fullPath, "utf-8");
4528
+ }
4529
+ }
4530
+ throw new Error(`Template not found: ${name}`);
4531
+ },
4532
+ parse(source2) {
4533
+ const lexer = new Lexer(source2);
4534
+ const tokens = lexer.tokenize();
4535
+ const parser = new Parser(tokens);
4536
+ return parser.parse();
4537
+ }
4538
+ };
4539
+ const source = loader.load(templateName);
4540
+ const ast = loader.parse(source);
4541
+ const check = canFlatten(ast);
4542
+ if (!check.canFlatten) {
4543
+ throw new Error(`Cannot compile template with AOT: ${check.reason}
4544
+ ` + `Use Environment.render() for dynamic template names.`);
4545
+ }
4546
+ const flattenedAst = flattenTemplate(ast, { loader });
4547
+ return compileToFunction(flattenedAst, options);
4548
+ }
4549
+ async function compileWithInheritanceToCode(templateName, options) {
4550
+ const extensions = options.extensions ?? [".html", ".jinja", ".jinja2", ""];
4551
+ const templatesDir = path.resolve(options.templates);
4552
+ const fs = __require("fs");
4553
+ const loader = {
4554
+ load(name) {
4555
+ const basePath = path.resolve(templatesDir, name);
4556
+ for (const ext of extensions) {
4557
+ const fullPath = basePath + ext;
4558
+ if (fs.existsSync(fullPath)) {
4559
+ return fs.readFileSync(fullPath, "utf-8");
4560
+ }
4561
+ }
4562
+ throw new Error(`Template not found: ${name}`);
4563
+ },
4564
+ parse(source2) {
4565
+ const lexer = new Lexer(source2);
4566
+ const tokens = lexer.tokenize();
4567
+ const parser = new Parser(tokens);
4568
+ return parser.parse();
4569
+ }
4570
+ };
4571
+ const source = loader.load(templateName);
4572
+ const ast = loader.parse(source);
4573
+ const check = canFlatten(ast);
4574
+ if (!check.canFlatten) {
4575
+ throw new Error(`Cannot compile template with AOT: ${check.reason}`);
4576
+ }
4577
+ const flattenedAst = flattenTemplate(ast, { loader });
4578
+ return compileToString(flattenedAst, options);
4579
+ }
4580
+ function compileToCode(source, options = {}) {
4581
+ const lexer = new Lexer(source);
4582
+ const tokens = lexer.tokenize();
4583
+ const parser = new Parser(tokens);
4584
+ const ast = parser.parse();
4585
+ return compileToString(ast, options);
4586
+ }
2489
4587
  export {
4588
+ renderWithDebug,
4589
+ renderStringWithDebug,
2490
4590
  render,
4591
+ generateDebugPanel,
4592
+ flattenTemplate,
4593
+ debugMiddleware,
4594
+ createDebugRenderer,
4595
+ compileWithInheritanceToCode,
4596
+ compileWithInheritance,
4597
+ compileToCode,
4598
+ compile,
4599
+ canFlatten,
4600
+ builtinTests,
2491
4601
  builtinFilters,
2492
4602
  TokenType,
2493
4603
  Template,