binja 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -660,6 +660,28 @@ class Parser {
660
660
  case "autoescape":
661
661
  case "verbatim":
662
662
  return this.parseSimpleBlock(start, tagName.value);
663
+ case "cycle":
664
+ return this.parseCycle(start);
665
+ case "firstof":
666
+ return this.parseFirstof(start);
667
+ case "ifchanged":
668
+ return this.parseIfchanged(start);
669
+ case "regroup":
670
+ return this.parseRegroup(start);
671
+ case "widthratio":
672
+ return this.parseWidthratio(start);
673
+ case "lorem":
674
+ return this.parseLorem(start);
675
+ case "csrf_token":
676
+ return this.parseCsrfToken(start);
677
+ case "debug":
678
+ return this.parseDebug(start);
679
+ case "templatetag":
680
+ return this.parseTemplatetag(start);
681
+ case "ifequal":
682
+ return this.parseIfequal(start, false);
683
+ case "ifnotequal":
684
+ return this.parseIfequal(start, true);
663
685
  default:
664
686
  this.skipToBlockEnd();
665
687
  return null;
@@ -963,7 +985,7 @@ class Parser {
963
985
  column: start.column
964
986
  };
965
987
  }
966
- parseComment(start) {
988
+ parseComment(_start) {
967
989
  this.expect("BLOCK_END" /* BLOCK_END */);
968
990
  while (!this.isAtEnd()) {
969
991
  if (this.checkBlockTag("endcomment"))
@@ -973,7 +995,7 @@ class Parser {
973
995
  this.expectBlockTag("endcomment");
974
996
  return null;
975
997
  }
976
- parseSimpleBlock(start, tagName) {
998
+ parseSimpleBlock(_start, tagName) {
977
999
  this.skipToBlockEnd();
978
1000
  const endTag = `end${tagName}`;
979
1001
  while (!this.isAtEnd()) {
@@ -988,6 +1010,236 @@ class Parser {
988
1010
  }
989
1011
  return null;
990
1012
  }
1013
+ parseCycle(start) {
1014
+ const values = [];
1015
+ let asVar = null;
1016
+ let silent = false;
1017
+ while (!this.check("BLOCK_END" /* BLOCK_END */)) {
1018
+ if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
1019
+ this.advance();
1020
+ asVar = this.expect("NAME" /* NAME */).value;
1021
+ if (this.check("NAME" /* NAME */) && this.peek().value === "silent") {
1022
+ this.advance();
1023
+ silent = true;
1024
+ }
1025
+ break;
1026
+ }
1027
+ values.push(this.parseExpression());
1028
+ }
1029
+ this.expect("BLOCK_END" /* BLOCK_END */);
1030
+ return {
1031
+ type: "Cycle",
1032
+ values,
1033
+ asVar,
1034
+ silent,
1035
+ line: start.line,
1036
+ column: start.column
1037
+ };
1038
+ }
1039
+ parseFirstof(start) {
1040
+ const values = [];
1041
+ let fallback = null;
1042
+ let asVar = null;
1043
+ while (!this.check("BLOCK_END" /* BLOCK_END */)) {
1044
+ if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
1045
+ this.advance();
1046
+ asVar = this.expect("NAME" /* NAME */).value;
1047
+ break;
1048
+ }
1049
+ values.push(this.parseExpression());
1050
+ }
1051
+ if (values.length > 0) {
1052
+ const last = values[values.length - 1];
1053
+ if (last.type === "Literal" && typeof last.value === "string") {
1054
+ fallback = values.pop();
1055
+ }
1056
+ }
1057
+ this.expect("BLOCK_END" /* BLOCK_END */);
1058
+ return {
1059
+ type: "Firstof",
1060
+ values,
1061
+ fallback,
1062
+ asVar,
1063
+ line: start.line,
1064
+ column: start.column
1065
+ };
1066
+ }
1067
+ parseIfchanged(start) {
1068
+ const values = [];
1069
+ while (!this.check("BLOCK_END" /* BLOCK_END */)) {
1070
+ values.push(this.parseExpression());
1071
+ }
1072
+ this.expect("BLOCK_END" /* BLOCK_END */);
1073
+ const body = [];
1074
+ let else_ = [];
1075
+ while (!this.isAtEnd()) {
1076
+ if (this.checkBlockTag("else") || this.checkBlockTag("endifchanged"))
1077
+ break;
1078
+ const node = this.parseStatement();
1079
+ if (node)
1080
+ body.push(node);
1081
+ }
1082
+ if (this.checkBlockTag("else")) {
1083
+ this.advance();
1084
+ this.advance();
1085
+ this.expect("BLOCK_END" /* BLOCK_END */);
1086
+ while (!this.isAtEnd()) {
1087
+ if (this.checkBlockTag("endifchanged"))
1088
+ break;
1089
+ const node = this.parseStatement();
1090
+ if (node)
1091
+ else_.push(node);
1092
+ }
1093
+ }
1094
+ this.expectBlockTag("endifchanged");
1095
+ return {
1096
+ type: "Ifchanged",
1097
+ values,
1098
+ body,
1099
+ else_,
1100
+ line: start.line,
1101
+ column: start.column
1102
+ };
1103
+ }
1104
+ parseRegroup(start) {
1105
+ const target = this.parseExpression();
1106
+ this.expectName("by");
1107
+ const key = this.expect("NAME" /* NAME */).value;
1108
+ this.expectName("as");
1109
+ const asVar = this.expect("NAME" /* NAME */).value;
1110
+ this.expect("BLOCK_END" /* BLOCK_END */);
1111
+ return {
1112
+ type: "Regroup",
1113
+ target,
1114
+ key,
1115
+ asVar,
1116
+ line: start.line,
1117
+ column: start.column
1118
+ };
1119
+ }
1120
+ parseWidthratio(start) {
1121
+ const value = this.parseExpression();
1122
+ const maxValue = this.parseExpression();
1123
+ const maxWidth = this.parseExpression();
1124
+ let asVar = null;
1125
+ if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
1126
+ this.advance();
1127
+ asVar = this.expect("NAME" /* NAME */).value;
1128
+ }
1129
+ this.expect("BLOCK_END" /* BLOCK_END */);
1130
+ return {
1131
+ type: "Widthratio",
1132
+ value,
1133
+ maxValue,
1134
+ maxWidth,
1135
+ asVar,
1136
+ line: start.line,
1137
+ column: start.column
1138
+ };
1139
+ }
1140
+ parseLorem(start) {
1141
+ let count = null;
1142
+ let method = "p";
1143
+ let random = false;
1144
+ if (this.check("NUMBER" /* NUMBER */)) {
1145
+ count = {
1146
+ type: "Literal",
1147
+ value: parseInt(this.advance().value, 10),
1148
+ line: start.line,
1149
+ column: start.column
1150
+ };
1151
+ }
1152
+ if (this.check("NAME" /* NAME */)) {
1153
+ const m = this.peek().value.toLowerCase();
1154
+ if (m === "w" || m === "p" || m === "b") {
1155
+ method = m;
1156
+ this.advance();
1157
+ }
1158
+ }
1159
+ if (this.check("NAME" /* NAME */) && this.peek().value === "random") {
1160
+ random = true;
1161
+ this.advance();
1162
+ }
1163
+ this.expect("BLOCK_END" /* BLOCK_END */);
1164
+ return {
1165
+ type: "Lorem",
1166
+ count,
1167
+ method,
1168
+ random,
1169
+ line: start.line,
1170
+ column: start.column
1171
+ };
1172
+ }
1173
+ parseCsrfToken(start) {
1174
+ this.expect("BLOCK_END" /* BLOCK_END */);
1175
+ return {
1176
+ type: "CsrfToken",
1177
+ line: start.line,
1178
+ column: start.column
1179
+ };
1180
+ }
1181
+ parseDebug(start) {
1182
+ this.expect("BLOCK_END" /* BLOCK_END */);
1183
+ return {
1184
+ type: "Debug",
1185
+ line: start.line,
1186
+ column: start.column
1187
+ };
1188
+ }
1189
+ parseTemplatetag(start) {
1190
+ const tagType = this.expect("NAME" /* NAME */).value;
1191
+ this.expect("BLOCK_END" /* BLOCK_END */);
1192
+ return {
1193
+ type: "Templatetag",
1194
+ tagType,
1195
+ line: start.line,
1196
+ column: start.column
1197
+ };
1198
+ }
1199
+ parseIfequal(start, negated) {
1200
+ const left = this.parseExpression();
1201
+ const right = this.parseExpression();
1202
+ this.expect("BLOCK_END" /* BLOCK_END */);
1203
+ const test = {
1204
+ type: "Compare",
1205
+ left,
1206
+ ops: [{ operator: negated ? "!=" : "==", right }],
1207
+ line: start.line,
1208
+ column: start.column
1209
+ };
1210
+ const body = [];
1211
+ let else_ = [];
1212
+ const endTag = negated ? "endifnotequal" : "endifequal";
1213
+ while (!this.isAtEnd()) {
1214
+ if (this.checkBlockTag("else") || this.checkBlockTag(endTag))
1215
+ break;
1216
+ const node = this.parseStatement();
1217
+ if (node)
1218
+ body.push(node);
1219
+ }
1220
+ if (this.checkBlockTag("else")) {
1221
+ this.advance();
1222
+ this.advance();
1223
+ this.expect("BLOCK_END" /* BLOCK_END */);
1224
+ while (!this.isAtEnd()) {
1225
+ if (this.checkBlockTag(endTag))
1226
+ break;
1227
+ const node = this.parseStatement();
1228
+ if (node)
1229
+ else_.push(node);
1230
+ }
1231
+ }
1232
+ this.expectBlockTag(endTag);
1233
+ return {
1234
+ type: "If",
1235
+ test,
1236
+ body,
1237
+ elifs: [],
1238
+ else_,
1239
+ line: start.line,
1240
+ column: start.column
1241
+ };
1242
+ }
991
1243
  parseExpression() {
992
1244
  return this.parseConditional();
993
1245
  }
@@ -1423,6 +1675,7 @@ class Parser {
1423
1675
  // src/runtime/context.ts
1424
1676
  function createForLoop(items, index, depth, lastCycleValue, parentloop) {
1425
1677
  const length = items.length;
1678
+ const revCount = length - index;
1426
1679
  const forloop = {
1427
1680
  _items: items,
1428
1681
  _idx: index,
@@ -1435,17 +1688,19 @@ function createForLoop(items, index, depth, lastCycleValue, parentloop) {
1435
1688
  index0: index,
1436
1689
  depth,
1437
1690
  depth0: depth - 1,
1438
- revcounter: length - index,
1439
- revcounter0: length - index - 1,
1440
- revindex: length - index,
1441
- revindex0: length - index - 1,
1691
+ revcounter: revCount,
1692
+ revcounter0: revCount - 1,
1693
+ revindex: revCount,
1694
+ revindex0: revCount - 1,
1442
1695
  get previtem() {
1443
1696
  return this._idx > 0 ? this._items[this._idx - 1] : undefined;
1444
1697
  },
1445
1698
  get nextitem() {
1446
1699
  return this._idx < this._items.length - 1 ? this._items[this._idx + 1] : undefined;
1447
1700
  },
1448
- cycle: (...args) => args[index % args.length],
1701
+ cycle(...args) {
1702
+ return args[this._idx % args.length];
1703
+ },
1449
1704
  changed: (value) => {
1450
1705
  const changed = value !== lastCycleValue.value;
1451
1706
  lastCycleValue.value = value;
@@ -1523,18 +1778,19 @@ class Context {
1523
1778
  return;
1524
1779
  const length = items.length;
1525
1780
  forloop._idx = index;
1526
- forloop._items = items;
1781
+ if (forloop._items !== items)
1782
+ forloop._items = items;
1527
1783
  forloop.counter = index + 1;
1528
1784
  forloop.counter0 = index;
1529
- forloop.first = index === 0;
1530
- forloop.last = index === length - 1;
1531
1785
  forloop.index = index + 1;
1532
1786
  forloop.index0 = index;
1533
- forloop.revcounter = length - index;
1534
- forloop.revcounter0 = length - index - 1;
1535
- forloop.revindex = length - index;
1536
- forloop.revindex0 = length - index - 1;
1537
- forloop.cycle = (...args) => args[index % args.length];
1787
+ forloop.first = index === 0;
1788
+ forloop.last = index === length - 1;
1789
+ const revCount = length - index;
1790
+ forloop.revcounter = revCount;
1791
+ forloop.revcounter0 = revCount - 1;
1792
+ forloop.revindex = revCount;
1793
+ forloop.revindex0 = revCount - 1;
1538
1794
  }
1539
1795
  toObject() {
1540
1796
  const result = {};
@@ -2227,6 +2483,172 @@ ${indent2}`;
2227
2483
  safeString.__safe__ = true;
2228
2484
  return safeString;
2229
2485
  };
2486
+ var addslashes = (value) => {
2487
+ const result = String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"");
2488
+ const safeString = new String(result);
2489
+ safeString.__safe__ = true;
2490
+ return safeString;
2491
+ };
2492
+ var get_digit = (value, digit) => {
2493
+ const num = parseInt(String(value), 10);
2494
+ if (isNaN(num))
2495
+ return value;
2496
+ const str = String(Math.abs(num));
2497
+ const pos = Number(digit) || 1;
2498
+ if (pos < 1 || pos > str.length)
2499
+ return value;
2500
+ return parseInt(str[str.length - pos], 10);
2501
+ };
2502
+ var iriencode = (value) => {
2503
+ return String(value).replace(/ /g, "%20").replace(/"/g, "%22").replace(/</g, "%3C").replace(/>/g, "%3E").replace(/\\/g, "%5C").replace(/\^/g, "%5E").replace(/`/g, "%60").replace(/\{/g, "%7B").replace(/\|/g, "%7C").replace(/\}/g, "%7D");
2504
+ };
2505
+ var json_script = (value, elementId) => {
2506
+ const jsonStr = JSON.stringify(value).replace(/</g, "\\u003C").replace(/>/g, "\\u003E").replace(/&/g, "\\u0026");
2507
+ const id = elementId ? ` id="${String(elementId)}"` : "";
2508
+ const html = `<script${id} type="application/json">${jsonStr}</script>`;
2509
+ const safeString = new String(html);
2510
+ safeString.__safe__ = true;
2511
+ return safeString;
2512
+ };
2513
+ var safeseq = (value) => {
2514
+ if (!Array.isArray(value))
2515
+ return value;
2516
+ return value.map((item) => {
2517
+ const safeString = new String(item);
2518
+ safeString.__safe__ = true;
2519
+ return safeString;
2520
+ });
2521
+ };
2522
+ var stringformat = (value, formatStr) => {
2523
+ const fmt = String(formatStr);
2524
+ const val = value;
2525
+ if (fmt === "s")
2526
+ return String(val);
2527
+ if (fmt === "d" || fmt === "i")
2528
+ return String(parseInt(String(val), 10) || 0);
2529
+ if (fmt === "f")
2530
+ return String(parseFloat(String(val)) || 0);
2531
+ if (fmt === "x")
2532
+ return (parseInt(String(val), 10) || 0).toString(16);
2533
+ if (fmt === "X")
2534
+ return (parseInt(String(val), 10) || 0).toString(16).toUpperCase();
2535
+ if (fmt === "o")
2536
+ return (parseInt(String(val), 10) || 0).toString(8);
2537
+ if (fmt === "b")
2538
+ return (parseInt(String(val), 10) || 0).toString(2);
2539
+ if (fmt === "e")
2540
+ return (parseFloat(String(val)) || 0).toExponential();
2541
+ const precisionMatch = fmt.match(/^\.?(\d+)f$/);
2542
+ if (precisionMatch) {
2543
+ const precision = parseInt(precisionMatch[1], 10);
2544
+ return (parseFloat(String(val)) || 0).toFixed(precision);
2545
+ }
2546
+ const widthMatch = fmt.match(/^(\d+)([sd])$/);
2547
+ if (widthMatch) {
2548
+ const width = parseInt(widthMatch[1], 10);
2549
+ const type = widthMatch[2];
2550
+ const strVal = type === "d" ? String(parseInt(String(val), 10) || 0) : String(val);
2551
+ return strVal.padStart(width, type === "d" ? "0" : " ");
2552
+ }
2553
+ return String(val);
2554
+ };
2555
+ var truncatechars_html = (value, length2 = 30) => {
2556
+ const str = String(value);
2557
+ const maxLen = Number(length2);
2558
+ let result = "";
2559
+ let textLen = 0;
2560
+ let inTag = false;
2561
+ const openTags = [];
2562
+ for (let i = 0;i < str.length && textLen < maxLen; i++) {
2563
+ const char = str[i];
2564
+ if (char === "<") {
2565
+ inTag = true;
2566
+ const closeMatch = str.slice(i).match(/^<\/(\w+)/);
2567
+ const openMatch = str.slice(i).match(/^<(\w+)/);
2568
+ if (closeMatch) {
2569
+ const tagName = closeMatch[1].toLowerCase();
2570
+ const lastOpen = openTags.lastIndexOf(tagName);
2571
+ if (lastOpen !== -1)
2572
+ openTags.splice(lastOpen, 1);
2573
+ } else if (openMatch && !str.slice(i).match(/^<\w+[^>]*\/>/)) {
2574
+ openTags.push(openMatch[1].toLowerCase());
2575
+ }
2576
+ }
2577
+ result += char;
2578
+ if (char === ">") {
2579
+ inTag = false;
2580
+ } else if (!inTag) {
2581
+ textLen++;
2582
+ }
2583
+ }
2584
+ if (textLen >= maxLen && str.length > result.length) {
2585
+ result += "...";
2586
+ }
2587
+ for (let i = openTags.length - 1;i >= 0; i--) {
2588
+ result += `</${openTags[i]}>`;
2589
+ }
2590
+ const safeString = new String(result);
2591
+ safeString.__safe__ = true;
2592
+ return safeString;
2593
+ };
2594
+ var truncatewords_html = (value, count = 15) => {
2595
+ const str = String(value);
2596
+ const maxWords = Number(count);
2597
+ let result = "";
2598
+ let wordCount = 0;
2599
+ let inTag = false;
2600
+ let inWord = false;
2601
+ const openTags = [];
2602
+ for (let i = 0;i < str.length && wordCount < maxWords; i++) {
2603
+ const char = str[i];
2604
+ if (char === "<") {
2605
+ inTag = true;
2606
+ const closeMatch = str.slice(i).match(/^<\/(\w+)/);
2607
+ const openMatch = str.slice(i).match(/^<(\w+)/);
2608
+ if (closeMatch) {
2609
+ const tagName = closeMatch[1].toLowerCase();
2610
+ const lastOpen = openTags.lastIndexOf(tagName);
2611
+ if (lastOpen !== -1)
2612
+ openTags.splice(lastOpen, 1);
2613
+ } else if (openMatch && !str.slice(i).match(/^<\w+[^>]*\/>/)) {
2614
+ openTags.push(openMatch[1].toLowerCase());
2615
+ }
2616
+ }
2617
+ result += char;
2618
+ if (char === ">") {
2619
+ inTag = false;
2620
+ } else if (!inTag) {
2621
+ const isSpace = /\s/.test(char);
2622
+ if (!isSpace && !inWord) {
2623
+ inWord = true;
2624
+ } else if (isSpace && inWord) {
2625
+ inWord = false;
2626
+ wordCount++;
2627
+ }
2628
+ }
2629
+ }
2630
+ if (inWord)
2631
+ wordCount++;
2632
+ if (wordCount >= maxWords) {
2633
+ result = result.trimEnd() + "...";
2634
+ }
2635
+ for (let i = openTags.length - 1;i >= 0; i--) {
2636
+ result += `</${openTags[i]}>`;
2637
+ }
2638
+ const safeString = new String(result);
2639
+ safeString.__safe__ = true;
2640
+ return safeString;
2641
+ };
2642
+ var urlizetrunc = (value, limit = 15) => {
2643
+ const maxLen = Number(limit);
2644
+ const html = String(value).replace(URLIZE_REGEX, (url) => {
2645
+ const displayUrl = url.length > maxLen ? url.slice(0, maxLen) + "..." : url;
2646
+ return `<a href="${url}">${displayUrl}</a>`;
2647
+ });
2648
+ const safeString = new String(html);
2649
+ safeString.__safe__ = true;
2650
+ return safeString;
2651
+ };
2230
2652
  var builtinFilters = {
2231
2653
  upper,
2232
2654
  lower,
@@ -2305,7 +2727,16 @@ var builtinFilters = {
2305
2727
  forceescape,
2306
2728
  phone2numeric,
2307
2729
  linenumbers,
2308
- unordered_list
2730
+ unordered_list,
2731
+ addslashes,
2732
+ get_digit,
2733
+ iriencode,
2734
+ json_script,
2735
+ safeseq,
2736
+ stringformat,
2737
+ truncatechars_html,
2738
+ truncatewords_html,
2739
+ urlizetrunc
2309
2740
  };
2310
2741
 
2311
2742
  // src/tests/index.ts
@@ -2594,11 +3025,13 @@ class Runtime {
2594
3025
  return parts.join("");
2595
3026
  }
2596
3027
  renderNodeSync(node, ctx) {
3028
+ if (node.type === "Text") {
3029
+ return node.value;
3030
+ }
3031
+ if (node.type === "Output") {
3032
+ return this.stringify(this.eval(node.expression, ctx));
3033
+ }
2597
3034
  switch (node.type) {
2598
- case "Text":
2599
- return node.value;
2600
- case "Output":
2601
- return this.stringify(this.eval(node.expression, ctx));
2602
3035
  case "If":
2603
3036
  return this.renderIfSync(node, ctx);
2604
3037
  case "For":
@@ -2619,6 +3052,24 @@ class Runtime {
2619
3052
  case "Load":
2620
3053
  case "Extends":
2621
3054
  return null;
3055
+ case "Cycle":
3056
+ return this.renderCycleSync(node, ctx);
3057
+ case "Firstof":
3058
+ return this.renderFirstofSync(node, ctx);
3059
+ case "Ifchanged":
3060
+ return this.renderIfchangedSync(node, ctx);
3061
+ case "Regroup":
3062
+ return this.renderRegroupSync(node, ctx);
3063
+ case "Widthratio":
3064
+ return this.renderWidthratioSync(node, ctx);
3065
+ case "Lorem":
3066
+ return this.renderLoremSync(node, ctx);
3067
+ case "CsrfToken":
3068
+ return this.renderCsrfTokenSync();
3069
+ case "Debug":
3070
+ return this.renderDebugSync(ctx);
3071
+ case "Templatetag":
3072
+ return this.renderTemplatetagSync(node);
2622
3073
  default:
2623
3074
  return null;
2624
3075
  }
@@ -2641,41 +3092,76 @@ class Runtime {
2641
3092
  if (len === 0) {
2642
3093
  return this.renderNodesSync(node.else_, ctx);
2643
3094
  }
2644
- const parts = new Array(len);
2645
3095
  ctx.push();
2646
3096
  ctx.pushForLoop(items, 0);
3097
+ let result;
2647
3098
  if (Array.isArray(node.target)) {
2648
3099
  const targets = node.target;
2649
3100
  const targetsLen = targets.length;
2650
- for (let i = 0;i < len; i++) {
2651
- const item = items[i];
2652
- if (i > 0)
2653
- ctx.updateForLoop(i, items);
2654
- let values;
2655
- if (Array.isArray(item)) {
2656
- values = item;
2657
- } else if (item && typeof item === "object" && (("0" in item) || ("key" in item))) {
2658
- values = [item[0] ?? item.key, item[1] ?? item.value];
2659
- } else {
2660
- values = [item, item];
3101
+ if (len < 50) {
3102
+ result = "";
3103
+ for (let i = 0;i < len; i++) {
3104
+ const item = items[i];
3105
+ if (i > 0)
3106
+ ctx.updateForLoop(i, items);
3107
+ let values;
3108
+ if (Array.isArray(item)) {
3109
+ values = item;
3110
+ } else if (item && typeof item === "object" && (("0" in item) || ("key" in item))) {
3111
+ values = [item[0] ?? item.key, item[1] ?? item.value];
3112
+ } else {
3113
+ values = [item, item];
3114
+ }
3115
+ for (let j = 0;j < targetsLen; j++) {
3116
+ ctx.set(targets[j], values[j]);
3117
+ }
3118
+ result += this.renderNodesSync(node.body, ctx);
2661
3119
  }
2662
- for (let j = 0;j < targetsLen; j++) {
2663
- ctx.set(targets[j], values[j]);
3120
+ } else {
3121
+ const parts = new Array(len);
3122
+ for (let i = 0;i < len; i++) {
3123
+ const item = items[i];
3124
+ if (i > 0)
3125
+ ctx.updateForLoop(i, items);
3126
+ let values;
3127
+ if (Array.isArray(item)) {
3128
+ values = item;
3129
+ } else if (item && typeof item === "object" && (("0" in item) || ("key" in item))) {
3130
+ values = [item[0] ?? item.key, item[1] ?? item.value];
3131
+ } else {
3132
+ values = [item, item];
3133
+ }
3134
+ for (let j = 0;j < targetsLen; j++) {
3135
+ ctx.set(targets[j], values[j]);
3136
+ }
3137
+ parts[i] = this.renderNodesSync(node.body, ctx);
2664
3138
  }
2665
- parts[i] = this.renderNodesSync(node.body, ctx);
3139
+ result = parts.join("");
2666
3140
  }
2667
3141
  } else {
2668
3142
  const target = node.target;
2669
- for (let i = 0;i < len; i++) {
2670
- if (i > 0)
2671
- ctx.updateForLoop(i, items);
2672
- ctx.set(target, items[i]);
2673
- parts[i] = this.renderNodesSync(node.body, ctx);
3143
+ if (len < 50) {
3144
+ result = "";
3145
+ for (let i = 0;i < len; i++) {
3146
+ if (i > 0)
3147
+ ctx.updateForLoop(i, items);
3148
+ ctx.set(target, items[i]);
3149
+ result += this.renderNodesSync(node.body, ctx);
3150
+ }
3151
+ } else {
3152
+ const parts = new Array(len);
3153
+ for (let i = 0;i < len; i++) {
3154
+ if (i > 0)
3155
+ ctx.updateForLoop(i, items);
3156
+ ctx.set(target, items[i]);
3157
+ parts[i] = this.renderNodesSync(node.body, ctx);
3158
+ }
3159
+ result = parts.join("");
2674
3160
  }
2675
3161
  }
2676
3162
  ctx.popForLoop();
2677
3163
  ctx.pop();
2678
- return parts.join("");
3164
+ return result;
2679
3165
  }
2680
3166
  renderBlockSync(node, ctx) {
2681
3167
  const blockToRender = this.blocks.get(node.name) || node;
@@ -2729,6 +3215,217 @@ class Runtime {
2729
3215
  }
2730
3216
  return result;
2731
3217
  }
3218
+ cycleState = new Map;
3219
+ renderCycleSync(node, ctx) {
3220
+ const key = node.values.map((v) => JSON.stringify(v)).join(",");
3221
+ const currentIndex = this.cycleState.get(key) ?? 0;
3222
+ const values = node.values.map((v) => this.eval(v, ctx));
3223
+ const value = values[currentIndex % values.length];
3224
+ this.cycleState.set(key, currentIndex + 1);
3225
+ if (node.asVar) {
3226
+ ctx.set(node.asVar, value);
3227
+ return node.silent ? "" : this.stringify(value);
3228
+ }
3229
+ return this.stringify(value);
3230
+ }
3231
+ renderFirstofSync(node, ctx) {
3232
+ for (const expr of node.values) {
3233
+ const value = this.eval(expr, ctx);
3234
+ if (this.isTruthy(value)) {
3235
+ if (node.asVar) {
3236
+ ctx.set(node.asVar, value);
3237
+ return "";
3238
+ }
3239
+ return this.stringify(value);
3240
+ }
3241
+ }
3242
+ const fallback = node.fallback ? this.eval(node.fallback, ctx) : "";
3243
+ if (node.asVar) {
3244
+ ctx.set(node.asVar, fallback);
3245
+ return "";
3246
+ }
3247
+ return this.stringify(fallback);
3248
+ }
3249
+ ifchangedState = new Map;
3250
+ renderIfchangedSync(node, ctx) {
3251
+ const key = `ifchanged_${node.line}_${node.column}`;
3252
+ let currentValue;
3253
+ if (node.values.length > 0) {
3254
+ currentValue = node.values.map((v) => this.eval(v, ctx));
3255
+ } else {
3256
+ currentValue = this.renderNodesSync(node.body, ctx);
3257
+ }
3258
+ const lastValue = this.ifchangedState.get(key);
3259
+ const changed = JSON.stringify(currentValue) !== JSON.stringify(lastValue);
3260
+ this.ifchangedState.set(key, currentValue);
3261
+ if (changed) {
3262
+ if (node.values.length > 0) {
3263
+ return this.renderNodesSync(node.body, ctx);
3264
+ }
3265
+ return currentValue;
3266
+ } else {
3267
+ return this.renderNodesSync(node.else_, ctx);
3268
+ }
3269
+ }
3270
+ renderRegroupSync(node, ctx) {
3271
+ const list2 = this.toIterable(this.eval(node.target, ctx));
3272
+ const groups = [];
3273
+ let currentGrouper = Symbol("initial");
3274
+ let currentList = [];
3275
+ for (const item of list2) {
3276
+ const grouper = item && typeof item === "object" ? item[node.key] : undefined;
3277
+ if (grouper !== currentGrouper) {
3278
+ if (currentList.length > 0) {
3279
+ groups.push({ grouper: currentGrouper, list: currentList });
3280
+ }
3281
+ currentGrouper = grouper;
3282
+ currentList = [item];
3283
+ } else {
3284
+ currentList.push(item);
3285
+ }
3286
+ }
3287
+ if (currentList.length > 0) {
3288
+ groups.push({ grouper: currentGrouper, list: currentList });
3289
+ }
3290
+ ctx.set(node.asVar, groups);
3291
+ return "";
3292
+ }
3293
+ renderWidthratioSync(node, ctx) {
3294
+ const value = Number(this.eval(node.value, ctx));
3295
+ const maxValue = Number(this.eval(node.maxValue, ctx));
3296
+ const maxWidth = Number(this.eval(node.maxWidth, ctx));
3297
+ const ratio = maxValue === 0 ? 0 : Math.round(value / maxValue * maxWidth);
3298
+ if (node.asVar) {
3299
+ ctx.set(node.asVar, ratio);
3300
+ return "";
3301
+ }
3302
+ return String(ratio);
3303
+ }
3304
+ renderLoremSync(node, ctx) {
3305
+ const count = node.count ? Number(this.eval(node.count, ctx)) : 1;
3306
+ const method = node.method;
3307
+ const words = [
3308
+ "lorem",
3309
+ "ipsum",
3310
+ "dolor",
3311
+ "sit",
3312
+ "amet",
3313
+ "consectetur",
3314
+ "adipiscing",
3315
+ "elit",
3316
+ "sed",
3317
+ "do",
3318
+ "eiusmod",
3319
+ "tempor",
3320
+ "incididunt",
3321
+ "ut",
3322
+ "labore",
3323
+ "et",
3324
+ "dolore",
3325
+ "magna",
3326
+ "aliqua",
3327
+ "enim",
3328
+ "ad",
3329
+ "minim",
3330
+ "veniam",
3331
+ "quis",
3332
+ "nostrud",
3333
+ "exercitation",
3334
+ "ullamco",
3335
+ "laboris",
3336
+ "nisi",
3337
+ "aliquip",
3338
+ "ex",
3339
+ "ea",
3340
+ "commodo",
3341
+ "consequat",
3342
+ "duis",
3343
+ "aute",
3344
+ "irure",
3345
+ "in",
3346
+ "reprehenderit",
3347
+ "voluptate",
3348
+ "velit",
3349
+ "esse",
3350
+ "cillum",
3351
+ "fugiat",
3352
+ "nulla",
3353
+ "pariatur",
3354
+ "excepteur",
3355
+ "sint",
3356
+ "occaecat",
3357
+ "cupidatat",
3358
+ "non",
3359
+ "proident",
3360
+ "sunt",
3361
+ "culpa",
3362
+ "qui",
3363
+ "officia",
3364
+ "deserunt",
3365
+ "mollit",
3366
+ "anim",
3367
+ "id",
3368
+ "est",
3369
+ "laborum"
3370
+ ];
3371
+ const getWord = (index) => {
3372
+ if (node.random) {
3373
+ return words[Math.floor(Math.random() * words.length)];
3374
+ }
3375
+ return words[index % words.length];
3376
+ };
3377
+ if (method === "w") {
3378
+ const result = [];
3379
+ for (let i = 0;i < count; i++) {
3380
+ result.push(getWord(i));
3381
+ }
3382
+ return result.join(" ");
3383
+ } else if (method === "p" || method === "b") {
3384
+ const paragraphs = [];
3385
+ for (let p = 0;p < count; p++) {
3386
+ const sentenceCount = 3 + p % 3;
3387
+ const sentences = [];
3388
+ for (let s = 0;s < sentenceCount; s++) {
3389
+ const wordCount = 8 + s % 7;
3390
+ const sentenceWords = [];
3391
+ for (let w = 0;w < wordCount; w++) {
3392
+ sentenceWords.push(getWord(p * 100 + s * 20 + w));
3393
+ }
3394
+ sentenceWords[0] = sentenceWords[0].charAt(0).toUpperCase() + sentenceWords[0].slice(1);
3395
+ sentences.push(sentenceWords.join(" ") + ".");
3396
+ }
3397
+ paragraphs.push(sentences.join(" "));
3398
+ }
3399
+ if (method === "b") {
3400
+ return paragraphs.join(`
3401
+
3402
+ `);
3403
+ }
3404
+ return paragraphs.map((p) => `<p>${p}</p>`).join(`
3405
+ `);
3406
+ }
3407
+ return "";
3408
+ }
3409
+ renderCsrfTokenSync() {
3410
+ return '<input type="hidden" name="csrfmiddlewaretoken" value="CSRF_TOKEN_PLACEHOLDER">';
3411
+ }
3412
+ renderDebugSync(ctx) {
3413
+ const data = ctx.toObject?.() || {};
3414
+ return `<pre>${JSON.stringify(data, null, 2)}</pre>`;
3415
+ }
3416
+ renderTemplatetagSync(node) {
3417
+ const tagMap = {
3418
+ openblock: "{%",
3419
+ closeblock: "%}",
3420
+ openvariable: "{{",
3421
+ closevariable: "}}",
3422
+ openbrace: "{",
3423
+ closebrace: "}",
3424
+ opencomment: "{#",
3425
+ closecomment: "#}"
3426
+ };
3427
+ return tagMap[node.tagType] || "";
3428
+ }
2732
3429
  getDateInTimezone(d, tz) {
2733
3430
  if (!tz) {
2734
3431
  return {
@@ -2837,22 +3534,36 @@ class Runtime {
2837
3534
  return result;
2838
3535
  }
2839
3536
  renderNodesSync(nodes2, ctx) {
3537
+ const len = nodes2.length;
3538
+ if (len === 0)
3539
+ return "";
3540
+ if (len === 1) {
3541
+ return this.renderNodeSync(nodes2[0], ctx) ?? "";
3542
+ }
3543
+ if (len === 2) {
3544
+ const a = this.renderNodeSync(nodes2[0], ctx) ?? "";
3545
+ const b = this.renderNodeSync(nodes2[1], ctx) ?? "";
3546
+ return a + b;
3547
+ }
2840
3548
  const parts = [];
2841
- for (const node of nodes2) {
2842
- const result = this.renderNodeSync(node, ctx);
3549
+ for (let i = 0;i < len; i++) {
3550
+ const result = this.renderNodeSync(nodes2[i], ctx);
2843
3551
  if (result !== null)
2844
3552
  parts.push(result);
2845
3553
  }
2846
3554
  return parts.join("");
2847
3555
  }
2848
3556
  eval(node, ctx) {
3557
+ if (node.type === "Literal") {
3558
+ return node.value;
3559
+ }
3560
+ if (node.type === "Name") {
3561
+ return ctx.get(node.name);
3562
+ }
3563
+ if (node.type === "GetAttr") {
3564
+ return this.evalGetAttr(node, ctx);
3565
+ }
2849
3566
  switch (node.type) {
2850
- case "Literal":
2851
- return node.value;
2852
- case "Name":
2853
- return ctx.get(node.name);
2854
- case "GetAttr":
2855
- return this.evalGetAttr(node, ctx);
2856
3567
  case "GetItem":
2857
3568
  return this.evalGetItem(node, ctx);
2858
3569
  case "FilterExpr":
@@ -2888,13 +3599,13 @@ class Runtime {
2888
3599
  if (obj == null)
2889
3600
  return;
2890
3601
  const attr2 = node.attribute;
2891
- if (Array.isArray(obj)) {
2892
- const numIndex = parseInt(attr2, 10);
2893
- if (!isNaN(numIndex))
2894
- return obj[numIndex];
2895
- }
2896
3602
  const value = obj[attr2];
2897
- if (value === undefined && !(attr2 in Object(obj))) {
3603
+ if (value === undefined) {
3604
+ if (Array.isArray(obj)) {
3605
+ const numIndex = parseInt(attr2, 10);
3606
+ if (!isNaN(numIndex))
3607
+ return obj[numIndex];
3608
+ }
2898
3609
  return;
2899
3610
  }
2900
3611
  if (typeof value === "function") {
@@ -2923,22 +3634,32 @@ class Runtime {
2923
3634
  }
2924
3635
  evalBinaryOp(node, ctx) {
2925
3636
  const left = this.eval(node.left, ctx);
2926
- if (node.operator === "and")
3637
+ const op = node.operator;
3638
+ if (op === "and")
2927
3639
  return this.isTruthy(left) ? this.eval(node.right, ctx) : left;
2928
- if (node.operator === "or")
3640
+ if (op === "or")
2929
3641
  return this.isTruthy(left) ? left : this.eval(node.right, ctx);
2930
3642
  const right = this.eval(node.right, ctx);
2931
- switch (node.operator) {
2932
- case "+":
2933
- return typeof left === "string" || typeof right === "string" ? String(left) + String(right) : Number(left) + Number(right);
3643
+ if (op === "%") {
3644
+ if (typeof left === "number" && typeof right === "number") {
3645
+ return right === 0 ? NaN : left % right;
3646
+ }
3647
+ const r = Number(right);
3648
+ return r === 0 ? NaN : Number(left) % r;
3649
+ }
3650
+ if (op === "+") {
3651
+ if (typeof left === "number" && typeof right === "number") {
3652
+ return left + right;
3653
+ }
3654
+ return typeof left === "string" || typeof right === "string" ? String(left) + String(right) : Number(left) + Number(right);
3655
+ }
3656
+ switch (op) {
2934
3657
  case "-":
2935
3658
  return Number(left) - Number(right);
2936
3659
  case "*":
2937
3660
  return Number(left) * Number(right);
2938
3661
  case "/":
2939
3662
  return Number(left) / Number(right);
2940
- case "%":
2941
- return Number(right) === 0 ? NaN : Number(left) % Number(right);
2942
3663
  case "~":
2943
3664
  return String(left) + String(right);
2944
3665
  default:
@@ -2959,47 +3680,74 @@ class Runtime {
2959
3680
  }
2960
3681
  }
2961
3682
  evalCompare(node, ctx) {
2962
- let left = this.eval(node.left, ctx);
2963
- for (const { operator, right: rightNode } of node.ops) {
3683
+ const left = this.eval(node.left, ctx);
3684
+ const ops = node.ops;
3685
+ if (ops.length === 1) {
3686
+ const { operator, right: rightNode } = ops[0];
3687
+ const right = this.eval(rightNode, ctx);
3688
+ if (operator === "==")
3689
+ return left === right;
3690
+ if (operator === "!=")
3691
+ return left !== right;
3692
+ if (operator === "<")
3693
+ return left < right;
3694
+ if (operator === ">")
3695
+ return left > right;
3696
+ if (operator === "<=")
3697
+ return left <= right;
3698
+ if (operator === ">=")
3699
+ return left >= right;
3700
+ if (operator === "in")
3701
+ return this.isIn(left, right);
3702
+ if (operator === "not in")
3703
+ return !this.isIn(left, right);
3704
+ if (operator === "is")
3705
+ return left === right;
3706
+ if (operator === "is not")
3707
+ return left !== right;
3708
+ return false;
3709
+ }
3710
+ let current = left;
3711
+ for (const { operator, right: rightNode } of ops) {
2964
3712
  const right = this.eval(rightNode, ctx);
2965
3713
  let result;
2966
3714
  switch (operator) {
2967
3715
  case "==":
2968
- result = left === right;
3716
+ result = current === right;
2969
3717
  break;
2970
3718
  case "!=":
2971
- result = left !== right;
3719
+ result = current !== right;
2972
3720
  break;
2973
3721
  case "<":
2974
- result = left < right;
3722
+ result = current < right;
2975
3723
  break;
2976
3724
  case ">":
2977
- result = left > right;
3725
+ result = current > right;
2978
3726
  break;
2979
3727
  case "<=":
2980
- result = left <= right;
3728
+ result = current <= right;
2981
3729
  break;
2982
3730
  case ">=":
2983
- result = left >= right;
3731
+ result = current >= right;
2984
3732
  break;
2985
3733
  case "in":
2986
- result = this.isIn(left, right);
3734
+ result = this.isIn(current, right);
2987
3735
  break;
2988
3736
  case "not in":
2989
- result = !this.isIn(left, right);
3737
+ result = !this.isIn(current, right);
2990
3738
  break;
2991
3739
  case "is":
2992
- result = left === right;
3740
+ result = current === right;
2993
3741
  break;
2994
3742
  case "is not":
2995
- result = left !== right;
3743
+ result = current !== right;
2996
3744
  break;
2997
3745
  default:
2998
3746
  result = false;
2999
3747
  }
3000
3748
  if (!result)
3001
3749
  return false;
3002
- left = right;
3750
+ current = right;
3003
3751
  }
3004
3752
  return true;
3005
3753
  }
@@ -3100,6 +3848,24 @@ class Runtime {
3100
3848
  return this.renderStaticSync(node, ctx);
3101
3849
  case "Now":
3102
3850
  return this.renderNowSync(node, ctx);
3851
+ case "Cycle":
3852
+ return this.renderCycleSync(node, ctx);
3853
+ case "Firstof":
3854
+ return this.renderFirstofSync(node, ctx);
3855
+ case "Ifchanged":
3856
+ return this.renderIfchangedSync(node, ctx);
3857
+ case "Regroup":
3858
+ return this.renderRegroupSync(node, ctx);
3859
+ case "Widthratio":
3860
+ return this.renderWidthratioSync(node, ctx);
3861
+ case "Lorem":
3862
+ return this.renderLoremSync(node, ctx);
3863
+ case "CsrfToken":
3864
+ return this.renderCsrfTokenSync();
3865
+ case "Debug":
3866
+ return this.renderDebugSync(ctx);
3867
+ case "Templatetag":
3868
+ return this.renderTemplatetagSync(node);
3103
3869
  default:
3104
3870
  return null;
3105
3871
  }
@@ -3205,25 +3971,29 @@ class Runtime {
3205
3971
  stringify(value) {
3206
3972
  if (value == null)
3207
3973
  return "";
3974
+ if (typeof value === "string") {
3975
+ if (value.__safe__)
3976
+ return value;
3977
+ return this.options.autoescape ? Bun.escapeHTML(value) : value;
3978
+ }
3208
3979
  if (typeof value === "boolean")
3209
3980
  return value ? "True" : "False";
3981
+ if (typeof value === "number")
3982
+ return String(value);
3210
3983
  const str = String(value);
3211
3984
  if (value.__safe__)
3212
3985
  return str;
3213
- if (this.options.autoescape) {
3214
- return Bun.escapeHTML(str);
3215
- }
3216
- return str;
3986
+ return this.options.autoescape ? Bun.escapeHTML(str) : str;
3217
3987
  }
3218
3988
  isTruthy(value) {
3219
- if (value == null)
3220
- return false;
3221
3989
  if (typeof value === "boolean")
3222
3990
  return value;
3223
- if (typeof value === "number")
3224
- return value !== 0;
3991
+ if (value == null)
3992
+ return false;
3225
3993
  if (typeof value === "string")
3226
3994
  return value.length > 0;
3995
+ if (typeof value === "number")
3996
+ return value !== 0;
3227
3997
  if (Array.isArray(value))
3228
3998
  return value.length > 0;
3229
3999
  if (typeof value === "object") {