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