binja 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/lexer/tokens.ts
3
5
  var TokenType;
4
6
  ((TokenType2) => {
@@ -95,6 +97,14 @@ class Lexer {
95
97
  const wsControl = this.peek() === "-";
96
98
  if (wsControl)
97
99
  this.advance();
100
+ const savedPos = this.state.pos;
101
+ this.skipWhitespace();
102
+ if (this.checkWord("raw") || this.checkWord("verbatim")) {
103
+ const tagName = this.checkWord("raw") ? "raw" : "verbatim";
104
+ this.scanRawBlock(tagName, wsControl);
105
+ return;
106
+ }
107
+ this.state.pos = savedPos;
98
108
  this.addToken("BLOCK_START" /* BLOCK_START */, this.blockStart + (wsControl ? "-" : ""));
99
109
  this.scanExpression(this.blockEnd, "BLOCK_END" /* BLOCK_END */);
100
110
  return;
@@ -105,6 +115,73 @@ class Lexer {
105
115
  }
106
116
  this.scanText();
107
117
  }
118
+ checkWord(word) {
119
+ const start = this.state.pos;
120
+ for (let i = 0;i < word.length; i++) {
121
+ if (this.state.source[start + i]?.toLowerCase() !== word[i]) {
122
+ return false;
123
+ }
124
+ }
125
+ const nextChar = this.state.source[start + word.length];
126
+ return !nextChar || !this.isAlphaNumeric(nextChar);
127
+ }
128
+ scanRawBlock(tagName, wsControl) {
129
+ const startLine = this.state.line;
130
+ const startColumn = this.state.column;
131
+ for (let i = 0;i < tagName.length; i++) {
132
+ this.advance();
133
+ }
134
+ this.skipWhitespace();
135
+ if (this.peek() === "-")
136
+ this.advance();
137
+ if (!this.match(this.blockEnd)) {
138
+ throw new Error(`Expected ${this.blockEnd} after ${tagName} at line ${this.state.line}`);
139
+ }
140
+ const endTag = `end${tagName}`;
141
+ const contentStart = this.state.pos;
142
+ while (!this.isAtEnd()) {
143
+ if (this.check(this.blockStart)) {
144
+ const savedPos = this.state.pos;
145
+ const savedLine = this.state.line;
146
+ const savedColumn = this.state.column;
147
+ this.match(this.blockStart);
148
+ if (this.peek() === "-")
149
+ this.advance();
150
+ this.skipWhitespace();
151
+ if (this.checkWord(endTag)) {
152
+ const content = this.state.source.slice(contentStart, savedPos);
153
+ if (content.length > 0) {
154
+ this.state.tokens.push({
155
+ type: "TEXT" /* TEXT */,
156
+ value: content,
157
+ line: startLine,
158
+ column: startColumn
159
+ });
160
+ }
161
+ for (let i = 0;i < endTag.length; i++) {
162
+ this.advance();
163
+ }
164
+ this.skipWhitespace();
165
+ if (this.peek() === "-")
166
+ this.advance();
167
+ if (!this.match(this.blockEnd)) {
168
+ throw new Error(`Expected ${this.blockEnd} after ${endTag} at line ${this.state.line}`);
169
+ }
170
+ return;
171
+ }
172
+ this.state.pos = savedPos;
173
+ this.state.line = savedLine;
174
+ this.state.column = savedColumn;
175
+ }
176
+ if (this.peek() === `
177
+ `) {
178
+ this.state.line++;
179
+ this.state.column = 0;
180
+ }
181
+ this.advance();
182
+ }
183
+ throw new Error(`Unclosed ${tagName} block starting at line ${startLine}`);
184
+ }
108
185
  scanText() {
109
186
  const start = this.state.pos;
110
187
  const startLine = this.state.line;
@@ -1704,6 +1781,263 @@ var groupby = (value, attribute) => {
1704
1781
  list
1705
1782
  }));
1706
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
+ };
1707
2041
  var builtinFilters = {
1708
2042
  upper,
1709
2043
  lower,
@@ -1762,7 +2096,27 @@ var builtinFilters = {
1762
2096
  tojson: json,
1763
2097
  random,
1764
2098
  batch,
1765
- 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
1766
2120
  };
1767
2121
 
1768
2122
  // src/tests/index.ts
@@ -1808,7 +2162,7 @@ var none = (value) => {
1808
2162
  var boolean = (value) => {
1809
2163
  return typeof value === "boolean";
1810
2164
  };
1811
- var string = (value) => {
2165
+ var string2 = (value) => {
1812
2166
  return typeof value === "string";
1813
2167
  };
1814
2168
  var mapping = (value) => {
@@ -1898,7 +2252,7 @@ var builtinTests = {
1898
2252
  undefined: undefined2,
1899
2253
  none,
1900
2254
  boolean,
1901
- string,
2255
+ string: string2,
1902
2256
  mapping,
1903
2257
  iterable,
1904
2258
  sequence,
@@ -2638,6 +2992,7 @@ class Compiler {
2638
2992
  indent = 0;
2639
2993
  varCounter = 0;
2640
2994
  loopStack = [];
2995
+ localVars = [];
2641
2996
  constructor(options = {}) {
2642
2997
  this.options = {
2643
2998
  functionName: options.functionName ?? "render",
@@ -2646,6 +3001,24 @@ class Compiler {
2646
3001
  autoescape: options.autoescape ?? true
2647
3002
  };
2648
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
+ }
2649
3022
  compile(ast) {
2650
3023
  const body = this.compileNodes(ast.body);
2651
3024
  const nl = this.options.minify ? "" : `
@@ -2773,12 +3146,15 @@ class Compiler {
2773
3146
  }
2774
3147
  compileWith(node) {
2775
3148
  let code = ` {${this.nl()}`;
3149
+ this.pushScope();
2776
3150
  for (const { target, value } of node.assignments) {
2777
3151
  const valueExpr = this.compileExpr(value);
2778
3152
  code += ` const ${target} = ${valueExpr};${this.nl()}`;
3153
+ this.addLocalVar(target);
2779
3154
  }
2780
3155
  code += this.compileNodes(node.body);
2781
3156
  code += ` }${this.nl()}`;
3157
+ this.popScope();
2782
3158
  return code;
2783
3159
  }
2784
3160
  compileExpr(node) {
@@ -2820,6 +3196,9 @@ class Compiler {
2820
3196
  return "null";
2821
3197
  if (node.name === "forloop" || node.name === "loop")
2822
3198
  return node.name;
3199
+ if (this.isLocalVar(node.name)) {
3200
+ return node.name;
3201
+ }
2823
3202
  return `__ctx.${node.name}`;
2824
3203
  }
2825
3204
  compileLiteral(node) {
@@ -2995,6 +3374,956 @@ class Compiler {
2995
3374
  }
2996
3375
  }
2997
3376
 
3377
+ // src/compiler/flattener.ts
3378
+ function flattenTemplate(ast, options) {
3379
+ const flattener = new TemplateFlattener(options);
3380
+ return flattener.flatten(ast);
3381
+ }
3382
+ function canFlatten(ast) {
3383
+ const checker = new StaticChecker;
3384
+ return checker.check(ast);
3385
+ }
3386
+
3387
+ class TemplateFlattener {
3388
+ loader;
3389
+ maxDepth;
3390
+ blocks = new Map;
3391
+ depth = 0;
3392
+ constructor(options) {
3393
+ this.loader = options.loader;
3394
+ this.maxDepth = options.maxDepth ?? 10;
3395
+ }
3396
+ flatten(ast) {
3397
+ this.blocks.clear();
3398
+ this.depth = 0;
3399
+ return this.processTemplate(ast);
3400
+ }
3401
+ processTemplate(ast, isChild = true) {
3402
+ if (this.depth > this.maxDepth) {
3403
+ throw new Error(`Maximum template inheritance depth (${this.maxDepth}) exceeded`);
3404
+ }
3405
+ this.collectBlocks(ast.body, isChild);
3406
+ const extendsNode = this.findExtends(ast.body);
3407
+ if (extendsNode) {
3408
+ const parentName = this.getStaticTemplateName(extendsNode.template);
3409
+ const parentSource = this.loader.load(parentName);
3410
+ const parentAst = this.loader.parse(parentSource);
3411
+ this.depth++;
3412
+ const flattenedParent = this.processTemplate(parentAst, false);
3413
+ this.depth--;
3414
+ return {
3415
+ type: "Template",
3416
+ body: this.replaceBlocks(flattenedParent.body),
3417
+ line: ast.line,
3418
+ column: ast.column
3419
+ };
3420
+ }
3421
+ return {
3422
+ type: "Template",
3423
+ body: this.processNodes(ast.body),
3424
+ line: ast.line,
3425
+ column: ast.column
3426
+ };
3427
+ }
3428
+ collectBlocks(nodes2, override = true) {
3429
+ for (const node of nodes2) {
3430
+ if (node.type === "Block") {
3431
+ const block = node;
3432
+ if (override || !this.blocks.has(block.name)) {
3433
+ this.blocks.set(block.name, block);
3434
+ }
3435
+ }
3436
+ this.collectBlocksFromNode(node, override);
3437
+ }
3438
+ }
3439
+ collectBlocksFromNode(node, override = true) {
3440
+ switch (node.type) {
3441
+ case "If": {
3442
+ const ifNode = node;
3443
+ this.collectBlocks(ifNode.body, override);
3444
+ for (const elif of ifNode.elifs) {
3445
+ this.collectBlocks(elif.body, override);
3446
+ }
3447
+ this.collectBlocks(ifNode.else_, override);
3448
+ break;
3449
+ }
3450
+ case "For": {
3451
+ const forNode = node;
3452
+ this.collectBlocks(forNode.body, override);
3453
+ this.collectBlocks(forNode.else_, override);
3454
+ break;
3455
+ }
3456
+ case "With": {
3457
+ const withNode = node;
3458
+ this.collectBlocks(withNode.body, override);
3459
+ break;
3460
+ }
3461
+ case "Block": {
3462
+ const blockNode = node;
3463
+ this.collectBlocks(blockNode.body, override);
3464
+ break;
3465
+ }
3466
+ }
3467
+ }
3468
+ findExtends(nodes2) {
3469
+ for (const node of nodes2) {
3470
+ if (node.type === "Extends") {
3471
+ return node;
3472
+ }
3473
+ }
3474
+ return null;
3475
+ }
3476
+ processNodes(nodes2) {
3477
+ const result = [];
3478
+ for (const node of nodes2) {
3479
+ if (node.type === "Extends") {
3480
+ continue;
3481
+ }
3482
+ if (node.type === "Include") {
3483
+ const includeNode = node;
3484
+ const inlined = this.inlineInclude(includeNode);
3485
+ result.push(...inlined);
3486
+ continue;
3487
+ }
3488
+ if (node.type === "Block") {
3489
+ const block = node;
3490
+ const childBlock = this.blocks.get(block.name);
3491
+ if (childBlock && childBlock !== block) {
3492
+ result.push(...this.processNodes(childBlock.body));
3493
+ } else {
3494
+ result.push(...this.processNodes(block.body));
3495
+ }
3496
+ continue;
3497
+ }
3498
+ const processed = this.processNode(node);
3499
+ if (processed) {
3500
+ result.push(processed);
3501
+ }
3502
+ }
3503
+ return result;
3504
+ }
3505
+ processNode(node) {
3506
+ switch (node.type) {
3507
+ case "If": {
3508
+ const ifNode = node;
3509
+ return {
3510
+ ...ifNode,
3511
+ body: this.processNodes(ifNode.body),
3512
+ elifs: ifNode.elifs.map((elif) => ({
3513
+ test: elif.test,
3514
+ body: this.processNodes(elif.body)
3515
+ })),
3516
+ else_: this.processNodes(ifNode.else_)
3517
+ };
3518
+ }
3519
+ case "For": {
3520
+ const forNode = node;
3521
+ return {
3522
+ ...forNode,
3523
+ body: this.processNodes(forNode.body),
3524
+ else_: this.processNodes(forNode.else_)
3525
+ };
3526
+ }
3527
+ case "With": {
3528
+ const withNode = node;
3529
+ return {
3530
+ ...withNode,
3531
+ body: this.processNodes(withNode.body)
3532
+ };
3533
+ }
3534
+ default:
3535
+ return node;
3536
+ }
3537
+ }
3538
+ replaceBlocks(nodes2) {
3539
+ return this.processNodes(nodes2);
3540
+ }
3541
+ inlineInclude(node) {
3542
+ const templateName = this.getStaticTemplateName(node.template);
3543
+ try {
3544
+ const source = this.loader.load(templateName);
3545
+ const ast = this.loader.parse(source);
3546
+ this.depth++;
3547
+ const flattened = this.processTemplate(ast);
3548
+ this.depth--;
3549
+ if (node.context && Object.keys(node.context).length > 0) {
3550
+ const withNode = {
3551
+ type: "With",
3552
+ assignments: Object.entries(node.context).map(([target, value]) => ({
3553
+ target,
3554
+ value
3555
+ })),
3556
+ body: flattened.body,
3557
+ line: node.line,
3558
+ column: node.column
3559
+ };
3560
+ return [withNode];
3561
+ }
3562
+ return flattened.body;
3563
+ } catch (error) {
3564
+ if (node.ignoreMissing) {
3565
+ return [];
3566
+ }
3567
+ throw error;
3568
+ }
3569
+ }
3570
+ getStaticTemplateName(expr) {
3571
+ if (expr.type === "Literal") {
3572
+ const literal = expr;
3573
+ if (typeof literal.value === "string") {
3574
+ return literal.value;
3575
+ }
3576
+ }
3577
+ throw new Error(`AOT compilation requires static template names. ` + `Found dynamic expression at line ${expr.line}. ` + `Use Environment.render() for dynamic template names.`);
3578
+ }
3579
+ }
3580
+
3581
+ class StaticChecker {
3582
+ check(ast) {
3583
+ return this.checkNodes(ast.body);
3584
+ }
3585
+ checkNodes(nodes2) {
3586
+ for (const node of nodes2) {
3587
+ const result = this.checkNode(node);
3588
+ if (!result.canFlatten) {
3589
+ return result;
3590
+ }
3591
+ }
3592
+ return { canFlatten: true };
3593
+ }
3594
+ checkNode(node) {
3595
+ switch (node.type) {
3596
+ case "Extends": {
3597
+ const extendsNode = node;
3598
+ if (!this.isStaticName(extendsNode.template)) {
3599
+ return {
3600
+ canFlatten: false,
3601
+ reason: `Dynamic extends at line ${node.line} - use static string literal`
3602
+ };
3603
+ }
3604
+ break;
3605
+ }
3606
+ case "Include": {
3607
+ const includeNode = node;
3608
+ if (!this.isStaticName(includeNode.template)) {
3609
+ return {
3610
+ canFlatten: false,
3611
+ reason: `Dynamic include at line ${node.line} - use static string literal`
3612
+ };
3613
+ }
3614
+ break;
3615
+ }
3616
+ case "If": {
3617
+ const ifNode = node;
3618
+ let result = this.checkNodes(ifNode.body);
3619
+ if (!result.canFlatten)
3620
+ return result;
3621
+ for (const elif of ifNode.elifs) {
3622
+ result = this.checkNodes(elif.body);
3623
+ if (!result.canFlatten)
3624
+ return result;
3625
+ }
3626
+ result = this.checkNodes(ifNode.else_);
3627
+ if (!result.canFlatten)
3628
+ return result;
3629
+ break;
3630
+ }
3631
+ case "For": {
3632
+ const forNode = node;
3633
+ let result = this.checkNodes(forNode.body);
3634
+ if (!result.canFlatten)
3635
+ return result;
3636
+ result = this.checkNodes(forNode.else_);
3637
+ if (!result.canFlatten)
3638
+ return result;
3639
+ break;
3640
+ }
3641
+ case "With": {
3642
+ const withNode = node;
3643
+ const result = this.checkNodes(withNode.body);
3644
+ if (!result.canFlatten)
3645
+ return result;
3646
+ break;
3647
+ }
3648
+ case "Block": {
3649
+ const blockNode = node;
3650
+ const result = this.checkNodes(blockNode.body);
3651
+ if (!result.canFlatten)
3652
+ return result;
3653
+ break;
3654
+ }
3655
+ }
3656
+ return { canFlatten: true };
3657
+ }
3658
+ isStaticName(expr) {
3659
+ return expr.type === "Literal" && typeof expr.value === "string";
3660
+ }
3661
+ }
3662
+
3663
+ // src/debug/collector.ts
3664
+ class DebugCollector {
3665
+ data;
3666
+ constructor() {
3667
+ this.data = {
3668
+ startTime: performance.now(),
3669
+ templateChain: [],
3670
+ mode: "runtime",
3671
+ isAsync: false,
3672
+ contextKeys: [],
3673
+ contextSnapshot: {},
3674
+ filtersUsed: new Map,
3675
+ testsUsed: new Map,
3676
+ cacheHits: 0,
3677
+ cacheMisses: 0,
3678
+ warnings: []
3679
+ };
3680
+ }
3681
+ startLexer() {
3682
+ this.data._lexerStart = performance.now();
3683
+ }
3684
+ endLexer() {
3685
+ if (this.data._lexerStart) {
3686
+ this.data.lexerTime = performance.now() - this.data._lexerStart;
3687
+ }
3688
+ }
3689
+ startParser() {
3690
+ this.data._parserStart = performance.now();
3691
+ }
3692
+ endParser() {
3693
+ if (this.data._parserStart) {
3694
+ this.data.parserTime = performance.now() - this.data._parserStart;
3695
+ }
3696
+ }
3697
+ startRender() {
3698
+ this.data._renderStart = performance.now();
3699
+ }
3700
+ endRender() {
3701
+ if (this.data._renderStart) {
3702
+ this.data.renderTime = performance.now() - this.data._renderStart;
3703
+ }
3704
+ this.data.endTime = performance.now();
3705
+ this.data.totalTime = this.data.endTime - this.data.startTime;
3706
+ }
3707
+ addTemplate(name, type, parent) {
3708
+ this.data.templateChain.push({ name, type, parent });
3709
+ if (type === "root") {
3710
+ this.data.rootTemplate = name;
3711
+ }
3712
+ }
3713
+ setMode(mode) {
3714
+ this.data.mode = mode;
3715
+ }
3716
+ setAsync(isAsync) {
3717
+ this.data.isAsync = isAsync;
3718
+ }
3719
+ captureContext(context) {
3720
+ this.data.contextKeys = Object.keys(context);
3721
+ for (const [key, value] of Object.entries(context)) {
3722
+ this.data.contextSnapshot[key] = this.captureValue(value);
3723
+ }
3724
+ }
3725
+ captureValue(value, depth = 0) {
3726
+ const type = this.getType(value);
3727
+ const preview = this.getPreview(value);
3728
+ const expandable = this.isExpandable(value);
3729
+ const result = { type, preview, value, expandable };
3730
+ if (expandable && depth < 3) {
3731
+ result.children = {};
3732
+ if (Array.isArray(value)) {
3733
+ value.forEach((item, i) => {
3734
+ result.children[String(i)] = this.captureValue(item, depth + 1);
3735
+ });
3736
+ } else if (typeof value === "object" && value !== null) {
3737
+ for (const [k, v] of Object.entries(value)) {
3738
+ result.children[k] = this.captureValue(v, depth + 1);
3739
+ }
3740
+ }
3741
+ }
3742
+ return result;
3743
+ }
3744
+ isExpandable(value) {
3745
+ if (value === null || value === undefined)
3746
+ return false;
3747
+ if (Array.isArray(value))
3748
+ return value.length > 0;
3749
+ if (typeof value === "object")
3750
+ return Object.keys(value).length > 0;
3751
+ return false;
3752
+ }
3753
+ getType(value) {
3754
+ if (value === null)
3755
+ return "null";
3756
+ if (value === undefined)
3757
+ return "undefined";
3758
+ if (Array.isArray(value))
3759
+ return `Array(${value.length})`;
3760
+ if (value instanceof Date)
3761
+ return "Date";
3762
+ if (typeof value === "object")
3763
+ return "Object";
3764
+ return typeof value;
3765
+ }
3766
+ getPreview(value, maxLen = 50) {
3767
+ if (value === null)
3768
+ return "null";
3769
+ if (value === undefined)
3770
+ return "undefined";
3771
+ if (typeof value === "string") {
3772
+ return value.length > maxLen ? `"${value.slice(0, maxLen)}..."` : `"${value}"`;
3773
+ }
3774
+ if (typeof value === "number" || typeof value === "boolean") {
3775
+ return String(value);
3776
+ }
3777
+ if (Array.isArray(value)) {
3778
+ if (value.length === 0)
3779
+ return "[]";
3780
+ if (value.length <= 3) {
3781
+ const items = value.map((v) => this.getPreview(v, 15)).join(", ");
3782
+ return `[${items}]`;
3783
+ }
3784
+ return `[${this.getPreview(value[0], 15)}, ... +${value.length - 1}]`;
3785
+ }
3786
+ if (value instanceof Date) {
3787
+ return value.toISOString();
3788
+ }
3789
+ if (typeof value === "object") {
3790
+ const keys = Object.keys(value);
3791
+ if (keys.length === 0)
3792
+ return "{}";
3793
+ if (keys.length <= 2) {
3794
+ return `{ ${keys.join(", ")} }`;
3795
+ }
3796
+ return `{ ${keys.slice(0, 2).join(", ")}, ... +${keys.length - 2} }`;
3797
+ }
3798
+ if (typeof value === "function") {
3799
+ return "function()";
3800
+ }
3801
+ return String(value);
3802
+ }
3803
+ recordFilter(name) {
3804
+ this.data.filtersUsed.set(name, (this.data.filtersUsed.get(name) || 0) + 1);
3805
+ }
3806
+ recordTest(name) {
3807
+ this.data.testsUsed.set(name, (this.data.testsUsed.get(name) || 0) + 1);
3808
+ }
3809
+ recordCacheHit() {
3810
+ this.data.cacheHits++;
3811
+ }
3812
+ recordCacheMiss() {
3813
+ this.data.cacheMisses++;
3814
+ }
3815
+ addWarning(message) {
3816
+ this.data.warnings.push(message);
3817
+ }
3818
+ getData() {
3819
+ return this.data;
3820
+ }
3821
+ getSummary() {
3822
+ return {
3823
+ totalTime: this.data.totalTime || 0,
3824
+ templateCount: this.data.templateChain.length,
3825
+ filterCount: this.data.filtersUsed.size,
3826
+ mode: this.data.mode
3827
+ };
3828
+ }
3829
+ }
3830
+ var currentCollector = null;
3831
+ function startDebugCollection() {
3832
+ currentCollector = new DebugCollector;
3833
+ return currentCollector;
3834
+ }
3835
+ function endDebugCollection() {
3836
+ if (currentCollector) {
3837
+ const data = currentCollector.getData();
3838
+ currentCollector = null;
3839
+ return data;
3840
+ }
3841
+ return null;
3842
+ }
3843
+
3844
+ // src/debug/panel.ts
3845
+ var DEFAULT_OPTIONS = {
3846
+ position: "bottom-right",
3847
+ collapsed: true,
3848
+ dark: true,
3849
+ width: 420
3850
+ };
3851
+ function generateDebugPanel(data, options = {}) {
3852
+ const opts = { ...DEFAULT_OPTIONS, ...options };
3853
+ const id = `binja-debug-${Date.now()}`;
3854
+ const colors = opts.dark ? darkTheme : lightTheme;
3855
+ return `
3856
+ <!-- Binja Debug Panel -->
3857
+ <div id="${id}" class="binja-debugger" data-theme="${opts.dark ? "dark" : "light"}">
3858
+ <style>${generateStyles(id, colors, opts)}</style>
3859
+ ${generateToggle(id, data, colors)}
3860
+ ${generatePanel(id, data, colors, opts)}
3861
+ <script>${generateScript(id)}</script>
3862
+ </div>
3863
+ <!-- /Binja Debug Panel -->
3864
+ `;
3865
+ }
3866
+ var darkTheme = {
3867
+ bg: "#0f0f0f",
3868
+ bgSecondary: "#1a1a1a",
3869
+ bgTertiary: "#242424",
3870
+ border: "#2a2a2a",
3871
+ borderLight: "#333",
3872
+ text: "#e5e5e5",
3873
+ textSecondary: "#a0a0a0",
3874
+ textMuted: "#666",
3875
+ accent: "#3b82f6",
3876
+ accentHover: "#2563eb",
3877
+ success: "#22c55e",
3878
+ warning: "#eab308",
3879
+ error: "#ef4444",
3880
+ info: "#06b6d4"
3881
+ };
3882
+ var lightTheme = {
3883
+ bg: "#ffffff",
3884
+ bgSecondary: "#f8f9fa",
3885
+ bgTertiary: "#f1f3f4",
3886
+ border: "#e5e7eb",
3887
+ borderLight: "#d1d5db",
3888
+ text: "#111827",
3889
+ textSecondary: "#4b5563",
3890
+ textMuted: "#9ca3af",
3891
+ accent: "#2563eb",
3892
+ accentHover: "#1d4ed8",
3893
+ success: "#16a34a",
3894
+ warning: "#ca8a04",
3895
+ error: "#dc2626",
3896
+ info: "#0891b2"
3897
+ };
3898
+ function generateStyles(id, c, opts) {
3899
+ const pos = getPosition(opts.position);
3900
+ return `
3901
+ #${id} { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; font-size: 13px; line-height: 1.5; position: fixed; ${pos} z-index: 2147483647; }
3902
+ #${id} * { box-sizing: border-box; margin: 0; padding: 0; }
3903
+ #${id} .dbg-toggle { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; background: ${c.bg}; border: 1px solid ${c.border}; border-radius: 8px; color: ${c.text}; cursor: pointer; font-size: 12px; font-weight: 500; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: all 0.2s ease; }
3904
+ #${id} .dbg-toggle:hover { border-color: ${c.accent}; box-shadow: 0 4px 16px rgba(0,0,0,0.2); }
3905
+ #${id} .dbg-toggle svg { width: 16px; height: 16px; }
3906
+ #${id} .dbg-toggle .dbg-time { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 11px; padding: 2px 8px; background: ${c.bgTertiary}; border-radius: 4px; color: ${c.success}; }
3907
+ #${id} .dbg-panel { display: none; width: ${opts.width}px; max-height: 85vh; background: ${c.bg}; border: 1px solid ${c.border}; border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.24); overflow: hidden; margin-top: 8px; }
3908
+ #${id} .dbg-panel.open { display: block; }
3909
+ #${id} .dbg-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${c.bgSecondary}; border-bottom: 1px solid ${c.border}; }
3910
+ #${id} .dbg-logo { display: flex; align-items: center; gap: 10px; font-weight: 600; color: ${c.text}; }
3911
+ #${id} .dbg-logo svg { width: 20px; height: 20px; color: ${c.accent}; }
3912
+ #${id} .dbg-meta { display: flex; align-items: center; gap: 12px; }
3913
+ #${id} .dbg-badge { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; padding: 3px 10px; border-radius: 4px; font-weight: 500; }
3914
+ #${id} .dbg-badge.time { background: rgba(34,197,94,0.1); color: ${c.success}; }
3915
+ #${id} .dbg-badge.mode { background: rgba(59,130,246,0.1); color: ${c.accent}; }
3916
+ #${id} .dbg-close { background: none; border: none; color: ${c.textMuted}; cursor: pointer; padding: 4px; border-radius: 4px; display: flex; }
3917
+ #${id} .dbg-close:hover { background: ${c.bgTertiary}; color: ${c.text}; }
3918
+ #${id} .dbg-close svg { width: 18px; height: 18px; }
3919
+ #${id} .dbg-body { max-height: calc(85vh - 52px); overflow-y: auto; }
3920
+ #${id} .dbg-section { border-bottom: 1px solid ${c.border}; }
3921
+ #${id} .dbg-section:last-child { border-bottom: none; }
3922
+ #${id} .dbg-section-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; cursor: pointer; user-select: none; transition: background 0.15s; }
3923
+ #${id} .dbg-section-header:hover { background: ${c.bgSecondary}; }
3924
+ #${id} .dbg-section-title { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: ${c.textSecondary}; }
3925
+ #${id} .dbg-section-title svg { width: 14px; height: 14px; opacity: 0.7; }
3926
+ #${id} .dbg-section-meta { font-size: 11px; color: ${c.textMuted}; font-family: 'SF Mono', Monaco, monospace; }
3927
+ #${id} .dbg-section-content { display: none; padding: 12px 16px; background: ${c.bgSecondary}; }
3928
+ #${id} .dbg-section.open .dbg-section-content { display: block; }
3929
+ #${id} .dbg-section .dbg-chevron { transition: transform 0.2s; color: ${c.textMuted}; }
3930
+ #${id} .dbg-section.open .dbg-chevron { transform: rotate(90deg); }
3931
+ #${id} .dbg-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid ${c.border}; }
3932
+ #${id} .dbg-row:last-child { border-bottom: none; }
3933
+ #${id} .dbg-label { color: ${c.textSecondary}; font-size: 12px; }
3934
+ #${id} .dbg-value { color: ${c.text}; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; text-align: right; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3935
+ #${id} .dbg-bar { height: 3px; background: ${c.bgTertiary}; border-radius: 2px; margin-top: 4px; overflow: hidden; }
3936
+ #${id} .dbg-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s ease; }
3937
+ #${id} .dbg-bar-fill.lexer { background: ${c.info}; }
3938
+ #${id} .dbg-bar-fill.parser { background: ${c.warning}; }
3939
+ #${id} .dbg-bar-fill.render { background: ${c.success}; }
3940
+ #${id} .dbg-templates { display: flex; flex-direction: column; gap: 6px; }
3941
+ #${id} .dbg-template { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: ${c.bg}; border-radius: 6px; font-size: 12px; }
3942
+ #${id} .dbg-template-icon { width: 16px; height: 16px; color: ${c.textMuted}; flex-shrink: 0; }
3943
+ #${id} .dbg-template-name { color: ${c.text}; font-family: 'SF Mono', Monaco, monospace; }
3944
+ #${id} .dbg-template-tag { font-size: 10px; padding: 2px 6px; border-radius: 3px; font-weight: 500; text-transform: uppercase; }
3945
+ #${id} .dbg-template-tag.root { background: rgba(59,130,246,0.15); color: ${c.accent}; }
3946
+ #${id} .dbg-template-tag.extends { background: rgba(168,85,247,0.15); color: #a855f7; }
3947
+ #${id} .dbg-template-tag.include { background: rgba(34,197,94,0.15); color: ${c.success}; }
3948
+ #${id} .dbg-ctx-grid { display: flex; flex-direction: column; gap: 4px; }
3949
+ #${id} .dbg-ctx-item { background: ${c.bg}; border-radius: 6px; overflow: hidden; }
3950
+ #${id} .dbg-ctx-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; cursor: default; }
3951
+ #${id} .dbg-ctx-row.expandable { cursor: pointer; }
3952
+ #${id} .dbg-ctx-row.expandable:hover { background: ${c.bgTertiary}; }
3953
+ #${id} .dbg-ctx-key { display: flex; align-items: center; gap: 6px; }
3954
+ #${id} .dbg-ctx-arrow { width: 12px; height: 12px; color: ${c.textMuted}; transition: transform 0.15s; flex-shrink: 0; }
3955
+ #${id} .dbg-ctx-item.open > .dbg-ctx-row .dbg-ctx-arrow { transform: rotate(90deg); }
3956
+ #${id} .dbg-ctx-name { color: ${c.text}; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; }
3957
+ #${id} .dbg-ctx-type { font-size: 10px; color: ${c.accent}; background: rgba(59,130,246,0.1); padding: 1px 5px; border-radius: 3px; }
3958
+ #${id} .dbg-ctx-preview { color: ${c.textSecondary}; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3959
+ #${id} .dbg-ctx-children { display: none; padding-left: 16px; border-left: 1px solid ${c.border}; margin-left: 10px; }
3960
+ #${id} .dbg-ctx-item.open > .dbg-ctx-children { display: block; }
3961
+ #${id} .dbg-ctx-children .dbg-ctx-item { background: transparent; }
3962
+ #${id} .dbg-ctx-children .dbg-ctx-row { padding: 4px 8px; }
3963
+ #${id} .dbg-filters { display: flex; flex-wrap: wrap; gap: 6px; }
3964
+ #${id} .dbg-filter { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; background: ${c.bg}; border-radius: 5px; font-size: 12px; font-family: 'SF Mono', Monaco, monospace; color: ${c.text}; }
3965
+ #${id} .dbg-filter-count { font-size: 10px; color: ${c.accent}; font-weight: 600; }
3966
+ #${id} .dbg-cache { display: flex; gap: 16px; }
3967
+ #${id} .dbg-cache-stat { flex: 1; padding: 12px; background: ${c.bg}; border-radius: 6px; text-align: center; }
3968
+ #${id} .dbg-cache-num { font-size: 24px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; }
3969
+ #${id} .dbg-cache-num.hit { color: ${c.success}; }
3970
+ #${id} .dbg-cache-num.miss { color: ${c.error}; }
3971
+ #${id} .dbg-cache-label { font-size: 11px; color: ${c.textMuted}; margin-top: 4px; }
3972
+ #${id} .dbg-warnings { display: flex; flex-direction: column; gap: 6px; }
3973
+ #${id} .dbg-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(234,179,8,0.1); border-radius: 6px; border-left: 3px solid ${c.warning}; }
3974
+ #${id} .dbg-warning-icon { color: ${c.warning}; flex-shrink: 0; margin-top: 1px; }
3975
+ #${id} .dbg-warning-text { color: ${c.text}; font-size: 12px; }
3976
+ `;
3977
+ }
3978
+ function getPosition(pos) {
3979
+ switch (pos) {
3980
+ case "bottom-left":
3981
+ return "bottom: 16px; left: 16px;";
3982
+ case "top-right":
3983
+ return "top: 16px; right: 16px;";
3984
+ case "top-left":
3985
+ return "top: 16px; left: 16px;";
3986
+ default:
3987
+ return "bottom: 16px; right: 16px;";
3988
+ }
3989
+ }
3990
+ var icons = {
3991
+ logo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`,
3992
+ close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
3993
+ chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
3994
+ perf: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>`,
3995
+ template: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>`,
3996
+ context: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>`,
3997
+ filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
3998
+ cache: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1010 10H12V2z"/><path d="M12 2a10 10 0 00-8.66 15"/></svg>`,
3999
+ warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
4000
+ file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg>`
4001
+ };
4002
+ function generateToggle(id, data, c) {
4003
+ const time2 = (data.totalTime || 0).toFixed(1);
4004
+ return `
4005
+ <button class="dbg-toggle" onclick="document.querySelector('#${id} .dbg-panel').classList.add('open');this.style.display='none'">
4006
+ ${icons.logo}
4007
+ <span>Binja</span>
4008
+ <span class="dbg-time">${time2}ms</span>
4009
+ </button>`;
4010
+ }
4011
+ function generatePanel(id, data, c, opts) {
4012
+ const time2 = (data.totalTime || 0).toFixed(2);
4013
+ const mode = data.mode === "aot" ? "AOT" : "Runtime";
4014
+ return `
4015
+ <div class="dbg-panel">
4016
+ <div class="dbg-header">
4017
+ <div class="dbg-logo">${icons.logo} Binja Debugger</div>
4018
+ <div class="dbg-meta">
4019
+ <span class="dbg-badge mode">${mode}</span>
4020
+ <span class="dbg-badge time">${time2}ms</span>
4021
+ <button class="dbg-close" onclick="document.querySelector('#${id} .dbg-panel').classList.remove('open');document.querySelector('#${id} .dbg-toggle').style.display='inline-flex'">${icons.close}</button>
4022
+ </div>
4023
+ </div>
4024
+ <div class="dbg-body">
4025
+ ${generatePerfSection(data)}
4026
+ ${generateTemplatesSection(data)}
4027
+ ${generateContextSection(data)}
4028
+ ${generateFiltersSection(data)}
4029
+ ${generateCacheSection(data)}
4030
+ ${generateWarningsSection(data)}
4031
+ </div>
4032
+ </div>`;
4033
+ }
4034
+ function generatePerfSection(data) {
4035
+ const total = data.totalTime || 0.01;
4036
+ const lexer = data.lexerTime || 0;
4037
+ const parser = data.parserTime || 0;
4038
+ const render = data.renderTime || 0;
4039
+ return `
4040
+ <div class="dbg-section open">
4041
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4042
+ <span class="dbg-section-title">${icons.perf} Performance</span>
4043
+ <span class="dbg-chevron">${icons.chevron}</span>
4044
+ </div>
4045
+ <div class="dbg-section-content">
4046
+ <div class="dbg-row">
4047
+ <span class="dbg-label">Lexer</span>
4048
+ <span class="dbg-value">${lexer.toFixed(2)}ms</span>
4049
+ </div>
4050
+ <div class="dbg-bar"><div class="dbg-bar-fill lexer" style="width:${lexer / total * 100}%"></div></div>
4051
+ <div class="dbg-row">
4052
+ <span class="dbg-label">Parser</span>
4053
+ <span class="dbg-value">${parser.toFixed(2)}ms</span>
4054
+ </div>
4055
+ <div class="dbg-bar"><div class="dbg-bar-fill parser" style="width:${parser / total * 100}%"></div></div>
4056
+ <div class="dbg-row">
4057
+ <span class="dbg-label">Render</span>
4058
+ <span class="dbg-value">${render.toFixed(2)}ms</span>
4059
+ </div>
4060
+ <div class="dbg-bar"><div class="dbg-bar-fill render" style="width:${render / total * 100}%"></div></div>
4061
+ <div class="dbg-row" style="margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.1)">
4062
+ <span class="dbg-label" style="font-weight:600">Total</span>
4063
+ <span class="dbg-value" style="font-weight:600">${total.toFixed(2)}ms</span>
4064
+ </div>
4065
+ </div>
4066
+ </div>`;
4067
+ }
4068
+ function generateTemplatesSection(data) {
4069
+ if (data.templateChain.length === 0)
4070
+ return "";
4071
+ const templates = data.templateChain.map((t) => `
4072
+ <div class="dbg-template">
4073
+ ${icons.file}
4074
+ <span class="dbg-template-name">${t.name}</span>
4075
+ <span class="dbg-template-tag ${t.type}">${t.type}</span>
4076
+ </div>
4077
+ `).join("");
4078
+ return `
4079
+ <div class="dbg-section">
4080
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4081
+ <span class="dbg-section-title">${icons.template} Templates</span>
4082
+ <span class="dbg-section-meta">${data.templateChain.length}</span>
4083
+ <span class="dbg-chevron">${icons.chevron}</span>
4084
+ </div>
4085
+ <div class="dbg-section-content">
4086
+ <div class="dbg-templates">${templates}</div>
4087
+ </div>
4088
+ </div>`;
4089
+ }
4090
+ function generateContextSection(data) {
4091
+ const keys = Object.keys(data.contextSnapshot);
4092
+ if (keys.length === 0)
4093
+ return "";
4094
+ const items = keys.map((key) => renderContextValue(key, data.contextSnapshot[key])).join("");
4095
+ return `
4096
+ <div class="dbg-section">
4097
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4098
+ <span class="dbg-section-title">${icons.context} Context</span>
4099
+ <span class="dbg-section-meta">${keys.length} vars</span>
4100
+ <span class="dbg-chevron">${icons.chevron}</span>
4101
+ </div>
4102
+ <div class="dbg-section-content">
4103
+ <div class="dbg-ctx-grid">${items}</div>
4104
+ </div>
4105
+ </div>`;
4106
+ }
4107
+ function renderContextValue(key, ctx) {
4108
+ const arrow = ctx.expandable ? `<svg class="dbg-ctx-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>` : "";
4109
+ const expandableClass = ctx.expandable ? "expandable" : "";
4110
+ const onClick = ctx.expandable ? `onclick="this.parentElement.classList.toggle('open')"` : "";
4111
+ let children = "";
4112
+ if (ctx.expandable && ctx.children) {
4113
+ const childItems = Object.entries(ctx.children).map(([k, v]) => renderContextValue(k, v)).join("");
4114
+ children = `<div class="dbg-ctx-children">${childItems}</div>`;
4115
+ }
4116
+ return `
4117
+ <div class="dbg-ctx-item">
4118
+ <div class="dbg-ctx-row ${expandableClass}" ${onClick}>
4119
+ <div class="dbg-ctx-key">
4120
+ ${arrow}
4121
+ <span class="dbg-ctx-name">${escapeHtml(key)}</span>
4122
+ <span class="dbg-ctx-type">${ctx.type}</span>
4123
+ </div>
4124
+ <span class="dbg-ctx-preview">${escapeHtml(ctx.preview)}</span>
4125
+ </div>
4126
+ ${children}
4127
+ </div>`;
4128
+ }
4129
+ function generateFiltersSection(data) {
4130
+ const filters = Array.from(data.filtersUsed.entries());
4131
+ if (filters.length === 0)
4132
+ return "";
4133
+ const items = filters.map(([name, count]) => `<span class="dbg-filter">${name}<span class="dbg-filter-count">\xD7${count}</span></span>`).join("");
4134
+ return `
4135
+ <div class="dbg-section">
4136
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4137
+ <span class="dbg-section-title">${icons.filter} Filters</span>
4138
+ <span class="dbg-section-meta">${filters.length}</span>
4139
+ <span class="dbg-chevron">${icons.chevron}</span>
4140
+ </div>
4141
+ <div class="dbg-section-content">
4142
+ <div class="dbg-filters">${items}</div>
4143
+ </div>
4144
+ </div>`;
4145
+ }
4146
+ function generateCacheSection(data) {
4147
+ const total = data.cacheHits + data.cacheMisses;
4148
+ if (total === 0)
4149
+ return "";
4150
+ return `
4151
+ <div class="dbg-section">
4152
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4153
+ <span class="dbg-section-title">${icons.cache} Cache</span>
4154
+ <span class="dbg-section-meta">${(data.cacheHits / total * 100).toFixed(0)}% hit</span>
4155
+ <span class="dbg-chevron">${icons.chevron}</span>
4156
+ </div>
4157
+ <div class="dbg-section-content">
4158
+ <div class="dbg-cache">
4159
+ <div class="dbg-cache-stat">
4160
+ <div class="dbg-cache-num hit">${data.cacheHits}</div>
4161
+ <div class="dbg-cache-label">Cache Hits</div>
4162
+ </div>
4163
+ <div class="dbg-cache-stat">
4164
+ <div class="dbg-cache-num miss">${data.cacheMisses}</div>
4165
+ <div class="dbg-cache-label">Cache Misses</div>
4166
+ </div>
4167
+ </div>
4168
+ </div>
4169
+ </div>`;
4170
+ }
4171
+ function generateWarningsSection(data) {
4172
+ if (data.warnings.length === 0)
4173
+ return "";
4174
+ const items = data.warnings.map((w) => `
4175
+ <div class="dbg-warning">
4176
+ ${icons.warning}
4177
+ <span class="dbg-warning-text">${escapeHtml(w)}</span>
4178
+ </div>
4179
+ `).join("");
4180
+ return `
4181
+ <div class="dbg-section open">
4182
+ <div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
4183
+ <span class="dbg-section-title">${icons.warning} Warnings</span>
4184
+ <span class="dbg-section-meta" style="color:#eab308">${data.warnings.length}</span>
4185
+ <span class="dbg-chevron">${icons.chevron}</span>
4186
+ </div>
4187
+ <div class="dbg-section-content">
4188
+ <div class="dbg-warnings">${items}</div>
4189
+ </div>
4190
+ </div>`;
4191
+ }
4192
+ function generateScript(id) {
4193
+ return `
4194
+ (function(){
4195
+ var panel = document.getElementById('${id}');
4196
+ if (!panel) return;
4197
+ var header = panel.querySelector('.dbg-header');
4198
+ if (!header) return;
4199
+ var isDrag = false, startX, startY, startL, startT;
4200
+ header.style.cursor = 'grab';
4201
+ header.onmousedown = function(e) {
4202
+ if (e.target.closest('.dbg-close')) return;
4203
+ isDrag = true;
4204
+ header.style.cursor = 'grabbing';
4205
+ startX = e.clientX;
4206
+ startY = e.clientY;
4207
+ var r = panel.getBoundingClientRect();
4208
+ startL = r.left;
4209
+ startT = r.top;
4210
+ panel.style.right = 'auto';
4211
+ panel.style.bottom = 'auto';
4212
+ panel.style.left = startL + 'px';
4213
+ panel.style.top = startT + 'px';
4214
+ };
4215
+ document.onmousemove = function(e) {
4216
+ if (!isDrag) return;
4217
+ panel.style.left = (startL + e.clientX - startX) + 'px';
4218
+ panel.style.top = (startT + e.clientY - startY) + 'px';
4219
+ };
4220
+ document.onmouseup = function() {
4221
+ isDrag = false;
4222
+ header.style.cursor = 'grab';
4223
+ };
4224
+ })();`;
4225
+ }
4226
+ function escapeHtml(str) {
4227
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4228
+ }
4229
+
4230
+ // src/debug/index.ts
4231
+ async function renderWithDebug(env, templateName, context = {}, options = {}) {
4232
+ const collector = startDebugCollection();
4233
+ collector.captureContext(context);
4234
+ collector.addTemplate(templateName, "root");
4235
+ collector.setMode("runtime");
4236
+ collector.startRender();
4237
+ const html = await env.render(templateName, context);
4238
+ collector.endRender();
4239
+ const data = endDebugCollection();
4240
+ if (options.htmlOnly !== false) {
4241
+ const isHtml = html.includes("<html") || html.includes("<body") || html.includes("<!DOCTYPE");
4242
+ if (!isHtml) {
4243
+ return html;
4244
+ }
4245
+ }
4246
+ const panel = generateDebugPanel(data, options.panel);
4247
+ if (html.includes("</body>")) {
4248
+ return html.replace("</body>", `${panel}</body>`);
4249
+ }
4250
+ return html + panel;
4251
+ }
4252
+ async function renderStringWithDebug(env, source, context = {}, options = {}) {
4253
+ const collector = startDebugCollection();
4254
+ collector.captureContext(context);
4255
+ collector.setMode("runtime");
4256
+ collector.startRender();
4257
+ const html = await env.renderString(source, context);
4258
+ collector.endRender();
4259
+ const data = endDebugCollection();
4260
+ if (options.htmlOnly !== false) {
4261
+ const isHtml = html.includes("<html") || html.includes("<body");
4262
+ if (!isHtml) {
4263
+ return html;
4264
+ }
4265
+ }
4266
+ const panel = generateDebugPanel(data, options.panel);
4267
+ if (html.includes("</body>")) {
4268
+ return html.replace("</body>", `${panel}</body>`);
4269
+ }
4270
+ return html + panel;
4271
+ }
4272
+ function createDebugRenderer(env, options = {}) {
4273
+ return {
4274
+ async render(templateName, context = {}) {
4275
+ return renderWithDebug(env, templateName, context, options);
4276
+ },
4277
+ async renderString(source, context = {}) {
4278
+ return renderStringWithDebug(env, source, context, options);
4279
+ }
4280
+ };
4281
+ }
4282
+ function debugMiddleware(env, options = {}) {
4283
+ return {
4284
+ hono() {
4285
+ return async (c, next) => {
4286
+ await next();
4287
+ const contentType = c.res.headers.get("content-type") || "";
4288
+ if (!contentType.includes("text/html"))
4289
+ return;
4290
+ const body = await c.res.text();
4291
+ const collector = startDebugCollection();
4292
+ collector.captureContext({});
4293
+ collector.setMode("runtime");
4294
+ collector.endRender();
4295
+ const data = endDebugCollection();
4296
+ const panel = generateDebugPanel(data, options.panel);
4297
+ const newBody = body.includes("</body>") ? body.replace("</body>", `${panel}</body>`) : body + panel;
4298
+ c.res = new Response(newBody, {
4299
+ status: c.res.status,
4300
+ headers: c.res.headers
4301
+ });
4302
+ };
4303
+ },
4304
+ express() {
4305
+ return (req, res, next) => {
4306
+ const originalSend = res.send.bind(res);
4307
+ res.send = (body) => {
4308
+ const contentType = res.get("Content-Type") || "";
4309
+ if (!contentType.includes("text/html") || typeof body !== "string") {
4310
+ return originalSend(body);
4311
+ }
4312
+ const collector = startDebugCollection();
4313
+ collector.captureContext({});
4314
+ collector.setMode("runtime");
4315
+ collector.endRender();
4316
+ const data = endDebugCollection();
4317
+ const panel = generateDebugPanel(data, options.panel);
4318
+ const newBody = body.includes("</body>") ? body.replace("</body>", `${panel}</body>`) : body + panel;
4319
+ return originalSend(newBody);
4320
+ };
4321
+ next();
4322
+ };
4323
+ }
4324
+ };
4325
+ }
4326
+
2998
4327
  // src/index.ts
2999
4328
  import * as path from "path";
3000
4329
  var URL_PARAM_REGEX = /<[^>]+>|:[a-zA-Z_]+|\(\?P<[^>]+>\[[^\]]+\]\)/g;
@@ -3013,7 +4342,9 @@ class Environment {
3013
4342
  urlResolver: options.urlResolver ?? this.defaultUrlResolver.bind(this),
3014
4343
  staticResolver: options.staticResolver ?? this.defaultStaticResolver.bind(this),
3015
4344
  cache: options.cache ?? true,
3016
- extensions: options.extensions ?? [".html", ".jinja", ".jinja2", ""]
4345
+ extensions: options.extensions ?? [".html", ".jinja", ".jinja2", ""],
4346
+ debug: options.debug ?? false,
4347
+ debugOptions: options.debugOptions ?? {}
3017
4348
  };
3018
4349
  this.runtime = new Runtime({
3019
4350
  autoescape: this.options.autoescape,
@@ -3025,13 +4356,60 @@ class Environment {
3025
4356
  });
3026
4357
  }
3027
4358
  async render(templateName, context = {}) {
4359
+ if (this.options.debug) {
4360
+ return this.renderWithDebug(templateName, context);
4361
+ }
3028
4362
  const ast = await this.loadTemplate(templateName);
3029
4363
  return this.runtime.render(ast, context);
3030
4364
  }
3031
4365
  async renderString(source, context = {}) {
4366
+ if (this.options.debug) {
4367
+ return this.renderStringWithDebug(source, context);
4368
+ }
3032
4369
  const ast = this.compile(source);
3033
4370
  return this.runtime.render(ast, context);
3034
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
+ }
3035
4413
  compile(source) {
3036
4414
  const lexer = new Lexer(source);
3037
4415
  const tokens = lexer.tokenize();
@@ -3135,6 +4513,70 @@ function compile(source, options = {}) {
3135
4513
  const ast = parser.parse();
3136
4514
  return compileToFunction(ast, options);
3137
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
+ }
3138
4580
  function compileToCode(source, options = {}) {
3139
4581
  const lexer = new Lexer(source);
3140
4582
  const tokens = lexer.tokenize();
@@ -3143,9 +4585,18 @@ function compileToCode(source, options = {}) {
3143
4585
  return compileToString(ast, options);
3144
4586
  }
3145
4587
  export {
4588
+ renderWithDebug,
4589
+ renderStringWithDebug,
3146
4590
  render,
4591
+ generateDebugPanel,
4592
+ flattenTemplate,
4593
+ debugMiddleware,
4594
+ createDebugRenderer,
4595
+ compileWithInheritanceToCode,
4596
+ compileWithInheritance,
3147
4597
  compileToCode,
3148
4598
  compile,
4599
+ canFlatten,
3149
4600
  builtinTests,
3150
4601
  builtinFilters,
3151
4602
  TokenType,