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/README.md +246 -104
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2316 -0
- package/dist/compiler/flattener.d.ts +36 -0
- package/dist/compiler/flattener.d.ts.map +1 -0
- package/dist/compiler/index.d.ts +32 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/debug/collector.d.ts +73 -0
- package/dist/debug/collector.d.ts.map +1 -0
- package/dist/debug/index.d.ts +54 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/panel.d.ts +16 -0
- package/dist/debug/panel.d.ts.map +1 -0
- package/dist/filters/index.d.ts +20 -0
- package/dist/filters/index.d.ts.map +1 -1
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2438 -328
- package/dist/lexer/index.d.ts +2 -0
- package/dist/lexer/index.d.ts.map +1 -1
- 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 +12 -2
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.
|
|
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.
|
|
1340
|
+
return this._currentForloop;
|
|
1224
1341
|
}
|
|
1225
1342
|
for (let i = this.scopes.length - 1;i >= 0; i--) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1343
|
+
const scope = this.scopes[i];
|
|
1344
|
+
if (name in scope) {
|
|
1345
|
+
return scope[name];
|
|
1228
1346
|
}
|
|
1229
1347
|
}
|
|
1230
|
-
|
|
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.
|
|
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]
|
|
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.
|
|
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
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
1354
|
-
const html = paragraphs.map((p) => `<p>${p.replace(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1910
|
-
if (
|
|
1911
|
-
|
|
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
|
-
|
|
2308
|
+
this.collectBlocksSync(ast);
|
|
2309
|
+
return this.renderTemplateSync(ast, ctx);
|
|
1914
2310
|
}
|
|
1915
|
-
|
|
2311
|
+
templateNeedsAsync(ast) {
|
|
1916
2312
|
for (const node of ast.body) {
|
|
1917
|
-
if (node.type === "Extends")
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
-
|
|
2384
|
+
renderTemplateSync(ast, ctx) {
|
|
1927
2385
|
const parts = [];
|
|
1928
2386
|
for (const node of ast.body) {
|
|
1929
|
-
const result =
|
|
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
|
-
|
|
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.
|
|
2398
|
+
return this.stringify(this.eval(node.expression, ctx));
|
|
1941
2399
|
case "If":
|
|
1942
|
-
return this.
|
|
2400
|
+
return this.renderIfSync(node, ctx);
|
|
1943
2401
|
case "For":
|
|
1944
|
-
return this.
|
|
2402
|
+
return this.renderForSync(node, ctx);
|
|
1945
2403
|
case "Block":
|
|
1946
|
-
return this.
|
|
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
|
-
|
|
2406
|
+
ctx.set(node.target, this.eval(node.value, ctx));
|
|
2407
|
+
return "";
|
|
1953
2408
|
case "With":
|
|
1954
|
-
return this.
|
|
1955
|
-
case "Load":
|
|
1956
|
-
return null;
|
|
2409
|
+
return this.renderWithSync(node, ctx);
|
|
1957
2410
|
case "Url":
|
|
1958
|
-
return this.
|
|
2411
|
+
return this.renderUrlSync(node, ctx);
|
|
1959
2412
|
case "Static":
|
|
1960
|
-
return this.
|
|
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
|
-
|
|
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);
|
|
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(
|
|
1975
|
-
return this.
|
|
2426
|
+
if (this.isTruthy(this.eval(elif.test, ctx))) {
|
|
2427
|
+
return this.renderNodesSync(elif.body, ctx);
|
|
1976
2428
|
}
|
|
1977
2429
|
}
|
|
1978
|
-
|
|
1979
|
-
return this.renderNodes(node.else_, ctx);
|
|
1980
|
-
}
|
|
1981
|
-
return "";
|
|
2430
|
+
return node.else_.length > 0 ? this.renderNodesSync(node.else_, ctx) : "";
|
|
1982
2431
|
}
|
|
1983
|
-
|
|
1984
|
-
const iterable2 =
|
|
2432
|
+
renderForSync(node, ctx) {
|
|
2433
|
+
const iterable2 = this.eval(node.iter, ctx);
|
|
1985
2434
|
const items = this.toIterable(iterable2);
|
|
1986
|
-
|
|
1987
|
-
|
|
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
|
-
|
|
2442
|
+
ctx.pushForLoop(items, 0);
|
|
2443
|
+
for (let i = 0;i < len; i++) {
|
|
1992
2444
|
const item = items[i];
|
|
1993
|
-
if (
|
|
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
|
|
2003
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2021
|
-
return this.renderNodes(node.body, ctx);
|
|
2022
|
-
}
|
|
2473
|
+
super: () => this.renderNodesSync(node.body, ctx)
|
|
2023
2474
|
});
|
|
2024
|
-
const result =
|
|
2475
|
+
const result = this.renderNodesSync(blockToRender.body, ctx);
|
|
2025
2476
|
ctx.pop();
|
|
2026
2477
|
return result;
|
|
2027
2478
|
}
|
|
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) {
|
|
2479
|
+
renderWithSync(node, ctx) {
|
|
2052
2480
|
ctx.push();
|
|
2053
2481
|
for (const { target, value } of node.assignments) {
|
|
2054
|
-
ctx.set(target,
|
|
2482
|
+
ctx.set(target, this.eval(value, ctx));
|
|
2055
2483
|
}
|
|
2056
|
-
const result =
|
|
2484
|
+
const result = this.renderNodesSync(node.body, ctx);
|
|
2057
2485
|
ctx.pop();
|
|
2058
2486
|
return result;
|
|
2059
2487
|
}
|
|
2060
|
-
|
|
2061
|
-
const name =
|
|
2062
|
-
const args =
|
|
2063
|
-
const kwargs =
|
|
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
|
-
|
|
2072
|
-
const path =
|
|
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
|
-
|
|
2508
|
+
renderNodesSync(nodes2, ctx) {
|
|
2081
2509
|
const parts = [];
|
|
2082
2510
|
for (const node of nodes2) {
|
|
2083
|
-
const result =
|
|
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
|
-
|
|
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.
|
|
2524
|
+
return this.evalGetAttr(node, ctx);
|
|
2097
2525
|
case "GetItem":
|
|
2098
|
-
return this.
|
|
2526
|
+
return this.evalGetItem(node, ctx);
|
|
2099
2527
|
case "FilterExpr":
|
|
2100
|
-
return this.
|
|
2528
|
+
return this.evalFilter(node, ctx);
|
|
2101
2529
|
case "BinaryOp":
|
|
2102
|
-
return this.
|
|
2530
|
+
return this.evalBinaryOp(node, ctx);
|
|
2103
2531
|
case "UnaryOp":
|
|
2104
|
-
return this.
|
|
2532
|
+
return this.evalUnaryOp(node, ctx);
|
|
2105
2533
|
case "Compare":
|
|
2106
|
-
return this.
|
|
2534
|
+
return this.evalCompare(node, ctx);
|
|
2107
2535
|
case "Conditional":
|
|
2108
|
-
return this.
|
|
2536
|
+
return this.evalConditional(node, ctx);
|
|
2109
2537
|
case "Array":
|
|
2110
|
-
return
|
|
2538
|
+
return node.elements.map((el) => this.eval(el, ctx));
|
|
2111
2539
|
case "Object":
|
|
2112
|
-
return this.
|
|
2540
|
+
return this.evalObjectLiteral(node, ctx);
|
|
2113
2541
|
case "FunctionCall":
|
|
2114
|
-
return this.
|
|
2542
|
+
return this.evalFunctionCall(node, ctx);
|
|
2115
2543
|
case "TestExpr":
|
|
2116
|
-
return this.
|
|
2544
|
+
return this.evalTest(node, ctx);
|
|
2117
2545
|
default:
|
|
2118
2546
|
return;
|
|
2119
2547
|
}
|
|
2120
2548
|
}
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
2163
|
-
const obj =
|
|
2164
|
-
const index =
|
|
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
|
-
|
|
2170
|
-
const value =
|
|
2171
|
-
const args =
|
|
2172
|
-
const kwargs =
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
2209
|
-
const operand =
|
|
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
|
-
|
|
2222
|
-
let left =
|
|
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 =
|
|
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
|
-
|
|
2267
|
-
|
|
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
|
-
|
|
2666
|
+
evalObjectLiteral(node, ctx) {
|
|
2271
2667
|
const result = {};
|
|
2272
2668
|
for (const { key, value } of node.pairs) {
|
|
2273
|
-
|
|
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
|
-
|
|
2279
|
-
const callee =
|
|
2280
|
-
const args =
|
|
2281
|
-
const kwargs =
|
|
2282
|
-
|
|
2283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2698
|
+
evalObjectSync(obj, ctx) {
|
|
2288
2699
|
const result = {};
|
|
2289
2700
|
for (const [key, value] of Object.entries(obj)) {
|
|
2290
|
-
result[key] =
|
|
2701
|
+
result[key] = this.eval(value, ctx);
|
|
2291
2702
|
}
|
|
2292
2703
|
return result;
|
|
2293
2704
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
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
|
-
|
|
2437
|
-
await fs.promises.access(fullPath, fs.constants.R_OK);
|
|
4455
|
+
if (await Bun.file(fullPath).exists()) {
|
|
2438
4456
|
return fullPath;
|
|
2439
|
-
}
|
|
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
|
|
2451
|
-
|
|
2452
|
-
url = url.
|
|
2453
|
-
url = url.
|
|
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
|
-
|
|
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,
|