binja 0.4.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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
  }
@@ -2231,6 +2483,195 @@ ${indent2}`;
2231
2483
  safeString.__safe__ = true;
2232
2484
  return safeString;
2233
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
+ };
2652
+ var items = (value) => {
2653
+ if (value == null || typeof value !== "object")
2654
+ return [];
2655
+ return Object.entries(value);
2656
+ };
2657
+ var xmlattr = (value, autospace = true) => {
2658
+ if (value == null || typeof value !== "object")
2659
+ return "";
2660
+ const attrs = [];
2661
+ for (const [key, val] of Object.entries(value)) {
2662
+ if (val === true) {
2663
+ attrs.push(key);
2664
+ } else if (val !== false && val != null) {
2665
+ const escaped = String(val).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2666
+ attrs.push(`${key}="${escaped}"`);
2667
+ }
2668
+ }
2669
+ const result = attrs.join(" ");
2670
+ const output = autospace && result ? " " + result : result;
2671
+ const safeString = new String(output);
2672
+ safeString.__safe__ = true;
2673
+ return safeString;
2674
+ };
2234
2675
  var builtinFilters = {
2235
2676
  upper,
2236
2677
  lower,
@@ -2309,7 +2750,18 @@ var builtinFilters = {
2309
2750
  forceescape,
2310
2751
  phone2numeric,
2311
2752
  linenumbers,
2312
- unordered_list
2753
+ unordered_list,
2754
+ addslashes,
2755
+ get_digit,
2756
+ iriencode,
2757
+ json_script,
2758
+ safeseq,
2759
+ stringformat,
2760
+ truncatechars_html,
2761
+ truncatewords_html,
2762
+ urlizetrunc,
2763
+ items,
2764
+ xmlattr
2313
2765
  };
2314
2766
 
2315
2767
  // src/tests/index.ts
@@ -2625,6 +3077,24 @@ class Runtime {
2625
3077
  case "Load":
2626
3078
  case "Extends":
2627
3079
  return null;
3080
+ case "Cycle":
3081
+ return this.renderCycleSync(node, ctx);
3082
+ case "Firstof":
3083
+ return this.renderFirstofSync(node, ctx);
3084
+ case "Ifchanged":
3085
+ return this.renderIfchangedSync(node, ctx);
3086
+ case "Regroup":
3087
+ return this.renderRegroupSync(node, ctx);
3088
+ case "Widthratio":
3089
+ return this.renderWidthratioSync(node, ctx);
3090
+ case "Lorem":
3091
+ return this.renderLoremSync(node, ctx);
3092
+ case "CsrfToken":
3093
+ return this.renderCsrfTokenSync();
3094
+ case "Debug":
3095
+ return this.renderDebugSync(ctx);
3096
+ case "Templatetag":
3097
+ return this.renderTemplatetagSync(node);
2628
3098
  default:
2629
3099
  return null;
2630
3100
  }
@@ -2642,13 +3112,13 @@ class Runtime {
2642
3112
  }
2643
3113
  renderForSync(node, ctx) {
2644
3114
  const iterable2 = this.eval(node.iter, ctx);
2645
- const items = this.toIterable(iterable2);
2646
- const len = items.length;
3115
+ const items2 = this.toIterable(iterable2);
3116
+ const len = items2.length;
2647
3117
  if (len === 0) {
2648
3118
  return this.renderNodesSync(node.else_, ctx);
2649
3119
  }
2650
3120
  ctx.push();
2651
- ctx.pushForLoop(items, 0);
3121
+ ctx.pushForLoop(items2, 0);
2652
3122
  let result;
2653
3123
  if (Array.isArray(node.target)) {
2654
3124
  const targets = node.target;
@@ -2656,9 +3126,9 @@ class Runtime {
2656
3126
  if (len < 50) {
2657
3127
  result = "";
2658
3128
  for (let i = 0;i < len; i++) {
2659
- const item = items[i];
3129
+ const item = items2[i];
2660
3130
  if (i > 0)
2661
- ctx.updateForLoop(i, items);
3131
+ ctx.updateForLoop(i, items2);
2662
3132
  let values;
2663
3133
  if (Array.isArray(item)) {
2664
3134
  values = item;
@@ -2675,9 +3145,9 @@ class Runtime {
2675
3145
  } else {
2676
3146
  const parts = new Array(len);
2677
3147
  for (let i = 0;i < len; i++) {
2678
- const item = items[i];
3148
+ const item = items2[i];
2679
3149
  if (i > 0)
2680
- ctx.updateForLoop(i, items);
3150
+ ctx.updateForLoop(i, items2);
2681
3151
  let values;
2682
3152
  if (Array.isArray(item)) {
2683
3153
  values = item;
@@ -2699,16 +3169,16 @@ class Runtime {
2699
3169
  result = "";
2700
3170
  for (let i = 0;i < len; i++) {
2701
3171
  if (i > 0)
2702
- ctx.updateForLoop(i, items);
2703
- ctx.set(target, items[i]);
3172
+ ctx.updateForLoop(i, items2);
3173
+ ctx.set(target, items2[i]);
2704
3174
  result += this.renderNodesSync(node.body, ctx);
2705
3175
  }
2706
3176
  } else {
2707
3177
  const parts = new Array(len);
2708
3178
  for (let i = 0;i < len; i++) {
2709
3179
  if (i > 0)
2710
- ctx.updateForLoop(i, items);
2711
- ctx.set(target, items[i]);
3180
+ ctx.updateForLoop(i, items2);
3181
+ ctx.set(target, items2[i]);
2712
3182
  parts[i] = this.renderNodesSync(node.body, ctx);
2713
3183
  }
2714
3184
  result = parts.join("");
@@ -2770,6 +3240,217 @@ class Runtime {
2770
3240
  }
2771
3241
  return result;
2772
3242
  }
3243
+ cycleState = new Map;
3244
+ renderCycleSync(node, ctx) {
3245
+ const key = node.values.map((v) => JSON.stringify(v)).join(",");
3246
+ const currentIndex = this.cycleState.get(key) ?? 0;
3247
+ const values = node.values.map((v) => this.eval(v, ctx));
3248
+ const value = values[currentIndex % values.length];
3249
+ this.cycleState.set(key, currentIndex + 1);
3250
+ if (node.asVar) {
3251
+ ctx.set(node.asVar, value);
3252
+ return node.silent ? "" : this.stringify(value);
3253
+ }
3254
+ return this.stringify(value);
3255
+ }
3256
+ renderFirstofSync(node, ctx) {
3257
+ for (const expr of node.values) {
3258
+ const value = this.eval(expr, ctx);
3259
+ if (this.isTruthy(value)) {
3260
+ if (node.asVar) {
3261
+ ctx.set(node.asVar, value);
3262
+ return "";
3263
+ }
3264
+ return this.stringify(value);
3265
+ }
3266
+ }
3267
+ const fallback = node.fallback ? this.eval(node.fallback, ctx) : "";
3268
+ if (node.asVar) {
3269
+ ctx.set(node.asVar, fallback);
3270
+ return "";
3271
+ }
3272
+ return this.stringify(fallback);
3273
+ }
3274
+ ifchangedState = new Map;
3275
+ renderIfchangedSync(node, ctx) {
3276
+ const key = `ifchanged_${node.line}_${node.column}`;
3277
+ let currentValue;
3278
+ if (node.values.length > 0) {
3279
+ currentValue = node.values.map((v) => this.eval(v, ctx));
3280
+ } else {
3281
+ currentValue = this.renderNodesSync(node.body, ctx);
3282
+ }
3283
+ const lastValue = this.ifchangedState.get(key);
3284
+ const changed = JSON.stringify(currentValue) !== JSON.stringify(lastValue);
3285
+ this.ifchangedState.set(key, currentValue);
3286
+ if (changed) {
3287
+ if (node.values.length > 0) {
3288
+ return this.renderNodesSync(node.body, ctx);
3289
+ }
3290
+ return currentValue;
3291
+ } else {
3292
+ return this.renderNodesSync(node.else_, ctx);
3293
+ }
3294
+ }
3295
+ renderRegroupSync(node, ctx) {
3296
+ const list2 = this.toIterable(this.eval(node.target, ctx));
3297
+ const groups = [];
3298
+ let currentGrouper = Symbol("initial");
3299
+ let currentList = [];
3300
+ for (const item of list2) {
3301
+ const grouper = item && typeof item === "object" ? item[node.key] : undefined;
3302
+ if (grouper !== currentGrouper) {
3303
+ if (currentList.length > 0) {
3304
+ groups.push({ grouper: currentGrouper, list: currentList });
3305
+ }
3306
+ currentGrouper = grouper;
3307
+ currentList = [item];
3308
+ } else {
3309
+ currentList.push(item);
3310
+ }
3311
+ }
3312
+ if (currentList.length > 0) {
3313
+ groups.push({ grouper: currentGrouper, list: currentList });
3314
+ }
3315
+ ctx.set(node.asVar, groups);
3316
+ return "";
3317
+ }
3318
+ renderWidthratioSync(node, ctx) {
3319
+ const value = Number(this.eval(node.value, ctx));
3320
+ const maxValue = Number(this.eval(node.maxValue, ctx));
3321
+ const maxWidth = Number(this.eval(node.maxWidth, ctx));
3322
+ const ratio = maxValue === 0 ? 0 : Math.round(value / maxValue * maxWidth);
3323
+ if (node.asVar) {
3324
+ ctx.set(node.asVar, ratio);
3325
+ return "";
3326
+ }
3327
+ return String(ratio);
3328
+ }
3329
+ renderLoremSync(node, ctx) {
3330
+ const count = node.count ? Number(this.eval(node.count, ctx)) : 1;
3331
+ const method = node.method;
3332
+ const words = [
3333
+ "lorem",
3334
+ "ipsum",
3335
+ "dolor",
3336
+ "sit",
3337
+ "amet",
3338
+ "consectetur",
3339
+ "adipiscing",
3340
+ "elit",
3341
+ "sed",
3342
+ "do",
3343
+ "eiusmod",
3344
+ "tempor",
3345
+ "incididunt",
3346
+ "ut",
3347
+ "labore",
3348
+ "et",
3349
+ "dolore",
3350
+ "magna",
3351
+ "aliqua",
3352
+ "enim",
3353
+ "ad",
3354
+ "minim",
3355
+ "veniam",
3356
+ "quis",
3357
+ "nostrud",
3358
+ "exercitation",
3359
+ "ullamco",
3360
+ "laboris",
3361
+ "nisi",
3362
+ "aliquip",
3363
+ "ex",
3364
+ "ea",
3365
+ "commodo",
3366
+ "consequat",
3367
+ "duis",
3368
+ "aute",
3369
+ "irure",
3370
+ "in",
3371
+ "reprehenderit",
3372
+ "voluptate",
3373
+ "velit",
3374
+ "esse",
3375
+ "cillum",
3376
+ "fugiat",
3377
+ "nulla",
3378
+ "pariatur",
3379
+ "excepteur",
3380
+ "sint",
3381
+ "occaecat",
3382
+ "cupidatat",
3383
+ "non",
3384
+ "proident",
3385
+ "sunt",
3386
+ "culpa",
3387
+ "qui",
3388
+ "officia",
3389
+ "deserunt",
3390
+ "mollit",
3391
+ "anim",
3392
+ "id",
3393
+ "est",
3394
+ "laborum"
3395
+ ];
3396
+ const getWord = (index) => {
3397
+ if (node.random) {
3398
+ return words[Math.floor(Math.random() * words.length)];
3399
+ }
3400
+ return words[index % words.length];
3401
+ };
3402
+ if (method === "w") {
3403
+ const result = [];
3404
+ for (let i = 0;i < count; i++) {
3405
+ result.push(getWord(i));
3406
+ }
3407
+ return result.join(" ");
3408
+ } else if (method === "p" || method === "b") {
3409
+ const paragraphs = [];
3410
+ for (let p = 0;p < count; p++) {
3411
+ const sentenceCount = 3 + p % 3;
3412
+ const sentences = [];
3413
+ for (let s = 0;s < sentenceCount; s++) {
3414
+ const wordCount = 8 + s % 7;
3415
+ const sentenceWords = [];
3416
+ for (let w = 0;w < wordCount; w++) {
3417
+ sentenceWords.push(getWord(p * 100 + s * 20 + w));
3418
+ }
3419
+ sentenceWords[0] = sentenceWords[0].charAt(0).toUpperCase() + sentenceWords[0].slice(1);
3420
+ sentences.push(sentenceWords.join(" ") + ".");
3421
+ }
3422
+ paragraphs.push(sentences.join(" "));
3423
+ }
3424
+ if (method === "b") {
3425
+ return paragraphs.join(`
3426
+
3427
+ `);
3428
+ }
3429
+ return paragraphs.map((p) => `<p>${p}</p>`).join(`
3430
+ `);
3431
+ }
3432
+ return "";
3433
+ }
3434
+ renderCsrfTokenSync() {
3435
+ return '<input type="hidden" name="csrfmiddlewaretoken" value="CSRF_TOKEN_PLACEHOLDER">';
3436
+ }
3437
+ renderDebugSync(ctx) {
3438
+ const data = ctx.toObject?.() || {};
3439
+ return `<pre>${JSON.stringify(data, null, 2)}</pre>`;
3440
+ }
3441
+ renderTemplatetagSync(node) {
3442
+ const tagMap = {
3443
+ openblock: "{%",
3444
+ closeblock: "%}",
3445
+ openvariable: "{{",
3446
+ closevariable: "}}",
3447
+ openbrace: "{",
3448
+ closebrace: "}",
3449
+ opencomment: "{#",
3450
+ closecomment: "#}"
3451
+ };
3452
+ return tagMap[node.tagType] || "";
3453
+ }
2773
3454
  getDateInTimezone(d, tz) {
2774
3455
  if (!tz) {
2775
3456
  return {
@@ -3192,6 +3873,24 @@ class Runtime {
3192
3873
  return this.renderStaticSync(node, ctx);
3193
3874
  case "Now":
3194
3875
  return this.renderNowSync(node, ctx);
3876
+ case "Cycle":
3877
+ return this.renderCycleSync(node, ctx);
3878
+ case "Firstof":
3879
+ return this.renderFirstofSync(node, ctx);
3880
+ case "Ifchanged":
3881
+ return this.renderIfchangedSync(node, ctx);
3882
+ case "Regroup":
3883
+ return this.renderRegroupSync(node, ctx);
3884
+ case "Widthratio":
3885
+ return this.renderWidthratioSync(node, ctx);
3886
+ case "Lorem":
3887
+ return this.renderLoremSync(node, ctx);
3888
+ case "CsrfToken":
3889
+ return this.renderCsrfTokenSync();
3890
+ case "Debug":
3891
+ return this.renderDebugSync(ctx);
3892
+ case "Templatetag":
3893
+ return this.renderTemplatetagSync(node);
3195
3894
  default:
3196
3895
  return null;
3197
3896
  }
@@ -3209,19 +3908,19 @@ class Runtime {
3209
3908
  }
3210
3909
  async renderForAsync(node, ctx) {
3211
3910
  const iterable2 = this.eval(node.iter, ctx);
3212
- const items = this.toIterable(iterable2);
3213
- const len = items.length;
3911
+ const items2 = this.toIterable(iterable2);
3912
+ const len = items2.length;
3214
3913
  if (len === 0) {
3215
3914
  return this.renderNodesAsync(node.else_, ctx);
3216
3915
  }
3217
3916
  const parts = new Array(len);
3218
3917
  const isUnpacking = Array.isArray(node.target);
3219
3918
  ctx.push();
3220
- ctx.pushForLoop(items, 0);
3919
+ ctx.pushForLoop(items2, 0);
3221
3920
  for (let i = 0;i < len; i++) {
3222
- const item = items[i];
3921
+ const item = items2[i];
3223
3922
  if (i > 0)
3224
- ctx.updateForLoop(i, items);
3923
+ ctx.updateForLoop(i, items2);
3225
3924
  if (isUnpacking) {
3226
3925
  let values;
3227
3926
  if (Array.isArray(item)) {
@@ -4233,8 +4932,8 @@ class DebugCollector {
4233
4932
  if (value.length === 0)
4234
4933
  return "[]";
4235
4934
  if (value.length <= 3) {
4236
- const items = value.map((v) => this.getPreview(v, 15)).join(", ");
4237
- return `[${items}]`;
4935
+ const items2 = value.map((v) => this.getPreview(v, 15)).join(", ");
4936
+ return `[${items2}]`;
4238
4937
  }
4239
4938
  return `[${this.getPreview(value[0], 15)}, ... +${value.length - 1}]`;
4240
4939
  }
@@ -4546,7 +5245,7 @@ function generateContextSection(data) {
4546
5245
  const keys = Object.keys(data.contextSnapshot);
4547
5246
  if (keys.length === 0)
4548
5247
  return "";
4549
- const items = keys.map((key) => renderContextValue(key, data.contextSnapshot[key])).join("");
5248
+ const items2 = keys.map((key) => renderContextValue(key, data.contextSnapshot[key])).join("");
4550
5249
  return `
4551
5250
  <div class="dbg-section">
4552
5251
  <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
@@ -4555,7 +5254,7 @@ function generateContextSection(data) {
4555
5254
  <span class="dbg-chevron">${icons.chevron}</span>
4556
5255
  </div>
4557
5256
  <div class="dbg-section-content">
4558
- <div class="dbg-ctx-grid">${items}</div>
5257
+ <div class="dbg-ctx-grid">${items2}</div>
4559
5258
  </div>
4560
5259
  </div>`;
4561
5260
  }
@@ -4585,7 +5284,7 @@ function generateFiltersSection(data) {
4585
5284
  const filters = Array.from(data.filtersUsed.entries());
4586
5285
  if (filters.length === 0)
4587
5286
  return "";
4588
- const items = filters.map(([name, count]) => `<span class="dbg-filter">${name}<span class="dbg-filter-count">\xD7${count}</span></span>`).join("");
5287
+ const items2 = filters.map(([name, count]) => `<span class="dbg-filter">${name}<span class="dbg-filter-count">\xD7${count}</span></span>`).join("");
4589
5288
  return `
4590
5289
  <div class="dbg-section">
4591
5290
  <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
@@ -4594,7 +5293,7 @@ function generateFiltersSection(data) {
4594
5293
  <span class="dbg-chevron">${icons.chevron}</span>
4595
5294
  </div>
4596
5295
  <div class="dbg-section-content">
4597
- <div class="dbg-filters">${items}</div>
5296
+ <div class="dbg-filters">${items2}</div>
4598
5297
  </div>
4599
5298
  </div>`;
4600
5299
  }
@@ -4626,7 +5325,7 @@ function generateCacheSection(data) {
4626
5325
  function generateWarningsSection(data) {
4627
5326
  if (data.warnings.length === 0)
4628
5327
  return "";
4629
- const items = data.warnings.map((w) => `
5328
+ const items2 = data.warnings.map((w) => `
4630
5329
  <div class="dbg-warning">
4631
5330
  ${icons.warning}
4632
5331
  <span class="dbg-warning-text">${escapeHtml(w)}</span>
@@ -4640,7 +5339,7 @@ function generateWarningsSection(data) {
4640
5339
  <span class="dbg-chevron">${icons.chevron}</span>
4641
5340
  </div>
4642
5341
  <div class="dbg-section-content">
4643
- <div class="dbg-warnings">${items}</div>
5342
+ <div class="dbg-warnings">${items2}</div>
4644
5343
  </div>
4645
5344
  </div>`;
4646
5345
  }