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/README.md +97 -2
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2316 -0
- package/dist/compiler/flattener.d.ts +36 -0
- package/dist/compiler/flattener.d.ts.map +1 -0
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/debug/collector.d.ts +73 -0
- package/dist/debug/collector.d.ts.map +1 -0
- package/dist/debug/index.d.ts +54 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/panel.d.ts +16 -0
- package/dist/debug/panel.d.ts.map +1 -0
- package/dist/filters/index.d.ts +20 -0
- package/dist/filters/index.d.ts.map +1 -1
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1455 -4
- package/dist/lexer/index.d.ts +2 -0
- package/dist/lexer/index.d.ts.map +1 -1
- package/package.json +12 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
2
4
|
// src/lexer/tokens.ts
|
|
3
5
|
var TokenType;
|
|
4
6
|
((TokenType2) => {
|
|
@@ -95,6 +97,14 @@ class Lexer {
|
|
|
95
97
|
const wsControl = this.peek() === "-";
|
|
96
98
|
if (wsControl)
|
|
97
99
|
this.advance();
|
|
100
|
+
const savedPos = this.state.pos;
|
|
101
|
+
this.skipWhitespace();
|
|
102
|
+
if (this.checkWord("raw") || this.checkWord("verbatim")) {
|
|
103
|
+
const tagName = this.checkWord("raw") ? "raw" : "verbatim";
|
|
104
|
+
this.scanRawBlock(tagName, wsControl);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.state.pos = savedPos;
|
|
98
108
|
this.addToken("BLOCK_START" /* BLOCK_START */, this.blockStart + (wsControl ? "-" : ""));
|
|
99
109
|
this.scanExpression(this.blockEnd, "BLOCK_END" /* BLOCK_END */);
|
|
100
110
|
return;
|
|
@@ -105,6 +115,73 @@ class Lexer {
|
|
|
105
115
|
}
|
|
106
116
|
this.scanText();
|
|
107
117
|
}
|
|
118
|
+
checkWord(word) {
|
|
119
|
+
const start = this.state.pos;
|
|
120
|
+
for (let i = 0;i < word.length; i++) {
|
|
121
|
+
if (this.state.source[start + i]?.toLowerCase() !== word[i]) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const nextChar = this.state.source[start + word.length];
|
|
126
|
+
return !nextChar || !this.isAlphaNumeric(nextChar);
|
|
127
|
+
}
|
|
128
|
+
scanRawBlock(tagName, wsControl) {
|
|
129
|
+
const startLine = this.state.line;
|
|
130
|
+
const startColumn = this.state.column;
|
|
131
|
+
for (let i = 0;i < tagName.length; i++) {
|
|
132
|
+
this.advance();
|
|
133
|
+
}
|
|
134
|
+
this.skipWhitespace();
|
|
135
|
+
if (this.peek() === "-")
|
|
136
|
+
this.advance();
|
|
137
|
+
if (!this.match(this.blockEnd)) {
|
|
138
|
+
throw new Error(`Expected ${this.blockEnd} after ${tagName} at line ${this.state.line}`);
|
|
139
|
+
}
|
|
140
|
+
const endTag = `end${tagName}`;
|
|
141
|
+
const contentStart = this.state.pos;
|
|
142
|
+
while (!this.isAtEnd()) {
|
|
143
|
+
if (this.check(this.blockStart)) {
|
|
144
|
+
const savedPos = this.state.pos;
|
|
145
|
+
const savedLine = this.state.line;
|
|
146
|
+
const savedColumn = this.state.column;
|
|
147
|
+
this.match(this.blockStart);
|
|
148
|
+
if (this.peek() === "-")
|
|
149
|
+
this.advance();
|
|
150
|
+
this.skipWhitespace();
|
|
151
|
+
if (this.checkWord(endTag)) {
|
|
152
|
+
const content = this.state.source.slice(contentStart, savedPos);
|
|
153
|
+
if (content.length > 0) {
|
|
154
|
+
this.state.tokens.push({
|
|
155
|
+
type: "TEXT" /* TEXT */,
|
|
156
|
+
value: content,
|
|
157
|
+
line: startLine,
|
|
158
|
+
column: startColumn
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
for (let i = 0;i < endTag.length; i++) {
|
|
162
|
+
this.advance();
|
|
163
|
+
}
|
|
164
|
+
this.skipWhitespace();
|
|
165
|
+
if (this.peek() === "-")
|
|
166
|
+
this.advance();
|
|
167
|
+
if (!this.match(this.blockEnd)) {
|
|
168
|
+
throw new Error(`Expected ${this.blockEnd} after ${endTag} at line ${this.state.line}`);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.state.pos = savedPos;
|
|
173
|
+
this.state.line = savedLine;
|
|
174
|
+
this.state.column = savedColumn;
|
|
175
|
+
}
|
|
176
|
+
if (this.peek() === `
|
|
177
|
+
`) {
|
|
178
|
+
this.state.line++;
|
|
179
|
+
this.state.column = 0;
|
|
180
|
+
}
|
|
181
|
+
this.advance();
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`Unclosed ${tagName} block starting at line ${startLine}`);
|
|
184
|
+
}
|
|
108
185
|
scanText() {
|
|
109
186
|
const start = this.state.pos;
|
|
110
187
|
const startLine = this.state.line;
|
|
@@ -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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
// src/debug/index.ts
|
|
4231
|
+
async function renderWithDebug(env, templateName, context = {}, options = {}) {
|
|
4232
|
+
const collector = startDebugCollection();
|
|
4233
|
+
collector.captureContext(context);
|
|
4234
|
+
collector.addTemplate(templateName, "root");
|
|
4235
|
+
collector.setMode("runtime");
|
|
4236
|
+
collector.startRender();
|
|
4237
|
+
const html = await env.render(templateName, context);
|
|
4238
|
+
collector.endRender();
|
|
4239
|
+
const data = endDebugCollection();
|
|
4240
|
+
if (options.htmlOnly !== false) {
|
|
4241
|
+
const isHtml = html.includes("<html") || html.includes("<body") || html.includes("<!DOCTYPE");
|
|
4242
|
+
if (!isHtml) {
|
|
4243
|
+
return html;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
const panel = generateDebugPanel(data, options.panel);
|
|
4247
|
+
if (html.includes("</body>")) {
|
|
4248
|
+
return html.replace("</body>", `${panel}</body>`);
|
|
4249
|
+
}
|
|
4250
|
+
return html + panel;
|
|
4251
|
+
}
|
|
4252
|
+
async function renderStringWithDebug(env, source, context = {}, options = {}) {
|
|
4253
|
+
const collector = startDebugCollection();
|
|
4254
|
+
collector.captureContext(context);
|
|
4255
|
+
collector.setMode("runtime");
|
|
4256
|
+
collector.startRender();
|
|
4257
|
+
const html = await env.renderString(source, context);
|
|
4258
|
+
collector.endRender();
|
|
4259
|
+
const data = endDebugCollection();
|
|
4260
|
+
if (options.htmlOnly !== false) {
|
|
4261
|
+
const isHtml = html.includes("<html") || html.includes("<body");
|
|
4262
|
+
if (!isHtml) {
|
|
4263
|
+
return html;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
const panel = generateDebugPanel(data, options.panel);
|
|
4267
|
+
if (html.includes("</body>")) {
|
|
4268
|
+
return html.replace("</body>", `${panel}</body>`);
|
|
4269
|
+
}
|
|
4270
|
+
return html + panel;
|
|
4271
|
+
}
|
|
4272
|
+
function createDebugRenderer(env, options = {}) {
|
|
4273
|
+
return {
|
|
4274
|
+
async render(templateName, context = {}) {
|
|
4275
|
+
return renderWithDebug(env, templateName, context, options);
|
|
4276
|
+
},
|
|
4277
|
+
async renderString(source, context = {}) {
|
|
4278
|
+
return renderStringWithDebug(env, source, context, options);
|
|
4279
|
+
}
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
function debugMiddleware(env, options = {}) {
|
|
4283
|
+
return {
|
|
4284
|
+
hono() {
|
|
4285
|
+
return async (c, next) => {
|
|
4286
|
+
await next();
|
|
4287
|
+
const contentType = c.res.headers.get("content-type") || "";
|
|
4288
|
+
if (!contentType.includes("text/html"))
|
|
4289
|
+
return;
|
|
4290
|
+
const body = await c.res.text();
|
|
4291
|
+
const collector = startDebugCollection();
|
|
4292
|
+
collector.captureContext({});
|
|
4293
|
+
collector.setMode("runtime");
|
|
4294
|
+
collector.endRender();
|
|
4295
|
+
const data = endDebugCollection();
|
|
4296
|
+
const panel = generateDebugPanel(data, options.panel);
|
|
4297
|
+
const newBody = body.includes("</body>") ? body.replace("</body>", `${panel}</body>`) : body + panel;
|
|
4298
|
+
c.res = new Response(newBody, {
|
|
4299
|
+
status: c.res.status,
|
|
4300
|
+
headers: c.res.headers
|
|
4301
|
+
});
|
|
4302
|
+
};
|
|
4303
|
+
},
|
|
4304
|
+
express() {
|
|
4305
|
+
return (req, res, next) => {
|
|
4306
|
+
const originalSend = res.send.bind(res);
|
|
4307
|
+
res.send = (body) => {
|
|
4308
|
+
const contentType = res.get("Content-Type") || "";
|
|
4309
|
+
if (!contentType.includes("text/html") || typeof body !== "string") {
|
|
4310
|
+
return originalSend(body);
|
|
4311
|
+
}
|
|
4312
|
+
const collector = startDebugCollection();
|
|
4313
|
+
collector.captureContext({});
|
|
4314
|
+
collector.setMode("runtime");
|
|
4315
|
+
collector.endRender();
|
|
4316
|
+
const data = endDebugCollection();
|
|
4317
|
+
const panel = generateDebugPanel(data, options.panel);
|
|
4318
|
+
const newBody = body.includes("</body>") ? body.replace("</body>", `${panel}</body>`) : body + panel;
|
|
4319
|
+
return originalSend(newBody);
|
|
4320
|
+
};
|
|
4321
|
+
next();
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
|
|
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,
|