binja 0.7.0 → 0.7.2

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/cli.js CHANGED
@@ -1,2361 +1,26 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
-
4
- // src/cli.ts
5
- import * as fs from "fs";
6
- import * as path from "path";
7
-
8
- // src/lexer/tokens.ts
9
- var KEYWORDS = {
10
- and: "AND" /* AND */,
11
- or: "OR" /* OR */,
12
- not: "NOT" /* NOT */,
13
- true: "NAME" /* NAME */,
14
- false: "NAME" /* NAME */,
15
- True: "NAME" /* NAME */,
16
- False: "NAME" /* NAME */,
17
- None: "NAME" /* NAME */,
18
- none: "NAME" /* NAME */,
19
- is: "NAME" /* NAME */,
20
- in: "NAME" /* NAME */
21
- };
22
-
23
- // src/errors/index.ts
24
- var colors = {
25
- red: "\x1B[31m",
26
- yellow: "\x1B[33m",
27
- cyan: "\x1B[36m",
28
- gray: "\x1B[90m",
29
- bold: "\x1B[1m",
30
- dim: "\x1B[2m",
31
- reset: "\x1B[0m"
32
- };
33
- var useColors = process.stdout?.isTTY !== false;
34
- function c(color, text) {
35
- return useColors ? `${colors[color]}${text}${colors.reset}` : text;
36
- }
37
- class TemplateSyntaxError extends Error {
38
- line;
39
- column;
40
- source;
41
- templateName;
42
- suggestion;
43
- constructor(message, options) {
44
- const formatted = formatError("TemplateSyntaxError", message, options);
45
- super(formatted);
46
- this.name = "TemplateSyntaxError";
47
- this.line = options.line;
48
- this.column = options.column;
49
- this.source = options.source;
50
- this.templateName = options.templateName;
51
- this.suggestion = options.suggestion;
52
- }
53
- }
54
- function formatError(type, message, options) {
55
- const parts = [];
56
- const location = options.templateName ? `${options.templateName}:${options.line}:${options.column}` : `line ${options.line}, column ${options.column}`;
57
- parts.push(`${c("red", c("bold", type))}: ${message} at ${c("cyan", location)}`);
58
- if (options.source) {
59
- parts.push("");
60
- parts.push(generateSnippet(options.source, options.line, options.column));
61
- }
62
- if (options.suggestion) {
63
- parts.push("");
64
- parts.push(`${c("yellow", "Did you mean")}: ${c("cyan", options.suggestion)}?`);
65
- }
66
- if (options.availableOptions && options.availableOptions.length > 0) {
67
- parts.push("");
68
- const truncated = options.availableOptions.slice(0, 8);
69
- const more = options.availableOptions.length > 8 ? ` ${c("gray", `... and ${options.availableOptions.length - 8} more`)}` : "";
70
- parts.push(`${c("gray", "Available")}: ${truncated.join(", ")}${more}`);
71
- }
72
- return parts.join(`
73
- `);
74
- }
75
- function generateSnippet(source, errorLine, errorColumn) {
76
- const lines = source.split(`
77
- `);
78
- const parts = [];
79
- const startLine = Math.max(1, errorLine - 2);
80
- const endLine = Math.min(lines.length, errorLine + 1);
81
- const gutterWidth = String(endLine).length;
82
- for (let i = startLine;i <= endLine; i++) {
83
- const lineContent = lines[i - 1] || "";
84
- const lineNum = String(i).padStart(gutterWidth, " ");
85
- const isErrorLine = i === errorLine;
86
- if (isErrorLine) {
87
- parts.push(`${c("red", " \u2192")} ${c("gray", lineNum)} ${c("dim", "\u2502")} ${lineContent}`);
88
- const caretPadding = " ".repeat(gutterWidth + 4 + Math.max(0, errorColumn - 1));
89
- const caret = c("red", "^");
90
- parts.push(`${caretPadding}${caret}`);
91
- } else {
92
- parts.push(` ${c("gray", lineNum)} ${c("dim", "\u2502")} ${c("gray", lineContent)}`);
93
- }
94
- }
95
- return parts.join(`
96
- `);
97
- }
98
-
99
- // src/lexer/index.ts
100
- class Lexer {
101
- state;
102
- variableStart;
103
- variableEnd;
104
- blockStart;
105
- blockEnd;
106
- commentStart;
107
- commentEnd;
108
- constructor(source, options = {}) {
109
- this.state = {
110
- source,
111
- pos: 0,
112
- line: 1,
113
- column: 1,
114
- tokens: []
115
- };
116
- this.variableStart = options.variableStart ?? "{{";
117
- this.variableEnd = options.variableEnd ?? "}}";
118
- this.blockStart = options.blockStart ?? "{%";
119
- this.blockEnd = options.blockEnd ?? "%}";
120
- this.commentStart = options.commentStart ?? "{#";
121
- this.commentEnd = options.commentEnd ?? "#}";
122
- }
123
- tokenize() {
124
- while (!this.isAtEnd()) {
125
- this.scanToken();
126
- }
127
- this.addToken("EOF" /* EOF */, "");
128
- return this.state.tokens;
129
- }
130
- scanToken() {
131
- if (this.match(this.variableStart)) {
132
- this.addToken("VARIABLE_START" /* VARIABLE_START */, this.variableStart);
133
- this.scanExpression(this.variableEnd, "VARIABLE_END" /* VARIABLE_END */);
134
- return;
135
- }
136
- if (this.match(this.blockStart)) {
137
- const wsControl = this.peek() === "-";
138
- if (wsControl)
139
- this.advance();
140
- const savedPos = this.state.pos;
141
- this.skipWhitespace();
142
- if (this.checkWord("raw") || this.checkWord("verbatim")) {
143
- const tagName = this.checkWord("raw") ? "raw" : "verbatim";
144
- this.scanRawBlock(tagName, wsControl);
145
- return;
146
- }
147
- this.state.pos = savedPos;
148
- this.addToken("BLOCK_START" /* BLOCK_START */, this.blockStart + (wsControl ? "-" : ""));
149
- this.scanExpression(this.blockEnd, "BLOCK_END" /* BLOCK_END */);
150
- return;
151
- }
152
- if (this.match(this.commentStart)) {
153
- this.scanComment();
154
- return;
155
- }
156
- this.scanText();
157
- }
158
- checkWord(word) {
159
- const start = this.state.pos;
160
- for (let i = 0;i < word.length; i++) {
161
- if (this.state.source[start + i]?.toLowerCase() !== word[i]) {
162
- return false;
163
- }
164
- }
165
- const nextChar = this.state.source[start + word.length];
166
- return !nextChar || !this.isAlphaNumeric(nextChar);
167
- }
168
- scanRawBlock(tagName, wsControl) {
169
- const startLine = this.state.line;
170
- const startColumn = this.state.column;
171
- for (let i = 0;i < tagName.length; i++) {
172
- this.advance();
173
- }
174
- this.skipWhitespace();
175
- if (this.peek() === "-")
176
- this.advance();
177
- if (!this.match(this.blockEnd)) {
178
- throw new TemplateSyntaxError(`Expected '${this.blockEnd}' after '${tagName}'`, {
179
- line: this.state.line,
180
- column: this.state.column,
181
- source: this.state.source
182
- });
183
- }
184
- const endTag = `end${tagName}`;
185
- const contentStart = this.state.pos;
186
- while (!this.isAtEnd()) {
187
- if (this.check(this.blockStart)) {
188
- const savedPos = this.state.pos;
189
- const savedLine = this.state.line;
190
- const savedColumn = this.state.column;
191
- this.match(this.blockStart);
192
- if (this.peek() === "-")
193
- this.advance();
194
- this.skipWhitespace();
195
- if (this.checkWord(endTag)) {
196
- const content = this.state.source.slice(contentStart, savedPos);
197
- if (content.length > 0) {
198
- this.state.tokens.push({
199
- type: "TEXT" /* TEXT */,
200
- value: content,
201
- line: startLine,
202
- column: startColumn
203
- });
204
- }
205
- for (let i = 0;i < endTag.length; i++) {
206
- this.advance();
207
- }
208
- this.skipWhitespace();
209
- if (this.peek() === "-")
210
- this.advance();
211
- if (!this.match(this.blockEnd)) {
212
- throw new TemplateSyntaxError(`Expected '${this.blockEnd}' after '${endTag}'`, {
213
- line: this.state.line,
214
- column: this.state.column,
215
- source: this.state.source
216
- });
217
- }
218
- return;
219
- }
220
- this.state.pos = savedPos;
221
- this.state.line = savedLine;
222
- this.state.column = savedColumn;
223
- }
224
- if (this.peek() === `
225
- `) {
226
- this.state.line++;
227
- this.state.column = 0;
228
- }
229
- this.advance();
230
- }
231
- throw new TemplateSyntaxError(`Unclosed '${tagName}' block`, {
232
- line: startLine,
233
- column: startColumn,
234
- source: this.state.source,
235
- suggestion: `Add {% end${tagName} %} to close the block`
236
- });
237
- }
238
- scanText() {
239
- const start = this.state.pos;
240
- const startLine = this.state.line;
241
- const startColumn = this.state.column;
242
- while (!this.isAtEnd()) {
243
- if (this.check(this.variableStart) || this.check(this.blockStart) || this.check(this.commentStart)) {
244
- break;
245
- }
246
- if (this.peek() === `
247
- `) {
248
- this.state.line++;
249
- this.state.column = 0;
250
- }
251
- this.advance();
252
- }
253
- if (this.state.pos > start) {
254
- const text = this.state.source.slice(start, this.state.pos);
255
- this.state.tokens.push({
256
- type: "TEXT" /* TEXT */,
257
- value: text,
258
- line: startLine,
259
- column: startColumn
260
- });
261
- }
262
- }
263
- scanExpression(endDelimiter, endTokenType) {
264
- this.skipWhitespace();
265
- while (!this.isAtEnd()) {
266
- this.skipWhitespace();
267
- if (this.peek() === "-" && this.check(endDelimiter, 1)) {
268
- this.advance();
269
- }
270
- if (this.match(endDelimiter)) {
271
- this.addToken(endTokenType, endDelimiter);
272
- return;
273
- }
274
- this.scanExpressionToken();
275
- }
276
- throw new TemplateSyntaxError(`Unclosed template tag`, {
277
- line: this.state.line,
278
- column: this.state.column,
279
- source: this.state.source,
280
- suggestion: `Add closing delimiter '${endDelimiter}'`
281
- });
282
- }
283
- scanExpressionToken() {
284
- this.skipWhitespace();
285
- if (this.isAtEnd())
286
- return;
287
- const c2 = this.peek();
288
- if (c2 === '"' || c2 === "'") {
289
- this.scanString(c2);
290
- return;
291
- }
292
- if (this.isDigit(c2)) {
293
- this.scanNumber();
294
- return;
295
- }
296
- if (this.isAlpha(c2) || c2 === "_") {
297
- this.scanIdentifier();
298
- return;
299
- }
300
- this.scanOperator();
301
- }
302
- scanString(quote) {
303
- this.advance();
304
- const start = this.state.pos;
305
- while (!this.isAtEnd() && this.peek() !== quote) {
306
- if (this.peek() === "\\" && this.peekNext() === quote) {
307
- this.advance();
308
- }
309
- if (this.peek() === `
310
- `) {
311
- this.state.line++;
312
- this.state.column = 0;
313
- }
314
- this.advance();
315
- }
316
- if (this.isAtEnd()) {
317
- throw new TemplateSyntaxError(`Unterminated string literal`, {
318
- line: this.state.line,
319
- column: this.state.column,
320
- source: this.state.source,
321
- suggestion: `Add closing quote '${quote}'`
322
- });
323
- }
324
- const value = this.state.source.slice(start, this.state.pos);
325
- this.advance();
326
- this.addToken("STRING" /* STRING */, value);
327
- }
328
- scanNumber() {
329
- const start = this.state.pos;
330
- while (this.isDigit(this.peek())) {
331
- this.advance();
332
- }
333
- if (this.peek() === "." && this.isDigit(this.peekNext())) {
334
- this.advance();
335
- while (this.isDigit(this.peek())) {
336
- this.advance();
337
- }
338
- }
339
- const value = this.state.source.slice(start, this.state.pos);
340
- this.addToken("NUMBER" /* NUMBER */, value);
341
- }
342
- scanIdentifier() {
343
- const start = this.state.pos;
344
- while (this.isAlphaNumeric(this.peek()) || this.peek() === "_") {
345
- this.advance();
346
- }
347
- const value = this.state.source.slice(start, this.state.pos);
348
- const type = KEYWORDS[value] ?? "NAME" /* NAME */;
349
- this.addToken(type, value);
350
- }
351
- scanOperator() {
352
- const c2 = this.advance();
353
- switch (c2) {
354
- case ".":
355
- this.addToken("DOT" /* DOT */, c2);
356
- break;
357
- case ",":
358
- this.addToken("COMMA" /* COMMA */, c2);
359
- break;
360
- case ":":
361
- this.addToken("COLON" /* COLON */, c2);
362
- break;
363
- case "|":
364
- this.addToken("PIPE" /* PIPE */, c2);
365
- break;
366
- case "(":
367
- this.addToken("LPAREN" /* LPAREN */, c2);
368
- break;
369
- case ")":
370
- this.addToken("RPAREN" /* RPAREN */, c2);
371
- break;
372
- case "[":
373
- this.addToken("LBRACKET" /* LBRACKET */, c2);
374
- break;
375
- case "]":
376
- this.addToken("RBRACKET" /* RBRACKET */, c2);
377
- break;
378
- case "{":
379
- this.addToken("LBRACE" /* LBRACE */, c2);
380
- break;
381
- case "}":
382
- this.addToken("RBRACE" /* RBRACE */, c2);
383
- break;
384
- case "+":
385
- this.addToken("ADD" /* ADD */, c2);
386
- break;
387
- case "-":
388
- this.addToken("SUB" /* SUB */, c2);
389
- break;
390
- case "*":
391
- this.addToken("MUL" /* MUL */, c2);
392
- break;
393
- case "/":
394
- this.addToken("DIV" /* DIV */, c2);
395
- break;
396
- case "%":
397
- this.addToken("MOD" /* MOD */, c2);
398
- break;
399
- case "~":
400
- this.addToken("TILDE" /* TILDE */, c2);
401
- break;
402
- case "=":
403
- if (this.match("=")) {
404
- this.addToken("EQ" /* EQ */, "==");
405
- } else {
406
- this.addToken("ASSIGN" /* ASSIGN */, "=");
407
- }
408
- break;
409
- case "!":
410
- if (this.match("=")) {
411
- this.addToken("NE" /* NE */, "!=");
412
- } else {
413
- throw new TemplateSyntaxError(`Unexpected character '!'`, {
414
- line: this.state.line,
415
- column: this.state.column - 1,
416
- source: this.state.source,
417
- suggestion: `Use '!=' for not-equal comparison or 'not' for negation`
418
- });
419
- }
420
- break;
421
- case "<":
422
- if (this.match("=")) {
423
- this.addToken("LE" /* LE */, "<=");
424
- } else {
425
- this.addToken("LT" /* LT */, "<");
426
- }
427
- break;
428
- case ">":
429
- if (this.match("=")) {
430
- this.addToken("GE" /* GE */, ">=");
431
- } else {
432
- this.addToken("GT" /* GT */, ">");
433
- }
434
- break;
435
- default:
436
- if (!this.isWhitespace(c2)) {
437
- throw new TemplateSyntaxError(`Unexpected character '${c2}'`, {
438
- line: this.state.line,
439
- column: this.state.column - 1,
440
- source: this.state.source
441
- });
442
- }
443
- }
444
- }
445
- scanComment() {
446
- while (!this.isAtEnd() && !this.check(this.commentEnd)) {
447
- if (this.peek() === `
448
- `) {
449
- this.state.line++;
450
- this.state.column = 0;
451
- }
452
- this.advance();
453
- }
454
- if (!this.isAtEnd()) {
455
- this.match(this.commentEnd);
456
- }
457
- }
458
- isAtEnd() {
459
- return this.state.pos >= this.state.source.length;
460
- }
461
- peek() {
462
- if (this.isAtEnd())
463
- return "\x00";
464
- return this.state.source[this.state.pos];
465
- }
466
- peekNext() {
467
- if (this.state.pos + 1 >= this.state.source.length)
468
- return "\x00";
469
- return this.state.source[this.state.pos + 1];
470
- }
471
- advance() {
472
- const c2 = this.state.source[this.state.pos];
473
- this.state.pos++;
474
- this.state.column++;
475
- return c2;
476
- }
477
- match(expected, offset = 0) {
478
- const source = this.state.source;
479
- const start = this.state.pos + offset;
480
- const len = expected.length;
481
- if (start + len > source.length)
482
- return false;
483
- for (let i = 0;i < len; i++) {
484
- if (source[start + i] !== expected[i])
485
- return false;
486
- }
487
- if (offset === 0) {
488
- this.state.pos += len;
489
- this.state.column += len;
490
- }
491
- return true;
492
- }
493
- check(expected, offset = 0) {
494
- const source = this.state.source;
495
- const start = this.state.pos + offset;
496
- const len = expected.length;
497
- if (start + len > source.length)
498
- return false;
499
- for (let i = 0;i < len; i++) {
500
- if (source[start + i] !== expected[i])
501
- return false;
502
- }
503
- return true;
504
- }
505
- skipWhitespace() {
506
- while (!this.isAtEnd() && this.isWhitespace(this.peek())) {
507
- if (this.peek() === `
508
- `) {
509
- this.state.line++;
510
- this.state.column = 0;
511
- }
512
- this.advance();
513
- }
514
- }
515
- isWhitespace(c2) {
516
- return c2 === " " || c2 === "\t" || c2 === `
517
- ` || c2 === "\r";
518
- }
519
- isDigit(c2) {
520
- const code = c2.charCodeAt(0);
521
- return code >= 48 && code <= 57;
522
- }
523
- isAlpha(c2) {
524
- const code = c2.charCodeAt(0);
525
- return code >= 97 && code <= 122 || code >= 65 && code <= 90;
526
- }
527
- isAlphaNumeric(c2) {
528
- const code = c2.charCodeAt(0);
529
- return code >= 48 && code <= 57 || code >= 97 && code <= 122 || code >= 65 && code <= 90;
530
- }
531
- addToken(type, value) {
532
- this.state.tokens.push({
533
- type,
534
- value,
535
- line: this.state.line,
536
- column: this.state.column - value.length
537
- });
538
- }
539
- }
540
-
541
- // src/parser/index.ts
542
- class Parser {
543
- tokens;
544
- current = 0;
545
- source;
546
- constructor(tokens, source) {
547
- this.tokens = tokens;
548
- this.source = source;
549
- }
550
- parse() {
551
- const body = [];
552
- while (!this.isAtEnd()) {
553
- const node = this.parseStatement();
554
- if (node)
555
- body.push(node);
556
- }
557
- return {
558
- type: "Template",
559
- body,
560
- line: 1,
561
- column: 1
562
- };
563
- }
564
- parseStatement() {
565
- const token = this.peek();
566
- switch (token.type) {
567
- case "TEXT" /* TEXT */:
568
- return this.parseText();
569
- case "VARIABLE_START" /* VARIABLE_START */:
570
- return this.parseOutput();
571
- case "BLOCK_START" /* BLOCK_START */:
572
- return this.parseBlock();
573
- case "EOF" /* EOF */:
574
- return null;
575
- default:
576
- this.advance();
577
- return null;
578
- }
579
- }
580
- parseText() {
581
- const token = this.advance();
582
- return {
583
- type: "Text",
584
- value: token.value,
585
- line: token.line,
586
- column: token.column
587
- };
588
- }
589
- parseOutput() {
590
- const start = this.advance();
591
- const expression = this.parseExpression();
592
- this.expect("VARIABLE_END" /* VARIABLE_END */);
593
- return {
594
- type: "Output",
595
- expression,
596
- line: start.line,
597
- column: start.column
598
- };
599
- }
600
- parseBlock() {
601
- const start = this.advance();
602
- const tagName = this.expect("NAME" /* NAME */);
603
- switch (tagName.value) {
604
- case "if":
605
- return this.parseIf(start);
606
- case "for":
607
- return this.parseFor(start);
608
- case "block":
609
- return this.parseBlockTag(start);
610
- case "extends":
611
- return this.parseExtends(start);
612
- case "include":
613
- return this.parseInclude(start);
614
- case "set":
615
- return this.parseSet(start);
616
- case "with":
617
- return this.parseWith(start);
618
- case "load":
619
- return this.parseLoad(start);
620
- case "url":
621
- return this.parseUrl(start);
622
- case "static":
623
- return this.parseStatic(start);
624
- case "now":
625
- return this.parseNow(start);
626
- case "comment":
627
- return this.parseComment(start);
628
- case "spaceless":
629
- case "autoescape":
630
- case "verbatim":
631
- return this.parseSimpleBlock(start, tagName.value);
632
- case "cycle":
633
- return this.parseCycle(start);
634
- case "firstof":
635
- return this.parseFirstof(start);
636
- case "ifchanged":
637
- return this.parseIfchanged(start);
638
- case "regroup":
639
- return this.parseRegroup(start);
640
- case "widthratio":
641
- return this.parseWidthratio(start);
642
- case "lorem":
643
- return this.parseLorem(start);
644
- case "csrf_token":
645
- return this.parseCsrfToken(start);
646
- case "debug":
647
- return this.parseDebug(start);
648
- case "templatetag":
649
- return this.parseTemplatetag(start);
650
- case "ifequal":
651
- return this.parseIfequal(start, false);
652
- case "ifnotequal":
653
- return this.parseIfequal(start, true);
654
- default:
655
- this.skipToBlockEnd();
656
- return null;
657
- }
658
- }
659
- parseIf(start) {
660
- const test = this.parseExpression();
661
- this.expect("BLOCK_END" /* BLOCK_END */);
662
- const body = [];
663
- const elifs = [];
664
- let else_ = [];
665
- while (!this.isAtEnd()) {
666
- if (this.checkBlockTag("elif") || this.checkBlockTag("else") || this.checkBlockTag("endif")) {
667
- break;
668
- }
669
- const node = this.parseStatement();
670
- if (node)
671
- body.push(node);
672
- }
673
- while (this.checkBlockTag("elif")) {
674
- this.advance();
675
- this.advance();
676
- const elifTest = this.parseExpression();
677
- this.expect("BLOCK_END" /* BLOCK_END */);
678
- const elifBody = [];
679
- while (!this.isAtEnd()) {
680
- if (this.checkBlockTag("elif") || this.checkBlockTag("else") || this.checkBlockTag("endif")) {
681
- break;
682
- }
683
- const node = this.parseStatement();
684
- if (node)
685
- elifBody.push(node);
686
- }
687
- elifs.push({ test: elifTest, body: elifBody });
688
- }
689
- if (this.checkBlockTag("else")) {
690
- this.advance();
691
- this.advance();
692
- this.expect("BLOCK_END" /* BLOCK_END */);
693
- while (!this.isAtEnd()) {
694
- if (this.checkBlockTag("endif"))
695
- break;
696
- const node = this.parseStatement();
697
- if (node)
698
- else_.push(node);
699
- }
700
- }
701
- this.expectBlockTag("endif");
702
- return {
703
- type: "If",
704
- test,
705
- body,
706
- elifs,
707
- else_,
708
- line: start.line,
709
- column: start.column
710
- };
711
- }
712
- parseFor(start) {
713
- let target;
714
- const firstName = this.expect("NAME" /* NAME */).value;
715
- if (this.check("COMMA" /* COMMA */)) {
716
- const targets = [firstName];
717
- while (this.match("COMMA" /* COMMA */)) {
718
- targets.push(this.expect("NAME" /* NAME */).value);
719
- }
720
- target = targets;
721
- } else {
722
- target = firstName;
723
- }
724
- const inToken = this.expect("NAME" /* NAME */);
725
- if (inToken.value !== "in") {
726
- throw this.error(`Expected 'in' in for loop, got '${inToken.value}'`);
727
- }
728
- const iter = this.parseExpression();
729
- const recursive = this.check("NAME" /* NAME */) && this.peek().value === "recursive";
730
- if (recursive)
731
- this.advance();
732
- this.expect("BLOCK_END" /* BLOCK_END */);
733
- const body = [];
734
- let else_ = [];
735
- while (!this.isAtEnd()) {
736
- if (this.checkBlockTag("empty") || this.checkBlockTag("else") || this.checkBlockTag("endfor")) {
737
- break;
738
- }
739
- const node = this.parseStatement();
740
- if (node)
741
- body.push(node);
742
- }
743
- if (this.checkBlockTag("empty") || this.checkBlockTag("else")) {
744
- this.advance();
745
- this.advance();
746
- this.expect("BLOCK_END" /* BLOCK_END */);
747
- while (!this.isAtEnd()) {
748
- if (this.checkBlockTag("endfor"))
749
- break;
750
- const node = this.parseStatement();
751
- if (node)
752
- else_.push(node);
753
- }
754
- }
755
- this.expectBlockTag("endfor");
756
- return {
757
- type: "For",
758
- target,
759
- iter,
760
- body,
761
- else_,
762
- recursive,
763
- line: start.line,
764
- column: start.column
765
- };
766
- }
767
- parseBlockTag(start) {
768
- const name = this.expect("NAME" /* NAME */).value;
769
- const scoped = this.check("NAME" /* NAME */) && this.peek().value === "scoped";
770
- if (scoped)
771
- this.advance();
772
- this.expect("BLOCK_END" /* BLOCK_END */);
773
- const body = [];
774
- while (!this.isAtEnd()) {
775
- if (this.checkBlockTag("endblock"))
776
- break;
777
- const node = this.parseStatement();
778
- if (node)
779
- body.push(node);
780
- }
781
- this.advance();
782
- this.advance();
783
- if (this.check("NAME" /* NAME */))
784
- this.advance();
785
- this.expect("BLOCK_END" /* BLOCK_END */);
786
- return {
787
- type: "Block",
788
- name,
789
- body,
790
- scoped,
791
- line: start.line,
792
- column: start.column
793
- };
794
- }
795
- parseExtends(start) {
796
- const template = this.parseExpression();
797
- this.expect("BLOCK_END" /* BLOCK_END */);
798
- return {
799
- type: "Extends",
800
- template,
801
- line: start.line,
802
- column: start.column
803
- };
804
- }
805
- parseInclude(start) {
806
- const template = this.parseExpression();
807
- let context = null;
808
- let only = false;
809
- let ignoreMissing = false;
810
- while (this.check("NAME" /* NAME */)) {
811
- const modifier = this.peek().value;
812
- if (modifier === "ignore" && this.peekNext()?.value === "missing") {
813
- this.advance();
814
- this.advance();
815
- ignoreMissing = true;
816
- } else if (modifier === "with") {
817
- this.advance();
818
- context = this.parseKeywordArgs();
819
- } else if (modifier === "only") {
820
- this.advance();
821
- only = true;
822
- } else if (modifier === "without") {
823
- this.advance();
824
- if (this.check("NAME" /* NAME */) && this.peek().value === "context") {
825
- this.advance();
826
- only = true;
827
- }
828
- } else {
829
- break;
830
- }
831
- }
832
- this.expect("BLOCK_END" /* BLOCK_END */);
833
- return {
834
- type: "Include",
835
- template,
836
- context,
837
- only,
838
- ignoreMissing,
839
- line: start.line,
840
- column: start.column
841
- };
842
- }
843
- parseSet(start) {
844
- const target = this.expect("NAME" /* NAME */).value;
845
- this.expect("ASSIGN" /* ASSIGN */);
846
- const value = this.parseExpression();
847
- this.expect("BLOCK_END" /* BLOCK_END */);
848
- return {
849
- type: "Set",
850
- target,
851
- value,
852
- line: start.line,
853
- column: start.column
854
- };
855
- }
856
- parseWith(start) {
857
- const assignments = [];
858
- do {
859
- const target = this.expect("NAME" /* NAME */).value;
860
- this.expect("ASSIGN" /* ASSIGN */);
861
- const value = this.parseExpression();
862
- assignments.push({ target, value });
863
- } while (this.match("COMMA" /* COMMA */) || this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */);
864
- this.expect("BLOCK_END" /* BLOCK_END */);
865
- const body = [];
866
- while (!this.isAtEnd()) {
867
- if (this.checkBlockTag("endwith"))
868
- break;
869
- const node = this.parseStatement();
870
- if (node)
871
- body.push(node);
872
- }
873
- this.expectBlockTag("endwith");
874
- return {
875
- type: "With",
876
- assignments,
877
- body,
878
- line: start.line,
879
- column: start.column
880
- };
881
- }
882
- parseLoad(start) {
883
- const names = [];
884
- while (this.check("NAME" /* NAME */)) {
885
- names.push(this.advance().value);
886
- }
887
- this.expect("BLOCK_END" /* BLOCK_END */);
888
- return {
889
- type: "Load",
890
- names,
891
- line: start.line,
892
- column: start.column
893
- };
894
- }
895
- parseUrl(start) {
896
- const name = this.parseExpression();
897
- const args = [];
898
- const kwargs = {};
899
- let asVar = null;
900
- while (!this.check("BLOCK_END" /* BLOCK_END */)) {
901
- if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
902
- this.advance();
903
- asVar = this.expect("NAME" /* NAME */).value;
904
- break;
905
- }
906
- if (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
907
- const key = this.advance().value;
908
- this.advance();
909
- kwargs[key] = this.parseExpression();
910
- } else {
911
- args.push(this.parseExpression());
912
- }
913
- }
914
- this.expect("BLOCK_END" /* BLOCK_END */);
915
- return {
916
- type: "Url",
917
- name,
918
- args,
919
- kwargs,
920
- asVar,
921
- line: start.line,
922
- column: start.column
923
- };
924
- }
925
- parseStatic(start) {
926
- const path = this.parseExpression();
927
- let asVar = null;
928
- if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
929
- this.advance();
930
- asVar = this.expect("NAME" /* NAME */).value;
931
- }
932
- this.expect("BLOCK_END" /* BLOCK_END */);
933
- return {
934
- type: "Static",
935
- path,
936
- asVar,
937
- line: start.line,
938
- column: start.column
939
- };
940
- }
941
- parseNow(start) {
942
- const format = this.parseExpression();
943
- let asVar = null;
944
- if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
945
- this.advance();
946
- asVar = this.expect("NAME" /* NAME */).value;
947
- }
948
- this.expect("BLOCK_END" /* BLOCK_END */);
949
- return {
950
- type: "Now",
951
- format,
952
- asVar,
953
- line: start.line,
954
- column: start.column
955
- };
956
- }
957
- parseComment(_start) {
958
- this.expect("BLOCK_END" /* BLOCK_END */);
959
- while (!this.isAtEnd()) {
960
- if (this.checkBlockTag("endcomment"))
961
- break;
962
- this.advance();
963
- }
964
- this.expectBlockTag("endcomment");
965
- return null;
966
- }
967
- parseSimpleBlock(_start, tagName) {
968
- this.skipToBlockEnd();
969
- const endTag = `end${tagName}`;
970
- while (!this.isAtEnd()) {
971
- if (this.checkBlockTag(endTag))
972
- break;
973
- this.advance();
974
- }
975
- if (this.checkBlockTag(endTag)) {
976
- this.advance();
977
- this.advance();
978
- this.expect("BLOCK_END" /* BLOCK_END */);
979
- }
980
- return null;
981
- }
982
- parseCycle(start) {
983
- const values = [];
984
- let asVar = null;
985
- let silent = false;
986
- while (!this.check("BLOCK_END" /* BLOCK_END */)) {
987
- if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
988
- this.advance();
989
- asVar = this.expect("NAME" /* NAME */).value;
990
- if (this.check("NAME" /* NAME */) && this.peek().value === "silent") {
991
- this.advance();
992
- silent = true;
993
- }
994
- break;
995
- }
996
- values.push(this.parseExpression());
997
- }
998
- this.expect("BLOCK_END" /* BLOCK_END */);
999
- return {
1000
- type: "Cycle",
1001
- values,
1002
- asVar,
1003
- silent,
1004
- line: start.line,
1005
- column: start.column
1006
- };
1007
- }
1008
- parseFirstof(start) {
1009
- const values = [];
1010
- let fallback = null;
1011
- let asVar = null;
1012
- while (!this.check("BLOCK_END" /* BLOCK_END */)) {
1013
- if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
1014
- this.advance();
1015
- asVar = this.expect("NAME" /* NAME */).value;
1016
- break;
1017
- }
1018
- values.push(this.parseExpression());
1019
- }
1020
- if (values.length > 0) {
1021
- const last = values[values.length - 1];
1022
- if (last.type === "Literal" && typeof last.value === "string") {
1023
- fallback = values.pop();
1024
- }
1025
- }
1026
- this.expect("BLOCK_END" /* BLOCK_END */);
1027
- return {
1028
- type: "Firstof",
1029
- values,
1030
- fallback,
1031
- asVar,
1032
- line: start.line,
1033
- column: start.column
1034
- };
1035
- }
1036
- parseIfchanged(start) {
1037
- const values = [];
1038
- while (!this.check("BLOCK_END" /* BLOCK_END */)) {
1039
- values.push(this.parseExpression());
1040
- }
1041
- this.expect("BLOCK_END" /* BLOCK_END */);
1042
- const body = [];
1043
- let else_ = [];
1044
- while (!this.isAtEnd()) {
1045
- if (this.checkBlockTag("else") || this.checkBlockTag("endifchanged"))
1046
- break;
1047
- const node = this.parseStatement();
1048
- if (node)
1049
- body.push(node);
1050
- }
1051
- if (this.checkBlockTag("else")) {
1052
- this.advance();
1053
- this.advance();
1054
- this.expect("BLOCK_END" /* BLOCK_END */);
1055
- while (!this.isAtEnd()) {
1056
- if (this.checkBlockTag("endifchanged"))
1057
- break;
1058
- const node = this.parseStatement();
1059
- if (node)
1060
- else_.push(node);
1061
- }
1062
- }
1063
- this.expectBlockTag("endifchanged");
1064
- return {
1065
- type: "Ifchanged",
1066
- values,
1067
- body,
1068
- else_,
1069
- line: start.line,
1070
- column: start.column
1071
- };
1072
- }
1073
- parseRegroup(start) {
1074
- const target = this.parseExpression();
1075
- this.expectName("by");
1076
- const key = this.expect("NAME" /* NAME */).value;
1077
- this.expectName("as");
1078
- const asVar = this.expect("NAME" /* NAME */).value;
1079
- this.expect("BLOCK_END" /* BLOCK_END */);
1080
- return {
1081
- type: "Regroup",
1082
- target,
1083
- key,
1084
- asVar,
1085
- line: start.line,
1086
- column: start.column
1087
- };
1088
- }
1089
- parseWidthratio(start) {
1090
- const value = this.parseExpression();
1091
- const maxValue = this.parseExpression();
1092
- const maxWidth = this.parseExpression();
1093
- let asVar = null;
1094
- if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
1095
- this.advance();
1096
- asVar = this.expect("NAME" /* NAME */).value;
1097
- }
1098
- this.expect("BLOCK_END" /* BLOCK_END */);
1099
- return {
1100
- type: "Widthratio",
1101
- value,
1102
- maxValue,
1103
- maxWidth,
1104
- asVar,
1105
- line: start.line,
1106
- column: start.column
1107
- };
1108
- }
1109
- parseLorem(start) {
1110
- let count = null;
1111
- let method = "p";
1112
- let random = false;
1113
- if (this.check("NUMBER" /* NUMBER */)) {
1114
- count = {
1115
- type: "Literal",
1116
- value: parseInt(this.advance().value, 10),
1117
- line: start.line,
1118
- column: start.column
1119
- };
1120
- }
1121
- if (this.check("NAME" /* NAME */)) {
1122
- const m = this.peek().value.toLowerCase();
1123
- if (m === "w" || m === "p" || m === "b") {
1124
- method = m;
1125
- this.advance();
1126
- }
1127
- }
1128
- if (this.check("NAME" /* NAME */) && this.peek().value === "random") {
1129
- random = true;
1130
- this.advance();
1131
- }
1132
- this.expect("BLOCK_END" /* BLOCK_END */);
1133
- return {
1134
- type: "Lorem",
1135
- count,
1136
- method,
1137
- random,
1138
- line: start.line,
1139
- column: start.column
1140
- };
1141
- }
1142
- parseCsrfToken(start) {
1143
- this.expect("BLOCK_END" /* BLOCK_END */);
1144
- return {
1145
- type: "CsrfToken",
1146
- line: start.line,
1147
- column: start.column
1148
- };
1149
- }
1150
- parseDebug(start) {
1151
- this.expect("BLOCK_END" /* BLOCK_END */);
1152
- return {
1153
- type: "Debug",
1154
- line: start.line,
1155
- column: start.column
1156
- };
1157
- }
1158
- parseTemplatetag(start) {
1159
- const tagType = this.expect("NAME" /* NAME */).value;
1160
- this.expect("BLOCK_END" /* BLOCK_END */);
1161
- return {
1162
- type: "Templatetag",
1163
- tagType,
1164
- line: start.line,
1165
- column: start.column
1166
- };
1167
- }
1168
- parseIfequal(start, negated) {
1169
- const left = this.parseExpression();
1170
- const right = this.parseExpression();
1171
- this.expect("BLOCK_END" /* BLOCK_END */);
1172
- const test = {
1173
- type: "Compare",
1174
- left,
1175
- ops: [{ operator: negated ? "!=" : "==", right }],
1176
- line: start.line,
1177
- column: start.column
1178
- };
1179
- const body = [];
1180
- let else_ = [];
1181
- const endTag = negated ? "endifnotequal" : "endifequal";
1182
- while (!this.isAtEnd()) {
1183
- if (this.checkBlockTag("else") || this.checkBlockTag(endTag))
1184
- break;
1185
- const node = this.parseStatement();
1186
- if (node)
1187
- body.push(node);
1188
- }
1189
- if (this.checkBlockTag("else")) {
1190
- this.advance();
1191
- this.advance();
1192
- this.expect("BLOCK_END" /* BLOCK_END */);
1193
- while (!this.isAtEnd()) {
1194
- if (this.checkBlockTag(endTag))
1195
- break;
1196
- const node = this.parseStatement();
1197
- if (node)
1198
- else_.push(node);
1199
- }
1200
- }
1201
- this.expectBlockTag(endTag);
1202
- return {
1203
- type: "If",
1204
- test,
1205
- body,
1206
- elifs: [],
1207
- else_,
1208
- line: start.line,
1209
- column: start.column
1210
- };
1211
- }
1212
- parseExpression() {
1213
- return this.parseConditional();
1214
- }
1215
- parseConditional() {
1216
- let expr = this.parseOr();
1217
- if (this.check("NAME" /* NAME */) && this.peek().value === "if") {
1218
- this.advance();
1219
- const test = this.parseOr();
1220
- this.expectName("else");
1221
- const falseExpr = this.parseConditional();
1222
- expr = {
1223
- type: "Conditional",
1224
- test,
1225
- trueExpr: expr,
1226
- falseExpr,
1227
- line: expr.line,
1228
- column: expr.column
1229
- };
1230
- }
1231
- return expr;
1232
- }
1233
- parseOr() {
1234
- let left = this.parseAnd();
1235
- while (this.check("OR" /* OR */) || this.check("NAME" /* NAME */) && this.peek().value === "or") {
1236
- this.advance();
1237
- const right = this.parseAnd();
1238
- left = {
1239
- type: "BinaryOp",
1240
- operator: "or",
1241
- left,
1242
- right,
1243
- line: left.line,
1244
- column: left.column
1245
- };
1246
- }
1247
- return left;
1248
- }
1249
- parseAnd() {
1250
- let left = this.parseNot();
1251
- while (this.check("AND" /* AND */) || this.check("NAME" /* NAME */) && this.peek().value === "and") {
1252
- this.advance();
1253
- const right = this.parseNot();
1254
- left = {
1255
- type: "BinaryOp",
1256
- operator: "and",
1257
- left,
1258
- right,
1259
- line: left.line,
1260
- column: left.column
1261
- };
1262
- }
1263
- return left;
1264
- }
1265
- parseNot() {
1266
- if (this.check("NOT" /* NOT */) || this.check("NAME" /* NAME */) && this.peek().value === "not") {
1267
- const op = this.advance();
1268
- const operand = this.parseNot();
1269
- return {
1270
- type: "UnaryOp",
1271
- operator: "not",
1272
- operand,
1273
- line: op.line,
1274
- column: op.column
1275
- };
1276
- }
1277
- return this.parseCompare();
1278
- }
1279
- parseCompare() {
1280
- let left = this.parseAddSub();
1281
- const ops = [];
1282
- while (true) {
1283
- let operator = null;
1284
- if (this.match("EQ" /* EQ */))
1285
- operator = "==";
1286
- else if (this.match("NE" /* NE */))
1287
- operator = "!=";
1288
- else if (this.match("LT" /* LT */))
1289
- operator = "<";
1290
- else if (this.match("GT" /* GT */))
1291
- operator = ">";
1292
- else if (this.match("LE" /* LE */))
1293
- operator = "<=";
1294
- else if (this.match("GE" /* GE */))
1295
- operator = ">=";
1296
- else if (this.check("NAME" /* NAME */)) {
1297
- const name = this.peek().value;
1298
- if (name === "in") {
1299
- this.advance();
1300
- operator = "in";
1301
- } else if (name === "not" && this.peekNext()?.value === "in") {
1302
- this.advance();
1303
- this.advance();
1304
- operator = "not in";
1305
- } else if (name === "is") {
1306
- this.advance();
1307
- const negated = this.check("NOT" /* NOT */);
1308
- if (negated) {
1309
- this.advance();
1310
- }
1311
- const testToken = this.expect("NAME" /* NAME */);
1312
- const testName = testToken.value;
1313
- const args = [];
1314
- if (this.match("LPAREN" /* LPAREN */)) {
1315
- while (!this.check("RPAREN" /* RPAREN */)) {
1316
- args.push(this.parseExpression());
1317
- if (!this.check("RPAREN" /* RPAREN */)) {
1318
- this.expect("COMMA" /* COMMA */);
1319
- }
1320
- }
1321
- this.expect("RPAREN" /* RPAREN */);
1322
- }
1323
- left = {
1324
- type: "TestExpr",
1325
- node: left,
1326
- test: testName,
1327
- args,
1328
- negated,
1329
- line: left.line,
1330
- column: left.column
1331
- };
1332
- continue;
1333
- }
1334
- }
1335
- if (!operator)
1336
- break;
1337
- const right = this.parseAddSub();
1338
- ops.push({ operator, right });
1339
- }
1340
- if (ops.length === 0)
1341
- return left;
1342
- return {
1343
- type: "Compare",
1344
- left,
1345
- ops,
1346
- line: left.line,
1347
- column: left.column
1348
- };
1349
- }
1350
- parseAddSub() {
1351
- let left = this.parseMulDiv();
1352
- while (this.check("ADD" /* ADD */) || this.check("SUB" /* SUB */) || this.check("TILDE" /* TILDE */)) {
1353
- const op = this.advance();
1354
- const right = this.parseMulDiv();
1355
- left = {
1356
- type: "BinaryOp",
1357
- operator: op.value,
1358
- left,
1359
- right,
1360
- line: left.line,
1361
- column: left.column
1362
- };
1363
- }
1364
- return left;
1365
- }
1366
- parseMulDiv() {
1367
- let left = this.parseUnary();
1368
- while (this.check("MUL" /* MUL */) || this.check("DIV" /* DIV */) || this.check("MOD" /* MOD */)) {
1369
- const op = this.advance();
1370
- const right = this.parseUnary();
1371
- left = {
1372
- type: "BinaryOp",
1373
- operator: op.value,
1374
- left,
1375
- right,
1376
- line: left.line,
1377
- column: left.column
1378
- };
1379
- }
1380
- return left;
1381
- }
1382
- parseUnary() {
1383
- if (this.check("SUB" /* SUB */) || this.check("ADD" /* ADD */)) {
1384
- const op = this.advance();
1385
- const operand = this.parseUnary();
1386
- return {
1387
- type: "UnaryOp",
1388
- operator: op.value,
1389
- operand,
1390
- line: op.line,
1391
- column: op.column
1392
- };
1393
- }
1394
- return this.parseFilter();
1395
- }
1396
- parseFilter() {
1397
- let node = this.parsePostfix();
1398
- while (this.match("PIPE" /* PIPE */)) {
1399
- const filterName = this.expect("NAME" /* NAME */).value;
1400
- const args = [];
1401
- const kwargs = {};
1402
- if (this.match("COLON" /* COLON */)) {
1403
- if (this.check("SUB" /* SUB */) || this.check("ADD" /* ADD */)) {
1404
- const op = this.advance();
1405
- const operand = this.parsePostfix();
1406
- args.push({
1407
- type: "UnaryOp",
1408
- operator: op.value,
1409
- operand,
1410
- line: op.line,
1411
- column: op.column
1412
- });
1413
- } else {
1414
- args.push(this.parsePostfix());
1415
- }
1416
- } else if (this.match("LPAREN" /* LPAREN */)) {
1417
- while (!this.check("RPAREN" /* RPAREN */)) {
1418
- if (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
1419
- const key = this.advance().value;
1420
- this.advance();
1421
- kwargs[key] = this.parseExpression();
1422
- } else {
1423
- args.push(this.parseExpression());
1424
- }
1425
- if (!this.check("RPAREN" /* RPAREN */)) {
1426
- this.expect("COMMA" /* COMMA */);
1427
- }
1428
- }
1429
- this.expect("RPAREN" /* RPAREN */);
1430
- }
1431
- node = {
1432
- type: "FilterExpr",
1433
- node,
1434
- filter: filterName,
1435
- args,
1436
- kwargs,
1437
- line: node.line,
1438
- column: node.column
1439
- };
1440
- }
1441
- return node;
1442
- }
1443
- parsePostfix() {
1444
- let node = this.parsePrimary();
1445
- while (true) {
1446
- if (this.match("DOT" /* DOT */)) {
1447
- let attr;
1448
- if (this.check("NUMBER" /* NUMBER */)) {
1449
- attr = this.advance().value;
1450
- } else {
1451
- attr = this.expect("NAME" /* NAME */).value;
1452
- }
1453
- node = {
1454
- type: "GetAttr",
1455
- object: node,
1456
- attribute: attr,
1457
- line: node.line,
1458
- column: node.column
1459
- };
1460
- } else if (this.match("LBRACKET" /* LBRACKET */)) {
1461
- const index = this.parseExpression();
1462
- this.expect("RBRACKET" /* RBRACKET */);
1463
- node = {
1464
- type: "GetItem",
1465
- object: node,
1466
- index,
1467
- line: node.line,
1468
- column: node.column
1469
- };
1470
- } else if (this.match("LPAREN" /* LPAREN */)) {
1471
- const args = [];
1472
- const kwargs = {};
1473
- while (!this.check("RPAREN" /* RPAREN */)) {
1474
- if (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
1475
- const key = this.advance().value;
1476
- this.advance();
1477
- kwargs[key] = this.parseExpression();
1478
- } else {
1479
- args.push(this.parseExpression());
1480
- }
1481
- if (!this.check("RPAREN" /* RPAREN */)) {
1482
- this.expect("COMMA" /* COMMA */);
1483
- }
1484
- }
1485
- this.expect("RPAREN" /* RPAREN */);
1486
- node = {
1487
- type: "FunctionCall",
1488
- callee: node,
1489
- args,
1490
- kwargs,
1491
- line: node.line,
1492
- column: node.column
1493
- };
1494
- } else {
1495
- break;
1496
- }
1497
- }
1498
- return node;
1499
- }
1500
- parsePrimary() {
1501
- const token = this.peek();
1502
- if (this.match("STRING" /* STRING */)) {
1503
- return {
1504
- type: "Literal",
1505
- value: token.value,
1506
- line: token.line,
1507
- column: token.column
1508
- };
1509
- }
1510
- if (this.match("NUMBER" /* NUMBER */)) {
1511
- const value = token.value.includes(".") ? parseFloat(token.value) : parseInt(token.value, 10);
1512
- return {
1513
- type: "Literal",
1514
- value,
1515
- line: token.line,
1516
- column: token.column
1517
- };
1518
- }
1519
- if (this.check("NAME" /* NAME */)) {
1520
- const name = this.advance().value;
1521
- if (name === "true" || name === "True") {
1522
- return { type: "Literal", value: true, line: token.line, column: token.column };
1523
- }
1524
- if (name === "false" || name === "False") {
1525
- return { type: "Literal", value: false, line: token.line, column: token.column };
1526
- }
1527
- if (name === "none" || name === "None" || name === "null") {
1528
- return { type: "Literal", value: null, line: token.line, column: token.column };
1529
- }
1530
- return { type: "Name", name, line: token.line, column: token.column };
1531
- }
1532
- if (this.match("LPAREN" /* LPAREN */)) {
1533
- const expr = this.parseExpression();
1534
- this.expect("RPAREN" /* RPAREN */);
1535
- return expr;
1536
- }
1537
- if (this.match("LBRACKET" /* LBRACKET */)) {
1538
- const elements = [];
1539
- while (!this.check("RBRACKET" /* RBRACKET */)) {
1540
- elements.push(this.parseExpression());
1541
- if (!this.check("RBRACKET" /* RBRACKET */)) {
1542
- this.expect("COMMA" /* COMMA */);
1543
- }
1544
- }
1545
- this.expect("RBRACKET" /* RBRACKET */);
1546
- return { type: "Array", elements, line: token.line, column: token.column };
1547
- }
1548
- if (this.match("LBRACE" /* LBRACE */)) {
1549
- const pairs = [];
1550
- while (!this.check("RBRACE" /* RBRACE */)) {
1551
- const key = this.parseExpression();
1552
- this.expect("COLON" /* COLON */);
1553
- const value = this.parseExpression();
1554
- pairs.push({ key, value });
1555
- if (!this.check("RBRACE" /* RBRACE */)) {
1556
- this.expect("COMMA" /* COMMA */);
1557
- }
1558
- }
1559
- this.expect("RBRACE" /* RBRACE */);
1560
- return { type: "Object", pairs, line: token.line, column: token.column };
1561
- }
1562
- throw this.error(`Unexpected token: ${token.type} (${token.value})`);
1563
- }
1564
- parseKeywordArgs() {
1565
- const kwargs = {};
1566
- while (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
1567
- const key = this.advance().value;
1568
- this.advance();
1569
- kwargs[key] = this.parseExpression();
1570
- }
1571
- return kwargs;
1572
- }
1573
- checkBlockTag(name) {
1574
- if (this.peek().type !== "BLOCK_START" /* BLOCK_START */)
1575
- return false;
1576
- const saved = this.current;
1577
- this.advance();
1578
- const isMatch = this.check("NAME" /* NAME */) && this.peek().value === name;
1579
- this.current = saved;
1580
- return isMatch;
1581
- }
1582
- expectBlockTag(name) {
1583
- this.advance();
1584
- const tag = this.expect("NAME" /* NAME */);
1585
- if (tag.value !== name) {
1586
- throw this.error(`Expected '${name}', got '${tag.value}'`);
1587
- }
1588
- this.expect("BLOCK_END" /* BLOCK_END */);
1589
- }
1590
- expectName(name) {
1591
- const token = this.expect("NAME" /* NAME */);
1592
- if (token.value !== name) {
1593
- throw this.error(`Expected '${name}', got '${token.value}'`);
1594
- }
1595
- }
1596
- skipToBlockEnd() {
1597
- while (!this.isAtEnd() && !this.check("BLOCK_END" /* BLOCK_END */)) {
1598
- this.advance();
1599
- }
1600
- if (this.check("BLOCK_END" /* BLOCK_END */)) {
1601
- this.advance();
1602
- }
1603
- }
1604
- isAtEnd() {
1605
- return this.peek().type === "EOF" /* EOF */;
1606
- }
1607
- peek() {
1608
- return this.tokens[this.current];
1609
- }
1610
- peekNext() {
1611
- if (this.current + 1 >= this.tokens.length)
1612
- return null;
1613
- return this.tokens[this.current + 1];
1614
- }
1615
- advance() {
1616
- if (!this.isAtEnd())
1617
- this.current++;
1618
- return this.tokens[this.current - 1];
1619
- }
1620
- check(type) {
1621
- if (this.isAtEnd())
1622
- return false;
1623
- return this.peek().type === type;
1624
- }
1625
- match(type) {
1626
- if (this.check(type)) {
1627
- this.advance();
1628
- return true;
1629
- }
1630
- return false;
1631
- }
1632
- expect(type) {
1633
- if (this.check(type))
1634
- return this.advance();
1635
- const token = this.peek();
1636
- throw this.error(`Expected ${type}, got ${token.type} (${token.value})`);
1637
- }
1638
- error(message) {
1639
- const token = this.peek();
1640
- return new TemplateSyntaxError(message, {
1641
- line: token.line,
1642
- column: token.column,
1643
- source: this.source
1644
- });
1645
- }
1646
- }
1647
-
1648
- // src/compiler/index.ts
1649
- function compileToString(ast, options = {}) {
1650
- const compiler = new Compiler(options);
1651
- return compiler.compile(ast);
1652
- }
1653
- class Compiler {
1654
- options;
1655
- indent = 0;
1656
- varCounter = 0;
1657
- loopStack = [];
1658
- localVars = [];
1659
- constructor(options = {}) {
1660
- this.options = {
1661
- functionName: options.functionName ?? "render",
1662
- inlineHelpers: options.inlineHelpers ?? true,
1663
- minify: options.minify ?? false,
1664
- autoescape: options.autoescape ?? true
1665
- };
1666
- }
1667
- pushScope() {
1668
- this.localVars.push(new Set);
1669
- }
1670
- popScope() {
1671
- this.localVars.pop();
1672
- }
1673
- addLocalVar(name) {
1674
- if (this.localVars.length > 0) {
1675
- this.localVars[this.localVars.length - 1].add(name);
1676
- }
1677
- }
1678
- isLocalVar(name) {
1679
- for (let i = this.localVars.length - 1;i >= 0; i--) {
1680
- if (this.localVars[i].has(name))
1681
- return true;
1682
- }
1683
- return false;
1684
- }
1685
- compile(ast) {
1686
- const body = this.compileNodes(ast.body);
1687
- const nl = this.options.minify ? "" : `
1688
- `;
1689
- return `function ${this.options.functionName}(__ctx) {${nl}` + ` let __out = '';${nl}` + body + ` return __out;${nl}` + `}`;
1690
- }
1691
- compileNodes(nodes2) {
1692
- return nodes2.map((node) => this.compileNode(node)).join("");
1693
- }
1694
- compileNode(node) {
1695
- switch (node.type) {
1696
- case "Text":
1697
- return this.compileText(node);
1698
- case "Output":
1699
- return this.compileOutput(node);
1700
- case "If":
1701
- return this.compileIf(node);
1702
- case "For":
1703
- return this.compileFor(node);
1704
- case "Set":
1705
- return this.compileSet(node);
1706
- case "With":
1707
- return this.compileWith(node);
1708
- case "Comment":
1709
- return "";
1710
- case "Extends":
1711
- case "Block":
1712
- case "Include":
1713
- throw new Error(`AOT compilation does not support '${node.type}' - use Environment.render() for templates with inheritance`);
1714
- case "Url":
1715
- case "Static":
1716
- throw new Error(`AOT compilation does not support '${node.type}' tag - use Environment.render() with urlResolver/staticResolver`);
1717
- default:
1718
- throw new Error(`Unknown node type in AOT compiler: ${node.type}`);
1719
- }
1720
- }
1721
- compileText(node) {
1722
- const escaped = JSON.stringify(node.value);
1723
- return ` __out += ${escaped};${this.nl()}`;
1724
- }
1725
- compileOutput(node) {
1726
- const expr = this.compileExpr(node.expression);
1727
- if (this.options.autoescape && !this.isMarkedSafe(node.expression)) {
1728
- return ` __out += escape(${expr});${this.nl()}`;
1729
- }
1730
- return ` __out += (${expr}) ?? '';${this.nl()}`;
1731
- }
1732
- compileIf(node) {
1733
- let code = "";
1734
- const test = this.compileExpr(node.test);
1735
- code += ` if (isTruthy(${test})) {${this.nl()}`;
1736
- code += this.compileNodes(node.body);
1737
- code += ` }`;
1738
- for (const elif of node.elifs) {
1739
- const elifTest = this.compileExpr(elif.test);
1740
- code += ` else if (isTruthy(${elifTest})) {${this.nl()}`;
1741
- code += this.compileNodes(elif.body);
1742
- code += ` }`;
1743
- }
1744
- if (node.else_.length > 0) {
1745
- code += ` else {${this.nl()}`;
1746
- code += this.compileNodes(node.else_);
1747
- code += ` }`;
1748
- }
1749
- code += this.nl();
1750
- return code;
1751
- }
1752
- compileFor(node) {
1753
- const iterVar = this.genVar("iter");
1754
- const indexVar = this.genVar("i");
1755
- const lenVar = this.genVar("len");
1756
- const loopVar = this.genVar("loop");
1757
- const itemVar = Array.isArray(node.target) ? node.target[0] : node.target;
1758
- const valueVar = Array.isArray(node.target) && node.target[1] ? node.target[1] : null;
1759
- const parentLoopVar = this.loopStack.length > 0 ? this.loopStack[this.loopStack.length - 1] : null;
1760
- const iter = this.compileExpr(node.iter);
1761
- let code = "";
1762
- code += ` const ${iterVar} = toArray(${iter});${this.nl()}`;
1763
- code += ` const ${lenVar} = ${iterVar}.length;${this.nl()}`;
1764
- if (node.else_.length > 0) {
1765
- code += ` if (${lenVar} === 0) {${this.nl()}`;
1766
- code += this.compileNodes(node.else_);
1767
- code += ` } else {${this.nl()}`;
1768
- }
1769
- code += ` for (let ${indexVar} = 0; ${indexVar} < ${lenVar}; ${indexVar}++) {${this.nl()}`;
1770
- if (valueVar) {
1771
- code += ` const ${itemVar} = ${iterVar}[${indexVar}][0];${this.nl()}`;
1772
- code += ` const ${valueVar} = ${iterVar}[${indexVar}][1];${this.nl()}`;
1773
- } else {
1774
- code += ` const ${itemVar} = ${iterVar}[${indexVar}];${this.nl()}`;
1775
- }
1776
- code += ` const ${loopVar} = {${this.nl()}`;
1777
- code += ` counter: ${indexVar} + 1,${this.nl()}`;
1778
- code += ` counter0: ${indexVar},${this.nl()}`;
1779
- code += ` revcounter: ${lenVar} - ${indexVar},${this.nl()}`;
1780
- code += ` revcounter0: ${lenVar} - ${indexVar} - 1,${this.nl()}`;
1781
- code += ` first: ${indexVar} === 0,${this.nl()}`;
1782
- code += ` last: ${indexVar} === ${lenVar} - 1,${this.nl()}`;
1783
- code += ` length: ${lenVar},${this.nl()}`;
1784
- code += ` index: ${indexVar} + 1,${this.nl()}`;
1785
- code += ` index0: ${indexVar},${this.nl()}`;
1786
- if (parentLoopVar) {
1787
- code += ` parentloop: ${parentLoopVar},${this.nl()}`;
1788
- code += ` parent: ${parentLoopVar}${this.nl()}`;
1789
- } else {
1790
- code += ` parentloop: null,${this.nl()}`;
1791
- code += ` parent: null${this.nl()}`;
1792
- }
1793
- code += ` };${this.nl()}`;
1794
- code += ` const forloop = ${loopVar};${this.nl()}`;
1795
- code += ` const loop = ${loopVar};${this.nl()}`;
1796
- this.loopStack.push(loopVar);
1797
- const bodyCode = this.compileNodes(node.body);
1798
- code += bodyCode.replace(new RegExp(`__ctx\\.${itemVar}`, "g"), itemVar);
1799
- this.loopStack.pop();
1800
- code += ` }${this.nl()}`;
1801
- if (node.else_.length > 0) {
1802
- code += ` }${this.nl()}`;
1803
- }
1804
- return code;
1805
- }
1806
- compileSet(node) {
1807
- const value = this.compileExpr(node.value);
1808
- return ` const ${node.target} = ${value};${this.nl()}`;
1809
- }
1810
- compileWith(node) {
1811
- let code = ` {${this.nl()}`;
1812
- this.pushScope();
1813
- for (const { target, value } of node.assignments) {
1814
- const valueExpr = this.compileExpr(value);
1815
- code += ` const ${target} = ${valueExpr};${this.nl()}`;
1816
- this.addLocalVar(target);
1817
- }
1818
- code += this.compileNodes(node.body);
1819
- code += ` }${this.nl()}`;
1820
- this.popScope();
1821
- return code;
1822
- }
1823
- compileExpr(node) {
1824
- switch (node.type) {
1825
- case "Name":
1826
- return this.compileName(node);
1827
- case "Literal":
1828
- return this.compileLiteral(node);
1829
- case "Array":
1830
- return this.compileArray(node);
1831
- case "Object":
1832
- return this.compileObject(node);
1833
- case "BinaryOp":
1834
- return this.compileBinaryOp(node);
1835
- case "UnaryOp":
1836
- return this.compileUnaryOp(node);
1837
- case "Compare":
1838
- return this.compileCompare(node);
1839
- case "GetAttr":
1840
- return this.compileGetAttr(node);
1841
- case "GetItem":
1842
- return this.compileGetItem(node);
1843
- case "FilterExpr":
1844
- return this.compileFilter(node);
1845
- case "TestExpr":
1846
- return this.compileTest(node);
1847
- case "Conditional":
1848
- return this.compileConditional(node);
1849
- default:
1850
- return "undefined";
1851
- }
1852
- }
1853
- compileName(node) {
1854
- if (node.name === "true" || node.name === "True")
1855
- return "true";
1856
- if (node.name === "false" || node.name === "False")
1857
- return "false";
1858
- if (node.name === "none" || node.name === "None" || node.name === "null")
1859
- return "null";
1860
- if (node.name === "forloop" || node.name === "loop")
1861
- return node.name;
1862
- if (this.isLocalVar(node.name)) {
1863
- return node.name;
1864
- }
1865
- return `__ctx.${node.name}`;
1866
- }
1867
- compileLiteral(node) {
1868
- if (typeof node.value === "string") {
1869
- return JSON.stringify(node.value);
1870
- }
1871
- return String(node.value);
1872
- }
1873
- compileArray(node) {
1874
- const elements = node.elements.map((el) => this.compileExpr(el)).join(", ");
1875
- return `[${elements}]`;
1876
- }
1877
- compileObject(node) {
1878
- const pairs = node.pairs.map(({ key, value }) => {
1879
- const k = this.compileExpr(key);
1880
- const v = this.compileExpr(value);
1881
- return `[${k}]: ${v}`;
1882
- }).join(", ");
1883
- return `{${pairs}}`;
1884
- }
1885
- compileBinaryOp(node) {
1886
- const left = this.compileExpr(node.left);
1887
- const right = this.compileExpr(node.right);
1888
- switch (node.operator) {
1889
- case "and":
1890
- return `(${left} && ${right})`;
1891
- case "or":
1892
- return `(${left} || ${right})`;
1893
- case "~":
1894
- return `(String(${left}) + String(${right}))`;
1895
- case "in":
1896
- return `(Array.isArray(${right}) ? ${right}.includes(${left}) : String(${right}).includes(String(${left})))`;
1897
- case "not in":
1898
- return `!(Array.isArray(${right}) ? ${right}.includes(${left}) : String(${right}).includes(String(${left})))`;
1899
- default:
1900
- return `(${left} ${node.operator} ${right})`;
1901
- }
1902
- }
1903
- compileUnaryOp(node) {
1904
- const operand = this.compileExpr(node.operand);
1905
- switch (node.operator) {
1906
- case "not":
1907
- return `!isTruthy(${operand})`;
1908
- case "-":
1909
- return `-(${operand})`;
1910
- case "+":
1911
- return `+(${operand})`;
1912
- default:
1913
- return operand;
1914
- }
1915
- }
1916
- compileCompare(node) {
1917
- let result = this.compileExpr(node.left);
1918
- for (const { operator, right } of node.ops) {
1919
- const rightExpr = this.compileExpr(right);
1920
- switch (operator) {
1921
- case "==":
1922
- case "===":
1923
- result = `(${result} === ${rightExpr})`;
1924
- break;
1925
- case "!=":
1926
- case "!==":
1927
- result = `(${result} !== ${rightExpr})`;
1928
- break;
1929
- default:
1930
- result = `(${result} ${operator} ${rightExpr})`;
1931
- }
1932
- }
1933
- return result;
1934
- }
1935
- compileGetAttr(node) {
1936
- const obj = this.compileExpr(node.object);
1937
- return `${obj}?.${node.attribute}`;
1938
- }
1939
- compileGetItem(node) {
1940
- const obj = this.compileExpr(node.object);
1941
- const index = this.compileExpr(node.index);
1942
- return `${obj}?.[${index}]`;
1943
- }
1944
- compileFilter(node) {
1945
- const value = this.compileExpr(node.node);
1946
- const args = node.args.map((arg) => this.compileExpr(arg));
1947
- switch (node.filter) {
1948
- case "upper":
1949
- return `String(${value}).toUpperCase()`;
1950
- case "lower":
1951
- return `String(${value}).toLowerCase()`;
1952
- case "title":
1953
- return `String(${value}).replace(/\\b\\w/g, c => c.toUpperCase())`;
1954
- case "trim":
1955
- return `String(${value}).trim()`;
1956
- case "length":
1957
- return `(${value}?.length ?? Object.keys(${value} ?? {}).length)`;
1958
- case "first":
1959
- return `(${value})?.[0]`;
1960
- case "last":
1961
- return `(${value})?.[(${value})?.length - 1]`;
1962
- case "default":
1963
- return `((${value}) ?? ${args[0] ?? '""'})`;
1964
- case "safe":
1965
- return `{ __safe: true, value: String(${value}) }`;
1966
- case "escape":
1967
- case "e":
1968
- return `escape(${value})`;
1969
- case "join":
1970
- return `(${value} ?? []).join(${args[0] ?? '""'})`;
1971
- case "abs":
1972
- return `Math.abs(${value})`;
1973
- case "round":
1974
- return args.length ? `Number(${value}).toFixed(${args[0]})` : `Math.round(${value})`;
1975
- case "int":
1976
- return `parseInt(${value}, 10)`;
1977
- case "float":
1978
- return `parseFloat(${value})`;
1979
- case "floatformat":
1980
- return `Number(${value}).toFixed(${args[0] ?? 1})`;
1981
- case "filesizeformat":
1982
- return `applyFilter('filesizeformat', ${value})`;
1983
- default:
1984
- const argsStr = args.length ? ", " + args.join(", ") : "";
1985
- return `applyFilter('${node.filter}', ${value}${argsStr})`;
1986
- }
1987
- }
1988
- compileTest(node) {
1989
- const value = this.compileExpr(node.node);
1990
- const args = node.args.map((arg) => this.compileExpr(arg));
1991
- const negation = node.negated ? "!" : "";
1992
- switch (node.test) {
1993
- case "defined":
1994
- return `${negation}(${value} !== undefined)`;
1995
- case "undefined":
1996
- return `${negation}(${value} === undefined)`;
1997
- case "none":
1998
- return `${negation}(${value} === null)`;
1999
- case "even":
2000
- return `${negation}(${value} % 2 === 0)`;
2001
- case "odd":
2002
- return `${negation}(${value} % 2 !== 0)`;
2003
- case "divisibleby":
2004
- return `${negation}(${value} % ${args[0]} === 0)`;
2005
- case "empty":
2006
- return `${negation}((${value} == null) || (${value}.length === 0) || (Object.keys(${value}).length === 0))`;
2007
- case "iterable":
2008
- return `${negation}(Array.isArray(${value}) || typeof ${value} === 'string')`;
2009
- case "number":
2010
- return `${negation}(typeof ${value} === 'number' && !isNaN(${value}))`;
2011
- case "string":
2012
- return `${negation}(typeof ${value} === 'string')`;
2013
- default:
2014
- const argsStr = args.length ? ", " + args.join(", ") : "";
2015
- return `${negation}applyTest('${node.test}', ${value}${argsStr})`;
2016
- }
2017
- }
2018
- compileConditional(node) {
2019
- const test = this.compileExpr(node.test);
2020
- const trueExpr = this.compileExpr(node.trueExpr);
2021
- const falseExpr = this.compileExpr(node.falseExpr);
2022
- return `(isTruthy(${test}) ? ${trueExpr} : ${falseExpr})`;
2023
- }
2024
- isMarkedSafe(node) {
2025
- if (node.type === "FilterExpr") {
2026
- const filter = node;
2027
- return filter.filter === "safe";
2028
- }
2029
- return false;
2030
- }
2031
- genVar(prefix) {
2032
- return `__${prefix}${this.varCounter++}`;
2033
- }
2034
- nl() {
2035
- return this.options.minify ? "" : `
2036
- `;
2037
- }
2038
- }
2039
-
2040
- // src/compiler/flattener.ts
2041
- function flattenTemplate(ast, options) {
2042
- const flattener = new TemplateFlattener(options);
2043
- return flattener.flatten(ast);
2044
- }
2045
- function canFlatten(ast) {
2046
- const checker = new StaticChecker;
2047
- return checker.check(ast);
2048
- }
2049
-
2050
- class TemplateFlattener {
2051
- loader;
2052
- maxDepth;
2053
- blocks = new Map;
2054
- depth = 0;
2055
- constructor(options) {
2056
- this.loader = options.loader;
2057
- this.maxDepth = options.maxDepth ?? 10;
2058
- }
2059
- flatten(ast) {
2060
- this.blocks.clear();
2061
- this.depth = 0;
2062
- return this.processTemplate(ast);
2063
- }
2064
- processTemplate(ast, isChild = true) {
2065
- if (this.depth > this.maxDepth) {
2066
- throw new Error(`Maximum template inheritance depth (${this.maxDepth}) exceeded`);
2067
- }
2068
- this.collectBlocks(ast.body, isChild);
2069
- const extendsNode = this.findExtends(ast.body);
2070
- if (extendsNode) {
2071
- const parentName = this.getStaticTemplateName(extendsNode.template);
2072
- const parentSource = this.loader.load(parentName);
2073
- const parentAst = this.loader.parse(parentSource);
2074
- this.depth++;
2075
- const flattenedParent = this.processTemplate(parentAst, false);
2076
- this.depth--;
2077
- return {
2078
- type: "Template",
2079
- body: this.replaceBlocks(flattenedParent.body),
2080
- line: ast.line,
2081
- column: ast.column
2082
- };
2083
- }
2084
- return {
2085
- type: "Template",
2086
- body: this.processNodes(ast.body),
2087
- line: ast.line,
2088
- column: ast.column
2089
- };
2090
- }
2091
- collectBlocks(nodes2, override = true) {
2092
- for (const node of nodes2) {
2093
- if (node.type === "Block") {
2094
- const block = node;
2095
- if (override || !this.blocks.has(block.name)) {
2096
- this.blocks.set(block.name, block);
2097
- }
2098
- }
2099
- this.collectBlocksFromNode(node, override);
2100
- }
2101
- }
2102
- collectBlocksFromNode(node, override = true) {
2103
- switch (node.type) {
2104
- case "If": {
2105
- const ifNode = node;
2106
- this.collectBlocks(ifNode.body, override);
2107
- for (const elif of ifNode.elifs) {
2108
- this.collectBlocks(elif.body, override);
2109
- }
2110
- this.collectBlocks(ifNode.else_, override);
2111
- break;
2112
- }
2113
- case "For": {
2114
- const forNode = node;
2115
- this.collectBlocks(forNode.body, override);
2116
- this.collectBlocks(forNode.else_, override);
2117
- break;
2118
- }
2119
- case "With": {
2120
- const withNode = node;
2121
- this.collectBlocks(withNode.body, override);
2122
- break;
2123
- }
2124
- case "Block": {
2125
- const blockNode = node;
2126
- this.collectBlocks(blockNode.body, override);
2127
- break;
2128
- }
2129
- }
2130
- }
2131
- findExtends(nodes2) {
2132
- for (const node of nodes2) {
2133
- if (node.type === "Extends") {
2134
- return node;
2135
- }
2136
- }
2137
- return null;
2138
- }
2139
- processNodes(nodes2) {
2140
- const result = [];
2141
- for (const node of nodes2) {
2142
- if (node.type === "Extends") {
2143
- continue;
2144
- }
2145
- if (node.type === "Include") {
2146
- const includeNode = node;
2147
- const inlined = this.inlineInclude(includeNode);
2148
- result.push(...inlined);
2149
- continue;
2150
- }
2151
- if (node.type === "Block") {
2152
- const block = node;
2153
- const childBlock = this.blocks.get(block.name);
2154
- if (childBlock && childBlock !== block) {
2155
- result.push(...this.processNodes(childBlock.body));
2156
- } else {
2157
- result.push(...this.processNodes(block.body));
2158
- }
2159
- continue;
2160
- }
2161
- const processed = this.processNode(node);
2162
- if (processed) {
2163
- result.push(processed);
2164
- }
2165
- }
2166
- return result;
2167
- }
2168
- processNode(node) {
2169
- switch (node.type) {
2170
- case "If": {
2171
- const ifNode = node;
2172
- return {
2173
- ...ifNode,
2174
- body: this.processNodes(ifNode.body),
2175
- elifs: ifNode.elifs.map((elif) => ({
2176
- test: elif.test,
2177
- body: this.processNodes(elif.body)
2178
- })),
2179
- else_: this.processNodes(ifNode.else_)
2180
- };
2181
- }
2182
- case "For": {
2183
- const forNode = node;
2184
- return {
2185
- ...forNode,
2186
- body: this.processNodes(forNode.body),
2187
- else_: this.processNodes(forNode.else_)
2188
- };
2189
- }
2190
- case "With": {
2191
- const withNode = node;
2192
- return {
2193
- ...withNode,
2194
- body: this.processNodes(withNode.body)
2195
- };
2196
- }
2197
- default:
2198
- return node;
2199
- }
2200
- }
2201
- replaceBlocks(nodes2) {
2202
- return this.processNodes(nodes2);
2203
- }
2204
- inlineInclude(node) {
2205
- const templateName = this.getStaticTemplateName(node.template);
2206
- try {
2207
- const source = this.loader.load(templateName);
2208
- const ast = this.loader.parse(source);
2209
- this.depth++;
2210
- const flattened = this.processTemplate(ast);
2211
- this.depth--;
2212
- if (node.context && Object.keys(node.context).length > 0) {
2213
- const withNode = {
2214
- type: "With",
2215
- assignments: Object.entries(node.context).map(([target, value]) => ({
2216
- target,
2217
- value
2218
- })),
2219
- body: flattened.body,
2220
- line: node.line,
2221
- column: node.column
2222
- };
2223
- return [withNode];
2224
- }
2225
- return flattened.body;
2226
- } catch (error) {
2227
- if (node.ignoreMissing) {
2228
- return [];
2229
- }
2230
- throw error;
2231
- }
2232
- }
2233
- getStaticTemplateName(expr) {
2234
- if (expr.type === "Literal") {
2235
- const literal = expr;
2236
- if (typeof literal.value === "string") {
2237
- return literal.value;
2238
- }
2239
- }
2240
- throw new Error(`AOT compilation requires static template names. ` + `Found dynamic expression at line ${expr.line}. ` + `Use Environment.render() for dynamic template names.`);
2241
- }
2242
- }
2243
-
2244
- class StaticChecker {
2245
- check(ast) {
2246
- return this.checkNodes(ast.body);
2247
- }
2248
- checkNodes(nodes2) {
2249
- for (const node of nodes2) {
2250
- const result = this.checkNode(node);
2251
- if (!result.canFlatten) {
2252
- return result;
2253
- }
2254
- }
2255
- return { canFlatten: true };
2256
- }
2257
- checkNode(node) {
2258
- switch (node.type) {
2259
- case "Extends": {
2260
- const extendsNode = node;
2261
- if (!this.isStaticName(extendsNode.template)) {
2262
- return {
2263
- canFlatten: false,
2264
- reason: `Dynamic extends at line ${node.line} - use static string literal`
2265
- };
2266
- }
2267
- break;
2268
- }
2269
- case "Include": {
2270
- const includeNode = node;
2271
- if (!this.isStaticName(includeNode.template)) {
2272
- return {
2273
- canFlatten: false,
2274
- reason: `Dynamic include at line ${node.line} - use static string literal`
2275
- };
2276
- }
2277
- break;
2278
- }
2279
- case "If": {
2280
- const ifNode = node;
2281
- let result = this.checkNodes(ifNode.body);
2282
- if (!result.canFlatten)
2283
- return result;
2284
- for (const elif of ifNode.elifs) {
2285
- result = this.checkNodes(elif.body);
2286
- if (!result.canFlatten)
2287
- return result;
2288
- }
2289
- result = this.checkNodes(ifNode.else_);
2290
- if (!result.canFlatten)
2291
- return result;
2292
- break;
2293
- }
2294
- case "For": {
2295
- const forNode = node;
2296
- let result = this.checkNodes(forNode.body);
2297
- if (!result.canFlatten)
2298
- return result;
2299
- result = this.checkNodes(forNode.else_);
2300
- if (!result.canFlatten)
2301
- return result;
2302
- break;
2303
- }
2304
- case "With": {
2305
- const withNode = node;
2306
- const result = this.checkNodes(withNode.body);
2307
- if (!result.canFlatten)
2308
- return result;
2309
- break;
2310
- }
2311
- case "Block": {
2312
- const blockNode = node;
2313
- const result = this.checkNodes(blockNode.body);
2314
- if (!result.canFlatten)
2315
- return result;
2316
- break;
2317
- }
2318
- }
2319
- return { canFlatten: true };
2320
- }
2321
- isStaticName(expr) {
2322
- return expr.type === "Literal" && typeof expr.value === "string";
2323
- }
2324
- }
2325
-
2326
- // src/cli.ts
2327
- var VERSION = "0.1.1";
2328
- var colors2 = {
2329
- reset: "\x1B[0m",
2330
- green: "\x1B[32m",
2331
- yellow: "\x1B[33m",
2332
- red: "\x1B[31m",
2333
- cyan: "\x1B[36m",
2334
- dim: "\x1B[2m"
2335
- };
2336
- function log(msg) {
2337
- console.log(msg);
2338
- }
2339
- function success(msg) {
2340
- console.log(`${colors2.green}\u2713${colors2.reset} ${msg}`);
2341
- }
2342
- function warn(msg) {
2343
- console.log(`${colors2.yellow}\u26A0${colors2.reset} ${msg}`);
2344
- }
2345
- function error(msg) {
2346
- console.error(`${colors2.red}\u2717${colors2.reset} ${msg}`);
2347
- }
2348
- function printHelp() {
2349
- console.log(`
2350
- ${colors2.cyan}binja${colors2.reset} - High-performance template compiler
2351
-
2352
- ${colors2.yellow}Usage:${colors2.reset}
3
+ import*as X from"fs";import*as Y from"path";var P={and:"AND",or:"OR",not:"NOT",true:"NAME",false:"NAME",True:"NAME",False:"NAME",None:"NAME",none:"NAME",is:"NAME",in:"NAME"};var x={red:"\x1B[31m",yellow:"\x1B[33m",cyan:"\x1B[36m",gray:"\x1B[90m",bold:"\x1B[1m",dim:"\x1B[2m",reset:"\x1B[0m"},p=process.stdout?.isTTY!==!1;function q(A,B){return p?`${x[A]}${B}${x.reset}`:B}class H extends Error{line;column;source;templateName;suggestion;constructor(A,B){let R=v("TemplateSyntaxError",A,B);super(R);this.name="TemplateSyntaxError",this.line=B.line,this.column=B.column,this.source=B.source,this.templateName=B.templateName,this.suggestion=B.suggestion}}function v(A,B,R){let K=[],O=R.templateName?`${R.templateName}:${R.line}:${R.column}`:`line ${R.line}, column ${R.column}`;if(K.push(`${q("red",q("bold",A))}: ${B} at ${q("cyan",O)}`),R.source)K.push(""),K.push(d(R.source,R.line,R.column));if(R.suggestion)K.push(""),K.push(`${q("yellow","Did you mean")}: ${q("cyan",R.suggestion)}?`);if(R.availableOptions&&R.availableOptions.length>0){K.push("");let Q=R.availableOptions.slice(0,8),U=R.availableOptions.length>8?` ${q("gray",`... and ${R.availableOptions.length-8} more`)}`:"";K.push(`${q("gray","Available")}: ${Q.join(", ")}${U}`)}return K.join(`
4
+ `)}function d(A,B,R){let K=A.split(`
5
+ `),O=[],Q=Math.max(1,B-2),U=Math.min(K.length,B+1),M=String(U).length;for(let Z=Q;Z<=U;Z++){let $=K[Z-1]||"",G=String(Z).padStart(M," ");if(Z===B){O.push(`${q("red"," \u2192")} ${q("gray",G)} ${q("dim","\u2502")} ${$}`);let z=" ".repeat(M+4+Math.max(0,R-1)),E=q("red","^");O.push(`${z}${E}`)}else O.push(` ${q("gray",G)} ${q("dim","\u2502")} ${q("gray",$)}`)}return O.join(`
6
+ `)}class D{state;variableStart;variableEnd;blockStart;blockEnd;commentStart;commentEnd;constructor(A,B={}){this.state={source:A,pos:0,line:1,column:1,tokens:[]},this.variableStart=B.variableStart??"{{",this.variableEnd=B.variableEnd??"}}",this.blockStart=B.blockStart??"{%",this.blockEnd=B.blockEnd??"%}",this.commentStart=B.commentStart??"{#",this.commentEnd=B.commentEnd??"#}"}tokenize(){while(!this.isAtEnd())this.scanToken();return this.addToken("EOF",""),this.state.tokens}scanToken(){if(this.match(this.variableStart)){this.addToken("VARIABLE_START",this.variableStart),this.scanExpression(this.variableEnd,"VARIABLE_END");return}if(this.match(this.blockStart)){let A=this.peek()==="-";if(A)this.advance();let B=this.state.pos;if(this.skipWhitespace(),this.checkWord("raw")||this.checkWord("verbatim")){let R=this.checkWord("raw")?"raw":"verbatim";this.scanRawBlock(R,A);return}this.state.pos=B,this.addToken("BLOCK_START",this.blockStart+(A?"-":"")),this.scanExpression(this.blockEnd,"BLOCK_END");return}if(this.match(this.commentStart)){this.scanComment();return}this.scanText()}checkWord(A){let B=this.state.pos;for(let K=0;K<A.length;K++)if(this.state.source[B+K]?.toLowerCase()!==A[K])return!1;let R=this.state.source[B+A.length];return!R||!this.isAlphaNumeric(R)}scanRawBlock(A,B){let R=this.state.line,K=this.state.column;for(let U=0;U<A.length;U++)this.advance();if(this.skipWhitespace(),this.peek()==="-")this.advance();if(!this.match(this.blockEnd))throw new H(`Expected '${this.blockEnd}' after '${A}'`,{line:this.state.line,column:this.state.column,source:this.state.source});let O=`end${A}`,Q=this.state.pos;while(!this.isAtEnd()){if(this.check(this.blockStart)){let U=this.state.pos,M=this.state.line,Z=this.state.column;if(this.match(this.blockStart),this.peek()==="-")this.advance();if(this.skipWhitespace(),this.checkWord(O)){let $=this.state.source.slice(Q,U);if($.length>0)this.state.tokens.push({type:"TEXT",value:$,line:R,column:K});for(let G=0;G<O.length;G++)this.advance();if(this.skipWhitespace(),this.peek()==="-")this.advance();if(!this.match(this.blockEnd))throw new H(`Expected '${this.blockEnd}' after '${O}'`,{line:this.state.line,column:this.state.column,source:this.state.source});return}this.state.pos=U,this.state.line=M,this.state.column=Z}if(this.peek()===`
7
+ `)this.state.line++,this.state.column=0;this.advance()}throw new H(`Unclosed '${A}' block`,{line:R,column:K,source:this.state.source,suggestion:`Add {% end${A} %} to close the block`})}scanText(){let A=this.state.pos,B=this.state.line,R=this.state.column;while(!this.isAtEnd()){if(this.check(this.variableStart)||this.check(this.blockStart)||this.check(this.commentStart))break;if(this.peek()===`
8
+ `)this.state.line++,this.state.column=0;this.advance()}if(this.state.pos>A){let K=this.state.source.slice(A,this.state.pos);this.state.tokens.push({type:"TEXT",value:K,line:B,column:R})}}scanExpression(A,B){this.skipWhitespace();while(!this.isAtEnd()){if(this.skipWhitespace(),this.peek()==="-"&&this.check(A,1))this.advance();if(this.match(A)){this.addToken(B,A);return}this.scanExpressionToken()}throw new H("Unclosed template tag",{line:this.state.line,column:this.state.column,source:this.state.source,suggestion:`Add closing delimiter '${A}'`})}scanExpressionToken(){if(this.skipWhitespace(),this.isAtEnd())return;let A=this.peek();if(A==='"'||A==="'"){this.scanString(A);return}if(this.isDigit(A)){this.scanNumber();return}if(this.isAlpha(A)||A==="_"){this.scanIdentifier();return}this.scanOperator()}scanString(A){this.advance();let B=this.state.pos;while(!this.isAtEnd()&&this.peek()!==A){if(this.peek()==="\\"&&this.peekNext()===A)this.advance();if(this.peek()===`
9
+ `)this.state.line++,this.state.column=0;this.advance()}if(this.isAtEnd())throw new H("Unterminated string literal",{line:this.state.line,column:this.state.column,source:this.state.source,suggestion:`Add closing quote '${A}'`});let R=this.state.source.slice(B,this.state.pos);this.advance(),this.addToken("STRING",R)}scanNumber(){let A=this.state.pos;while(this.isDigit(this.peek()))this.advance();if(this.peek()==="."&&this.isDigit(this.peekNext())){this.advance();while(this.isDigit(this.peek()))this.advance()}let B=this.state.source.slice(A,this.state.pos);this.addToken("NUMBER",B)}scanIdentifier(){let A=this.state.pos;while(this.isAlphaNumeric(this.peek())||this.peek()==="_")this.advance();let B=this.state.source.slice(A,this.state.pos),R=P[B]??"NAME";this.addToken(R,B)}scanOperator(){let A=this.advance();switch(A){case".":this.addToken("DOT",A);break;case",":this.addToken("COMMA",A);break;case":":this.addToken("COLON",A);break;case"|":this.addToken("PIPE",A);break;case"(":this.addToken("LPAREN",A);break;case")":this.addToken("RPAREN",A);break;case"[":this.addToken("LBRACKET",A);break;case"]":this.addToken("RBRACKET",A);break;case"{":this.addToken("LBRACE",A);break;case"}":this.addToken("RBRACE",A);break;case"+":this.addToken("ADD",A);break;case"-":this.addToken("SUB",A);break;case"*":this.addToken("MUL",A);break;case"/":this.addToken("DIV",A);break;case"%":this.addToken("MOD",A);break;case"~":this.addToken("TILDE",A);break;case"=":if(this.match("="))this.addToken("EQ","==");else this.addToken("ASSIGN","=");break;case"!":if(this.match("="))this.addToken("NE","!=");else throw new H("Unexpected character '!'",{line:this.state.line,column:this.state.column-1,source:this.state.source,suggestion:"Use '!=' for not-equal comparison or 'not' for negation"});break;case"<":if(this.match("="))this.addToken("LE","<=");else this.addToken("LT","<");break;case">":if(this.match("="))this.addToken("GE",">=");else this.addToken("GT",">");break;default:if(!this.isWhitespace(A))throw new H(`Unexpected character '${A}'`,{line:this.state.line,column:this.state.column-1,source:this.state.source})}}scanComment(){while(!this.isAtEnd()&&!this.check(this.commentEnd)){if(this.peek()===`
10
+ `)this.state.line++,this.state.column=0;this.advance()}if(!this.isAtEnd())this.match(this.commentEnd)}isAtEnd(){return this.state.pos>=this.state.source.length}peek(){if(this.isAtEnd())return"\x00";return this.state.source[this.state.pos]}peekNext(){if(this.state.pos+1>=this.state.source.length)return"\x00";return this.state.source[this.state.pos+1]}advance(){let A=this.state.source[this.state.pos];return this.state.pos++,this.state.column++,A}match(A,B=0){let R=this.state.source,K=this.state.pos+B,O=A.length;if(K+O>R.length)return!1;for(let Q=0;Q<O;Q++)if(R[K+Q]!==A[Q])return!1;if(B===0)this.state.pos+=O,this.state.column+=O;return!0}check(A,B=0){let R=this.state.source,K=this.state.pos+B,O=A.length;if(K+O>R.length)return!1;for(let Q=0;Q<O;Q++)if(R[K+Q]!==A[Q])return!1;return!0}skipWhitespace(){while(!this.isAtEnd()&&this.isWhitespace(this.peek())){if(this.peek()===`
11
+ `)this.state.line++,this.state.column=0;this.advance()}}isWhitespace(A){return A===" "||A==="\t"||A===`
12
+ `||A==="\r"}isDigit(A){let B=A.charCodeAt(0);return B>=48&&B<=57}isAlpha(A){let B=A.charCodeAt(0);return B>=97&&B<=122||B>=65&&B<=90}isAlphaNumeric(A){let B=A.charCodeAt(0);return B>=48&&B<=57||B>=97&&B<=122||B>=65&&B<=90}addToken(A,B){this.state.tokens.push({type:A,value:B,line:this.state.line,column:this.state.column-B.length})}}class S{tokens;current=0;source;constructor(A,B){this.tokens=A,this.source=B}parse(){let A=[];while(!this.isAtEnd()){let B=this.parseStatement();if(B)A.push(B)}return{type:"Template",body:A,line:1,column:1}}parseStatement(){switch(this.peek().type){case"TEXT":return this.parseText();case"VARIABLE_START":return this.parseOutput();case"BLOCK_START":return this.parseBlock();case"EOF":return null;default:return this.advance(),null}}parseText(){let A=this.advance();return{type:"Text",value:A.value,line:A.line,column:A.column}}parseOutput(){let A=this.advance(),B=this.parseExpression();return this.expect("VARIABLE_END"),{type:"Output",expression:B,line:A.line,column:A.column}}parseBlock(){let A=this.advance(),B=this.expect("NAME");switch(B.value){case"if":return this.parseIf(A);case"for":return this.parseFor(A);case"block":return this.parseBlockTag(A);case"extends":return this.parseExtends(A);case"include":return this.parseInclude(A);case"set":return this.parseSet(A);case"with":return this.parseWith(A);case"load":return this.parseLoad(A);case"url":return this.parseUrl(A);case"static":return this.parseStatic(A);case"now":return this.parseNow(A);case"comment":return this.parseComment(A);case"spaceless":case"autoescape":case"verbatim":return this.parseSimpleBlock(A,B.value);case"cycle":return this.parseCycle(A);case"firstof":return this.parseFirstof(A);case"ifchanged":return this.parseIfchanged(A);case"regroup":return this.parseRegroup(A);case"widthratio":return this.parseWidthratio(A);case"lorem":return this.parseLorem(A);case"csrf_token":return this.parseCsrfToken(A);case"debug":return this.parseDebug(A);case"templatetag":return this.parseTemplatetag(A);case"ifequal":return this.parseIfequal(A,!1);case"ifnotequal":return this.parseIfequal(A,!0);default:return this.skipToBlockEnd(),null}}parseIf(A){let B=this.parseExpression();this.expect("BLOCK_END");let R=[],K=[],O=[];while(!this.isAtEnd()){if(this.checkBlockTag("elif")||this.checkBlockTag("else")||this.checkBlockTag("endif"))break;let Q=this.parseStatement();if(Q)R.push(Q)}while(this.checkBlockTag("elif")){this.advance(),this.advance();let Q=this.parseExpression();this.expect("BLOCK_END");let U=[];while(!this.isAtEnd()){if(this.checkBlockTag("elif")||this.checkBlockTag("else")||this.checkBlockTag("endif"))break;let M=this.parseStatement();if(M)U.push(M)}K.push({test:Q,body:U})}if(this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endif"))break;let Q=this.parseStatement();if(Q)O.push(Q)}}return this.expectBlockTag("endif"),{type:"If",test:B,body:R,elifs:K,else_:O,line:A.line,column:A.column}}parseFor(A){let B,R=this.expect("NAME").value;if(this.check("COMMA")){let Z=[R];while(this.match("COMMA"))Z.push(this.expect("NAME").value);B=Z}else B=R;let K=this.expect("NAME");if(K.value!=="in")throw this.error(`Expected 'in' in for loop, got '${K.value}'`);let O=this.parseExpression(),Q=this.check("NAME")&&this.peek().value==="recursive";if(Q)this.advance();this.expect("BLOCK_END");let U=[],M=[];while(!this.isAtEnd()){if(this.checkBlockTag("empty")||this.checkBlockTag("else")||this.checkBlockTag("endfor"))break;let Z=this.parseStatement();if(Z)U.push(Z)}if(this.checkBlockTag("empty")||this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endfor"))break;let Z=this.parseStatement();if(Z)M.push(Z)}}return this.expectBlockTag("endfor"),{type:"For",target:B,iter:O,body:U,else_:M,recursive:Q,line:A.line,column:A.column}}parseBlockTag(A){let B=this.expect("NAME").value,R=this.check("NAME")&&this.peek().value==="scoped";if(R)this.advance();this.expect("BLOCK_END");let K=[];while(!this.isAtEnd()){if(this.checkBlockTag("endblock"))break;let O=this.parseStatement();if(O)K.push(O)}if(this.advance(),this.advance(),this.check("NAME"))this.advance();return this.expect("BLOCK_END"),{type:"Block",name:B,body:K,scoped:R,line:A.line,column:A.column}}parseExtends(A){let B=this.parseExpression();return this.expect("BLOCK_END"),{type:"Extends",template:B,line:A.line,column:A.column}}parseInclude(A){let B=this.parseExpression(),R=null,K=!1,O=!1;while(this.check("NAME")){let Q=this.peek().value;if(Q==="ignore"&&this.peekNext()?.value==="missing")this.advance(),this.advance(),O=!0;else if(Q==="with")this.advance(),R=this.parseKeywordArgs();else if(Q==="only")this.advance(),K=!0;else if(Q==="without"){if(this.advance(),this.check("NAME")&&this.peek().value==="context")this.advance(),K=!0}else break}return this.expect("BLOCK_END"),{type:"Include",template:B,context:R,only:K,ignoreMissing:O,line:A.line,column:A.column}}parseSet(A){let B=this.expect("NAME").value;this.expect("ASSIGN");let R=this.parseExpression();return this.expect("BLOCK_END"),{type:"Set",target:B,value:R,line:A.line,column:A.column}}parseWith(A){let B=[];do{let K=this.expect("NAME").value;this.expect("ASSIGN");let O=this.parseExpression();B.push({target:K,value:O})}while(this.match("COMMA")||this.check("NAME")&&this.peekNext()?.type==="ASSIGN");this.expect("BLOCK_END");let R=[];while(!this.isAtEnd()){if(this.checkBlockTag("endwith"))break;let K=this.parseStatement();if(K)R.push(K)}return this.expectBlockTag("endwith"),{type:"With",assignments:B,body:R,line:A.line,column:A.column}}parseLoad(A){let B=[];while(this.check("NAME"))B.push(this.advance().value);return this.expect("BLOCK_END"),{type:"Load",names:B,line:A.line,column:A.column}}parseUrl(A){let B=this.parseExpression(),R=[],K={},O=null;while(!this.check("BLOCK_END")){if(this.check("NAME")&&this.peek().value==="as"){this.advance(),O=this.expect("NAME").value;break}if(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let Q=this.advance().value;this.advance(),K[Q]=this.parseExpression()}else R.push(this.parseExpression())}return this.expect("BLOCK_END"),{type:"Url",name:B,args:R,kwargs:K,asVar:O,line:A.line,column:A.column}}parseStatic(A){let B=this.parseExpression(),R=null;if(this.check("NAME")&&this.peek().value==="as")this.advance(),R=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Static",path:B,asVar:R,line:A.line,column:A.column}}parseNow(A){let B=this.parseExpression(),R=null;if(this.check("NAME")&&this.peek().value==="as")this.advance(),R=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Now",format:B,asVar:R,line:A.line,column:A.column}}parseComment(A){this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endcomment"))break;this.advance()}return this.expectBlockTag("endcomment"),null}parseSimpleBlock(A,B){this.skipToBlockEnd();let R=`end${B}`;while(!this.isAtEnd()){if(this.checkBlockTag(R))break;this.advance()}if(this.checkBlockTag(R))this.advance(),this.advance(),this.expect("BLOCK_END");return null}parseCycle(A){let B=[],R=null,K=!1;while(!this.check("BLOCK_END")){if(this.check("NAME")&&this.peek().value==="as"){if(this.advance(),R=this.expect("NAME").value,this.check("NAME")&&this.peek().value==="silent")this.advance(),K=!0;break}B.push(this.parseExpression())}return this.expect("BLOCK_END"),{type:"Cycle",values:B,asVar:R,silent:K,line:A.line,column:A.column}}parseFirstof(A){let B=[],R=null,K=null;while(!this.check("BLOCK_END")){if(this.check("NAME")&&this.peek().value==="as"){this.advance(),K=this.expect("NAME").value;break}B.push(this.parseExpression())}if(B.length>0){let O=B[B.length-1];if(O.type==="Literal"&&typeof O.value==="string")R=B.pop()}return this.expect("BLOCK_END"),{type:"Firstof",values:B,fallback:R,asVar:K,line:A.line,column:A.column}}parseIfchanged(A){let B=[];while(!this.check("BLOCK_END"))B.push(this.parseExpression());this.expect("BLOCK_END");let R=[],K=[];while(!this.isAtEnd()){if(this.checkBlockTag("else")||this.checkBlockTag("endifchanged"))break;let O=this.parseStatement();if(O)R.push(O)}if(this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag("endifchanged"))break;let O=this.parseStatement();if(O)K.push(O)}}return this.expectBlockTag("endifchanged"),{type:"Ifchanged",values:B,body:R,else_:K,line:A.line,column:A.column}}parseRegroup(A){let B=this.parseExpression();this.expectName("by");let R=this.expect("NAME").value;this.expectName("as");let K=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Regroup",target:B,key:R,asVar:K,line:A.line,column:A.column}}parseWidthratio(A){let B=this.parseExpression(),R=this.parseExpression(),K=this.parseExpression(),O=null;if(this.check("NAME")&&this.peek().value==="as")this.advance(),O=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Widthratio",value:B,maxValue:R,maxWidth:K,asVar:O,line:A.line,column:A.column}}parseLorem(A){let B=null,R="p",K=!1;if(this.check("NUMBER"))B={type:"Literal",value:parseInt(this.advance().value,10),line:A.line,column:A.column};if(this.check("NAME")){let O=this.peek().value.toLowerCase();if(O==="w"||O==="p"||O==="b")R=O,this.advance()}if(this.check("NAME")&&this.peek().value==="random")K=!0,this.advance();return this.expect("BLOCK_END"),{type:"Lorem",count:B,method:R,random:K,line:A.line,column:A.column}}parseCsrfToken(A){return this.expect("BLOCK_END"),{type:"CsrfToken",line:A.line,column:A.column}}parseDebug(A){return this.expect("BLOCK_END"),{type:"Debug",line:A.line,column:A.column}}parseTemplatetag(A){let B=this.expect("NAME").value;return this.expect("BLOCK_END"),{type:"Templatetag",tagType:B,line:A.line,column:A.column}}parseIfequal(A,B){let R=this.parseExpression(),K=this.parseExpression();this.expect("BLOCK_END");let O={type:"Compare",left:R,ops:[{operator:B?"!=":"==",right:K}],line:A.line,column:A.column},Q=[],U=[],M=B?"endifnotequal":"endifequal";while(!this.isAtEnd()){if(this.checkBlockTag("else")||this.checkBlockTag(M))break;let Z=this.parseStatement();if(Z)Q.push(Z)}if(this.checkBlockTag("else")){this.advance(),this.advance(),this.expect("BLOCK_END");while(!this.isAtEnd()){if(this.checkBlockTag(M))break;let Z=this.parseStatement();if(Z)U.push(Z)}}return this.expectBlockTag(M),{type:"If",test:O,body:Q,elifs:[],else_:U,line:A.line,column:A.column}}parseExpression(){return this.parseConditional()}parseConditional(){let A=this.parseOr();if(this.check("NAME")&&this.peek().value==="if"){this.advance();let B=this.parseOr();this.expectName("else");let R=this.parseConditional();A={type:"Conditional",test:B,trueExpr:A,falseExpr:R,line:A.line,column:A.column}}return A}parseOr(){let A=this.parseAnd();while(this.check("OR")||this.check("NAME")&&this.peek().value==="or"){this.advance();let B=this.parseAnd();A={type:"BinaryOp",operator:"or",left:A,right:B,line:A.line,column:A.column}}return A}parseAnd(){let A=this.parseNot();while(this.check("AND")||this.check("NAME")&&this.peek().value==="and"){this.advance();let B=this.parseNot();A={type:"BinaryOp",operator:"and",left:A,right:B,line:A.line,column:A.column}}return A}parseNot(){if(this.check("NOT")||this.check("NAME")&&this.peek().value==="not"){let A=this.advance();return{type:"UnaryOp",operator:"not",operand:this.parseNot(),line:A.line,column:A.column}}return this.parseCompare()}parseCompare(){let A=this.parseAddSub(),B=[];while(!0){let R=null;if(this.match("EQ"))R="==";else if(this.match("NE"))R="!=";else if(this.match("LT"))R="<";else if(this.match("GT"))R=">";else if(this.match("LE"))R="<=";else if(this.match("GE"))R=">=";else if(this.check("NAME")){let O=this.peek().value;if(O==="in")this.advance(),R="in";else if(O==="not"&&this.peekNext()?.value==="in")this.advance(),this.advance(),R="not in";else if(O==="is"){this.advance();let Q=this.check("NOT");if(Q)this.advance();let M=this.expect("NAME").value,Z=[];if(this.match("LPAREN")){while(!this.check("RPAREN"))if(Z.push(this.parseExpression()),!this.check("RPAREN"))this.expect("COMMA");this.expect("RPAREN")}A={type:"TestExpr",node:A,test:M,args:Z,negated:Q,line:A.line,column:A.column};continue}}if(!R)break;let K=this.parseAddSub();B.push({operator:R,right:K})}if(B.length===0)return A;return{type:"Compare",left:A,ops:B,line:A.line,column:A.column}}parseAddSub(){let A=this.parseMulDiv();while(this.check("ADD")||this.check("SUB")||this.check("TILDE")){let B=this.advance(),R=this.parseMulDiv();A={type:"BinaryOp",operator:B.value,left:A,right:R,line:A.line,column:A.column}}return A}parseMulDiv(){let A=this.parseUnary();while(this.check("MUL")||this.check("DIV")||this.check("MOD")){let B=this.advance(),R=this.parseUnary();A={type:"BinaryOp",operator:B.value,left:A,right:R,line:A.line,column:A.column}}return A}parseUnary(){if(this.check("SUB")||this.check("ADD")){let A=this.advance(),B=this.parseUnary();return{type:"UnaryOp",operator:A.value,operand:B,line:A.line,column:A.column}}return this.parseFilter()}parseFilter(){let A=this.parsePostfix();while(this.match("PIPE")){let B=this.expect("NAME").value,R=[],K={};if(this.match("COLON"))if(this.check("SUB")||this.check("ADD")){let O=this.advance(),Q=this.parsePostfix();R.push({type:"UnaryOp",operator:O.value,operand:Q,line:O.line,column:O.column})}else R.push(this.parsePostfix());else if(this.match("LPAREN")){while(!this.check("RPAREN")){if(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let O=this.advance().value;this.advance(),K[O]=this.parseExpression()}else R.push(this.parseExpression());if(!this.check("RPAREN"))this.expect("COMMA")}this.expect("RPAREN")}A={type:"FilterExpr",node:A,filter:B,args:R,kwargs:K,line:A.line,column:A.column}}return A}parsePostfix(){let A=this.parsePrimary();while(!0)if(this.match("DOT")){let B;if(this.check("NUMBER"))B=this.advance().value;else B=this.expect("NAME").value;A={type:"GetAttr",object:A,attribute:B,line:A.line,column:A.column}}else if(this.match("LBRACKET")){let B=this.parseExpression();this.expect("RBRACKET"),A={type:"GetItem",object:A,index:B,line:A.line,column:A.column}}else if(this.match("LPAREN")){let B=[],R={};while(!this.check("RPAREN")){if(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let K=this.advance().value;this.advance(),R[K]=this.parseExpression()}else B.push(this.parseExpression());if(!this.check("RPAREN"))this.expect("COMMA")}this.expect("RPAREN"),A={type:"FunctionCall",callee:A,args:B,kwargs:R,line:A.line,column:A.column}}else break;return A}parsePrimary(){let A=this.peek();if(this.match("STRING"))return{type:"Literal",value:A.value,line:A.line,column:A.column};if(this.match("NUMBER"))return{type:"Literal",value:A.value.includes(".")?parseFloat(A.value):parseInt(A.value,10),line:A.line,column:A.column};if(this.check("NAME")){let B=this.advance().value;if(B==="true"||B==="True")return{type:"Literal",value:!0,line:A.line,column:A.column};if(B==="false"||B==="False")return{type:"Literal",value:!1,line:A.line,column:A.column};if(B==="none"||B==="None"||B==="null")return{type:"Literal",value:null,line:A.line,column:A.column};return{type:"Name",name:B,line:A.line,column:A.column}}if(this.match("LPAREN")){let B=this.parseExpression();return this.expect("RPAREN"),B}if(this.match("LBRACKET")){let B=[];while(!this.check("RBRACKET"))if(B.push(this.parseExpression()),!this.check("RBRACKET"))this.expect("COMMA");return this.expect("RBRACKET"),{type:"Array",elements:B,line:A.line,column:A.column}}if(this.match("LBRACE")){let B=[];while(!this.check("RBRACE")){let R=this.parseExpression();this.expect("COLON");let K=this.parseExpression();if(B.push({key:R,value:K}),!this.check("RBRACE"))this.expect("COMMA")}return this.expect("RBRACE"),{type:"Object",pairs:B,line:A.line,column:A.column}}throw this.error(`Unexpected token: ${A.type} (${A.value})`)}parseKeywordArgs(){let A={};while(this.check("NAME")&&this.peekNext()?.type==="ASSIGN"){let B=this.advance().value;this.advance(),A[B]=this.parseExpression()}return A}checkBlockTag(A){if(this.peek().type!=="BLOCK_START")return!1;let B=this.current+1;if(B>=this.tokens.length)return!1;let R=this.tokens[B];return R.type==="NAME"&&R.value===A}expectBlockTag(A){this.advance();let B=this.expect("NAME");if(B.value!==A)throw this.error(`Expected '${A}', got '${B.value}'`);this.expect("BLOCK_END")}expectName(A){let B=this.expect("NAME");if(B.value!==A)throw this.error(`Expected '${A}', got '${B.value}'`)}skipToBlockEnd(){while(!this.isAtEnd()&&!this.check("BLOCK_END"))this.advance();if(this.check("BLOCK_END"))this.advance()}isAtEnd(){return this.peek().type==="EOF"}peek(){return this.tokens[this.current]}peekNext(){if(this.current+1>=this.tokens.length)return null;return this.tokens[this.current+1]}advance(){if(!this.isAtEnd())this.current++;return this.tokens[this.current-1]}check(A){if(this.isAtEnd())return!1;return this.peek().type===A}match(A){if(this.check(A))return this.advance(),!0;return!1}expect(A){if(this.check(A))return this.advance();let B=this.peek();throw this.error(`Expected ${A}, got ${B.type} (${B.value})`)}error(A){let B=this.peek();return new H(A,{line:B.line,column:B.column,source:this.source})}}function f(A,B={}){return new y(B).compile(A)}class y{options;indent=0;varCounter=0;loopStack=[];localVars=[];constructor(A={}){this.options={functionName:A.functionName??"render",inlineHelpers:A.inlineHelpers??!0,minify:A.minify??!1,autoescape:A.autoescape??!0}}pushScope(){this.localVars.push(new Set)}popScope(){this.localVars.pop()}addLocalVar(A){if(this.localVars.length>0)this.localVars[this.localVars.length-1].add(A)}isLocalVar(A){for(let B=this.localVars.length-1;B>=0;B--)if(this.localVars[B].has(A))return!0;return!1}compile(A){let B=this.compileNodes(A.body),R=this.options.minify?"":`
13
+ `;return`function ${this.options.functionName}(__ctx) {${R} let __out = '';${R}`+B+` return __out;${R}}`}compileNodes(A){return A.map((B)=>this.compileNode(B)).join("")}compileNode(A){switch(A.type){case"Text":return this.compileText(A);case"Output":return this.compileOutput(A);case"If":return this.compileIf(A);case"For":return this.compileFor(A);case"Set":return this.compileSet(A);case"With":return this.compileWith(A);case"Comment":return"";case"Extends":case"Block":case"Include":throw Error(`AOT compilation does not support '${A.type}' - use Environment.render() for templates with inheritance`);case"Url":case"Static":throw Error(`AOT compilation does not support '${A.type}' tag - use Environment.render() with urlResolver/staticResolver`);default:throw Error(`Unknown node type in AOT compiler: ${A.type}`)}}compileText(A){return` __out += ${JSON.stringify(A.value)};${this.nl()}`}compileOutput(A){let B=this.compileExpr(A.expression);if(this.options.autoescape&&!this.isMarkedSafe(A.expression))return` __out += escape(${B});${this.nl()}`;return` __out += (${B}) ?? '';${this.nl()}`}compileIf(A){let B="",R=this.compileExpr(A.test);B+=` if (isTruthy(${R})) {${this.nl()}`,B+=this.compileNodes(A.body),B+=" }";for(let K of A.elifs){let O=this.compileExpr(K.test);B+=` else if (isTruthy(${O})) {${this.nl()}`,B+=this.compileNodes(K.body),B+=" }"}if(A.else_.length>0)B+=` else {${this.nl()}`,B+=this.compileNodes(A.else_),B+=" }";return B+=this.nl(),B}compileFor(A){let B=this.genVar("iter"),R=this.genVar("i"),K=this.genVar("len"),O=this.genVar("loop"),Q=Array.isArray(A.target)?A.target[0]:A.target,U=Array.isArray(A.target)&&A.target[1]?A.target[1]:null,M=this.loopStack.length>0?this.loopStack[this.loopStack.length-1]:null,Z=this.compileExpr(A.iter),$="";if($+=` const ${B} = toArray(${Z});${this.nl()}`,$+=` const ${K} = ${B}.length;${this.nl()}`,A.else_.length>0)$+=` if (${K} === 0) {${this.nl()}`,$+=this.compileNodes(A.else_),$+=` } else {${this.nl()}`;if($+=` for (let ${R} = 0; ${R} < ${K}; ${R}++) {${this.nl()}`,U)$+=` const ${Q} = ${B}[${R}][0];${this.nl()}`,$+=` const ${U} = ${B}[${R}][1];${this.nl()}`;else $+=` const ${Q} = ${B}[${R}];${this.nl()}`;if($+=` const ${O} = {${this.nl()}`,$+=` counter: ${R} + 1,${this.nl()}`,$+=` counter0: ${R},${this.nl()}`,$+=` revcounter: ${K} - ${R},${this.nl()}`,$+=` revcounter0: ${K} - ${R} - 1,${this.nl()}`,$+=` first: ${R} === 0,${this.nl()}`,$+=` last: ${R} === ${K} - 1,${this.nl()}`,$+=` length: ${K},${this.nl()}`,$+=` index: ${R} + 1,${this.nl()}`,$+=` index0: ${R},${this.nl()}`,M)$+=` parentloop: ${M},${this.nl()}`,$+=` parent: ${M}${this.nl()}`;else $+=` parentloop: null,${this.nl()}`,$+=` parent: null${this.nl()}`;$+=` };${this.nl()}`,$+=` const forloop = ${O};${this.nl()}`,$+=` const loop = ${O};${this.nl()}`,this.loopStack.push(O);let G=this.compileNodes(A.body);if($+=G.replace(new RegExp(`__ctx\\.${Q}`,"g"),Q),this.loopStack.pop(),$+=` }${this.nl()}`,A.else_.length>0)$+=` }${this.nl()}`;return $}compileSet(A){let B=this.compileExpr(A.value);return` const ${A.target} = ${B};${this.nl()}`}compileWith(A){let B=` {${this.nl()}`;this.pushScope();for(let{target:R,value:K}of A.assignments){let O=this.compileExpr(K);B+=` const ${R} = ${O};${this.nl()}`,this.addLocalVar(R)}return B+=this.compileNodes(A.body),B+=` }${this.nl()}`,this.popScope(),B}compileExpr(A){switch(A.type){case"Name":return this.compileName(A);case"Literal":return this.compileLiteral(A);case"Array":return this.compileArray(A);case"Object":return this.compileObject(A);case"BinaryOp":return this.compileBinaryOp(A);case"UnaryOp":return this.compileUnaryOp(A);case"Compare":return this.compileCompare(A);case"GetAttr":return this.compileGetAttr(A);case"GetItem":return this.compileGetItem(A);case"FilterExpr":return this.compileFilter(A);case"TestExpr":return this.compileTest(A);case"Conditional":return this.compileConditional(A);default:return"undefined"}}compileName(A){if(A.name==="true"||A.name==="True")return"true";if(A.name==="false"||A.name==="False")return"false";if(A.name==="none"||A.name==="None"||A.name==="null")return"null";if(A.name==="forloop"||A.name==="loop")return A.name;if(this.isLocalVar(A.name))return A.name;return`__ctx.${A.name}`}compileLiteral(A){if(typeof A.value==="string")return JSON.stringify(A.value);return String(A.value)}compileArray(A){return`[${A.elements.map((R)=>this.compileExpr(R)).join(", ")}]`}compileObject(A){return`{${A.pairs.map(({key:R,value:K})=>{let O=this.compileExpr(R),Q=this.compileExpr(K);return`[${O}]: ${Q}`}).join(", ")}}`}compileBinaryOp(A){let B=this.compileExpr(A.left),R=this.compileExpr(A.right);switch(A.operator){case"and":return`(${B} && ${R})`;case"or":return`(${B} || ${R})`;case"~":return`(String(${B}) + String(${R}))`;case"in":return`(Array.isArray(${R}) ? ${R}.includes(${B}) : String(${R}).includes(String(${B})))`;case"not in":return`!(Array.isArray(${R}) ? ${R}.includes(${B}) : String(${R}).includes(String(${B})))`;default:return`(${B} ${A.operator} ${R})`}}compileUnaryOp(A){let B=this.compileExpr(A.operand);switch(A.operator){case"not":return`!isTruthy(${B})`;case"-":return`-(${B})`;case"+":return`+(${B})`;default:return B}}compileCompare(A){let B=this.compileExpr(A.left);for(let{operator:R,right:K}of A.ops){let O=this.compileExpr(K);switch(R){case"==":case"===":B=`(${B} === ${O})`;break;case"!=":case"!==":B=`(${B} !== ${O})`;break;default:B=`(${B} ${R} ${O})`}}return B}compileGetAttr(A){return`${this.compileExpr(A.object)}?.${A.attribute}`}compileGetItem(A){let B=this.compileExpr(A.object),R=this.compileExpr(A.index);return`${B}?.[${R}]`}compileFilter(A){let B=this.compileExpr(A.node),R=A.args.map((K)=>this.compileExpr(K));switch(A.filter){case"upper":return`String(${B}).toUpperCase()`;case"lower":return`String(${B}).toLowerCase()`;case"title":return`String(${B}).replace(/\\b\\w/g, c => c.toUpperCase())`;case"trim":return`String(${B}).trim()`;case"length":return`(${B}?.length ?? Object.keys(${B} ?? {}).length)`;case"first":return`(${B})?.[0]`;case"last":return`(${B})?.[(${B})?.length - 1]`;case"default":return`((${B}) ?? ${R[0]??'""'})`;case"safe":return`{ __safe: true, value: String(${B}) }`;case"escape":case"e":return`escape(${B})`;case"join":return`(${B} ?? []).join(${R[0]??'""'})`;case"abs":return`Math.abs(${B})`;case"round":return R.length?`Number(${B}).toFixed(${R[0]})`:`Math.round(${B})`;case"int":return`parseInt(${B}, 10)`;case"float":return`parseFloat(${B})`;case"floatformat":return`Number(${B}).toFixed(${R[0]??1})`;case"filesizeformat":return`applyFilter('filesizeformat', ${B})`;default:let K=R.length?", "+R.join(", "):"";return`applyFilter('${A.filter}', ${B}${K})`}}compileTest(A){let B=this.compileExpr(A.node),R=A.args.map((O)=>this.compileExpr(O)),K=A.negated?"!":"";switch(A.test){case"defined":return`${K}(${B} !== undefined)`;case"undefined":return`${K}(${B} === undefined)`;case"none":return`${K}(${B} === null)`;case"even":return`${K}(${B} % 2 === 0)`;case"odd":return`${K}(${B} % 2 !== 0)`;case"divisibleby":return`${K}(${B} % ${R[0]} === 0)`;case"empty":return`${K}((${B} == null) || (${B}.length === 0) || (Object.keys(${B}).length === 0))`;case"iterable":return`${K}(Array.isArray(${B}) || typeof ${B} === 'string')`;case"number":return`${K}(typeof ${B} === 'number' && !isNaN(${B}))`;case"string":return`${K}(typeof ${B} === 'string')`;default:let O=R.length?", "+R.join(", "):"";return`${K}applyTest('${A.test}', ${B}${O})`}}compileConditional(A){let B=this.compileExpr(A.test),R=this.compileExpr(A.trueExpr),K=this.compileExpr(A.falseExpr);return`(isTruthy(${B}) ? ${R} : ${K})`}isMarkedSafe(A){if(A.type==="FilterExpr")return A.filter==="safe";return!1}genVar(A){return`__${A}${this.varCounter++}`}nl(){return this.options.minify?"":`
14
+ `}}function k(A,B){return new g(B).flatten(A)}function N(A){return new m().check(A)}class g{loader;maxDepth;blocks=new Map;depth=0;constructor(A){this.loader=A.loader,this.maxDepth=A.maxDepth??10}flatten(A){return this.blocks.clear(),this.depth=0,this.processTemplate(A)}processTemplate(A,B=!0){if(this.depth>this.maxDepth)throw Error(`Maximum template inheritance depth (${this.maxDepth}) exceeded`);this.collectBlocks(A.body,B);let R=this.findExtends(A.body);if(R){let K=this.getStaticTemplateName(R.template),O=this.loader.load(K),Q=this.loader.parse(O);this.depth++;let U=this.processTemplate(Q,!1);return this.depth--,{type:"Template",body:this.replaceBlocks(U.body),line:A.line,column:A.column}}return{type:"Template",body:this.processNodes(A.body),line:A.line,column:A.column}}collectBlocks(A,B=!0){for(let R of A){if(R.type==="Block"){let K=R;if(B||!this.blocks.has(K.name))this.blocks.set(K.name,K)}this.collectBlocksFromNode(R,B)}}collectBlocksFromNode(A,B=!0){switch(A.type){case"If":{let R=A;this.collectBlocks(R.body,B);for(let K of R.elifs)this.collectBlocks(K.body,B);this.collectBlocks(R.else_,B);break}case"For":{let R=A;this.collectBlocks(R.body,B),this.collectBlocks(R.else_,B);break}case"With":{let R=A;this.collectBlocks(R.body,B);break}case"Block":{let R=A;this.collectBlocks(R.body,B);break}}}findExtends(A){for(let B of A)if(B.type==="Extends")return B;return null}processNodes(A){let B=[];for(let R of A){if(R.type==="Extends")continue;if(R.type==="Include"){let O=R,Q=this.inlineInclude(O);B.push(...Q);continue}if(R.type==="Block"){let O=R,Q=this.blocks.get(O.name);if(Q&&Q!==O)B.push(...this.processNodes(Q.body));else B.push(...this.processNodes(O.body));continue}let K=this.processNode(R);if(K)B.push(K)}return B}processNode(A){switch(A.type){case"If":{let B=A;return{...B,body:this.processNodes(B.body),elifs:B.elifs.map((R)=>({test:R.test,body:this.processNodes(R.body)})),else_:this.processNodes(B.else_)}}case"For":{let B=A;return{...B,body:this.processNodes(B.body),else_:this.processNodes(B.else_)}}case"With":{let B=A;return{...B,body:this.processNodes(B.body)}}default:return A}}replaceBlocks(A){return this.processNodes(A)}inlineInclude(A){let B=this.getStaticTemplateName(A.template);try{let R=this.loader.load(B),K=this.loader.parse(R);this.depth++;let O=this.processTemplate(K);if(this.depth--,A.context&&Object.keys(A.context).length>0)return[{type:"With",assignments:Object.entries(A.context).map(([U,M])=>({target:U,value:M})),body:O.body,line:A.line,column:A.column}];return O.body}catch(R){if(A.ignoreMissing)return[];throw R}}getStaticTemplateName(A){if(A.type==="Literal"){let B=A;if(typeof B.value==="string")return B.value}throw Error(`AOT compilation requires static template names. Found dynamic expression at line ${A.line}. Use Environment.render() for dynamic template names.`)}}class m{check(A){return this.checkNodes(A.body)}checkNodes(A){for(let B of A){let R=this.checkNode(B);if(!R.canFlatten)return R}return{canFlatten:!0}}checkNode(A){switch(A.type){case"Extends":{let B=A;if(!this.isStaticName(B.template))return{canFlatten:!1,reason:`Dynamic extends at line ${A.line} - use static string literal`};break}case"Include":{let B=A;if(!this.isStaticName(B.template))return{canFlatten:!1,reason:`Dynamic include at line ${A.line} - use static string literal`};break}case"If":{let B=A,R=this.checkNodes(B.body);if(!R.canFlatten)return R;for(let K of B.elifs)if(R=this.checkNodes(K.body),!R.canFlatten)return R;if(R=this.checkNodes(B.else_),!R.canFlatten)return R;break}case"For":{let B=A,R=this.checkNodes(B.body);if(!R.canFlatten)return R;if(R=this.checkNodes(B.else_),!R.canFlatten)return R;break}case"With":{let B=A,R=this.checkNodes(B.body);if(!R.canFlatten)return R;break}case"Block":{let B=A,R=this.checkNodes(B.body);if(!R.canFlatten)return R;break}}return{canFlatten:!0}}isStaticName(A){return A.type==="Literal"&&typeof A.value==="string"}}var i="0.1.1",J={reset:"\x1B[0m",green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m",cyan:"\x1B[36m",dim:"\x1B[2m"};function F(A){console.log(A)}function C(A){console.log(`${J.green}\u2713${J.reset} ${A}`)}function _(A){console.log(`${J.yellow}\u26A0${J.reset} ${A}`)}function I(A){console.error(`${J.red}\u2717${J.reset} ${A}`)}function w(){console.log(`
15
+ ${J.cyan}binja${J.reset} - High-performance template compiler
16
+
17
+ ${J.yellow}Usage:${J.reset}
2353
18
  binja compile <source> [options] Compile templates to JavaScript
2354
19
  binja check <source> Check if templates can be AOT compiled
2355
20
  binja --help Show this help
2356
21
  binja --version Show version
2357
22
 
2358
- ${colors2.yellow}Compile Options:${colors2.reset}
23
+ ${J.yellow}Compile Options:${J.reset}
2359
24
  -o, --output <dir> Output directory (required)
2360
25
  -n, --name <name> Function name for single file compilation
2361
26
  -m, --minify Minify output
@@ -2363,94 +28,29 @@ ${colors2.yellow}Compile Options:${colors2.reset}
2363
28
  -v, --verbose Verbose output
2364
29
  -w, --watch Watch for changes and recompile
2365
30
 
2366
- ${colors2.yellow}Examples:${colors2.reset}
2367
- ${colors2.dim}# Compile all templates in a directory${colors2.reset}
31
+ ${J.yellow}Examples:${J.reset}
32
+ ${J.dim}# Compile all templates in a directory${J.reset}
2368
33
  binja compile ./templates -o ./dist/templates
2369
34
 
2370
- ${colors2.dim}# Compile with minification${colors2.reset}
35
+ ${J.dim}# Compile with minification${J.reset}
2371
36
  binja compile ./views -o ./compiled --minify
2372
37
 
2373
- ${colors2.dim}# Compile single file with custom function name${colors2.reset}
38
+ ${J.dim}# Compile single file with custom function name${J.reset}
2374
39
  binja compile ./templates/home.html -o ./dist --name renderHome
2375
40
 
2376
- ${colors2.dim}# Watch mode for development${colors2.reset}
41
+ ${J.dim}# Watch mode for development${J.reset}
2377
42
  binja compile ./templates -o ./dist --watch
2378
43
 
2379
- ${colors2.yellow}Output:${colors2.reset}
44
+ ${J.yellow}Output:${J.reset}
2380
45
  Generated files export a render function:
2381
46
 
2382
- ${colors2.dim}// dist/templates/home.js${colors2.reset}
47
+ ${J.dim}// dist/templates/home.js${J.reset}
2383
48
  export function render(ctx) { ... }
2384
49
 
2385
- ${colors2.dim}// Usage${colors2.reset}
50
+ ${J.dim}// Usage${J.reset}
2386
51
  import { render } from './dist/templates/home.js'
2387
52
  const html = render({ title: 'Hello' })
2388
- `);
2389
- }
2390
- function parseArgs(args) {
2391
- const options = {
2392
- output: "",
2393
- minify: false,
2394
- watch: false,
2395
- extensions: [".html", ".jinja", ".jinja2"],
2396
- verbose: false
2397
- };
2398
- let command = "";
2399
- let source = "";
2400
- for (let i = 0;i < args.length; i++) {
2401
- const arg = args[i];
2402
- if (arg === "compile" || arg === "check" || arg === "watch") {
2403
- command = arg;
2404
- if (arg === "watch") {
2405
- options.watch = true;
2406
- command = "compile";
2407
- }
2408
- } else if (arg === "-o" || arg === "--output") {
2409
- options.output = args[++i];
2410
- } else if (arg === "-n" || arg === "--name") {
2411
- options.name = args[++i];
2412
- } else if (arg === "-m" || arg === "--minify") {
2413
- options.minify = true;
2414
- } else if (arg === "-w" || arg === "--watch") {
2415
- options.watch = true;
2416
- } else if (arg === "-v" || arg === "--verbose") {
2417
- options.verbose = true;
2418
- } else if (arg === "-e" || arg === "--ext") {
2419
- options.extensions = args[++i].split(",").map((e) => e.startsWith(".") ? e : `.${e}`);
2420
- } else if (arg === "--help" || arg === "-h") {
2421
- printHelp();
2422
- process.exit(0);
2423
- } else if (arg === "--version" || arg === "-V") {
2424
- console.log(`binja v${VERSION}`);
2425
- process.exit(0);
2426
- } else if (!arg.startsWith("-") && !source) {
2427
- source = arg;
2428
- }
2429
- }
2430
- return { command, source, options };
2431
- }
2432
- function createTemplateLoader(baseDir, extensions) {
2433
- return {
2434
- load(name) {
2435
- const basePath = path.resolve(baseDir, name);
2436
- for (const ext of [...extensions, ""]) {
2437
- const fullPath = basePath + ext;
2438
- if (fs.existsSync(fullPath)) {
2439
- return fs.readFileSync(fullPath, "utf-8");
2440
- }
2441
- }
2442
- throw new Error(`Template not found: ${name}`);
2443
- },
2444
- parse(source) {
2445
- const lexer = new Lexer(source);
2446
- const tokens = lexer.tokenize();
2447
- const parser = new Parser(tokens);
2448
- return parser.parse();
2449
- }
2450
- };
2451
- }
2452
- function generateOutputCode(code, functionName) {
2453
- return `// Generated by binja - DO NOT EDIT
53
+ `)}function u(A){let B={output:"",minify:!1,watch:!1,extensions:[".html",".jinja",".jinja2"],verbose:!1},R="",K="";for(let O=0;O<A.length;O++){let Q=A[O];if(Q==="compile"||Q==="check"||Q==="watch"){if(R=Q,Q==="watch")B.watch=!0,R="compile"}else if(Q==="-o"||Q==="--output")B.output=A[++O];else if(Q==="-n"||Q==="--name")B.name=A[++O];else if(Q==="-m"||Q==="--minify")B.minify=!0;else if(Q==="-w"||Q==="--watch")B.watch=!0;else if(Q==="-v"||Q==="--verbose")B.verbose=!0;else if(Q==="-e"||Q==="--ext")B.extensions=A[++O].split(",").map((U)=>U.startsWith(".")?U:`.${U}`);else if(Q==="--help"||Q==="-h")w(),process.exit(0);else if(Q==="--version"||Q==="-V")console.log(`binja v${i}`),process.exit(0);else if(!Q.startsWith("-")&&!K)K=Q}return{command:R,source:K,options:B}}function V(A,B){return{load(R){let K=Y.resolve(A,R);for(let O of[...B,""]){let Q=K+O;if(X.existsSync(Q))return X.readFileSync(Q,"utf-8")}throw Error(`Template not found: ${R}`)},parse(R){let O=new D(R).tokenize();return new S(O).parse()}}}function s(A,B){return`// Generated by binja - DO NOT EDIT
2454
54
  // Source: binja compile
2455
55
 
2456
56
  const escape = (v) => {
@@ -2490,223 +90,9 @@ const applyTest = (name, value, ...args) => {
2490
90
  throw new Error(\`Test '\${name}' not available in compiled template.\`);
2491
91
  };
2492
92
 
2493
- ${code}
93
+ ${A}
2494
94
 
2495
- export { ${functionName} as render };
2496
- export default ${functionName};
2497
- `;
2498
- }
2499
- async function compileFile(filePath, outputDir, baseDir, options) {
2500
- try {
2501
- const source = fs.readFileSync(filePath, "utf-8");
2502
- const loader = createTemplateLoader(baseDir, options.extensions);
2503
- const ast = loader.parse(source);
2504
- const check = canFlatten(ast);
2505
- let finalAst = ast;
2506
- if (!check.canFlatten) {
2507
- if (options.verbose) {
2508
- warn(`${path.basename(filePath)}: ${check.reason} - compiling without inheritance resolution`);
2509
- }
2510
- } else {
2511
- finalAst = flattenTemplate(ast, { loader });
2512
- }
2513
- const relativePath = path.relative(baseDir, filePath);
2514
- const functionName = options.name || "render" + relativePath.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+|_+$/g, "").replace(/_([a-z])/g, (_, c2) => c2.toUpperCase());
2515
- const code = compileToString(finalAst, {
2516
- functionName,
2517
- minify: options.minify
2518
- });
2519
- const outputFileName = relativePath.replace(/\.[^.]+$/, ".js");
2520
- const outputPath = path.join(outputDir, outputFileName);
2521
- const outputDirPath = path.dirname(outputPath);
2522
- if (!fs.existsSync(outputDirPath)) {
2523
- fs.mkdirSync(outputDirPath, { recursive: true });
2524
- }
2525
- const outputCode = generateOutputCode(code, functionName);
2526
- fs.writeFileSync(outputPath, outputCode);
2527
- return { success: true, outputPath };
2528
- } catch (err) {
2529
- return { success: false, error: err.message };
2530
- }
2531
- }
2532
- async function compileDirectory(sourceDir, outputDir, options) {
2533
- let compiled = 0;
2534
- let failed = 0;
2535
- const files = [];
2536
- function walkDir(dir) {
2537
- const entries = fs.readdirSync(dir, { withFileTypes: true });
2538
- for (const entry of entries) {
2539
- const fullPath = path.join(dir, entry.name);
2540
- if (entry.isDirectory()) {
2541
- walkDir(fullPath);
2542
- } else if (entry.isFile()) {
2543
- const ext = path.extname(entry.name);
2544
- if (options.extensions.includes(ext)) {
2545
- files.push(fullPath);
2546
- }
2547
- }
2548
- }
2549
- }
2550
- walkDir(sourceDir);
2551
- for (const fullPath of files) {
2552
- const result = await compileFile(fullPath, outputDir, sourceDir, options);
2553
- if (result.success) {
2554
- compiled++;
2555
- if (options.verbose) {
2556
- success(`${path.relative(sourceDir, fullPath)} \u2192 ${path.relative(process.cwd(), result.outputPath)}`);
2557
- }
2558
- } else {
2559
- failed++;
2560
- error(`${path.relative(sourceDir, fullPath)}: ${result.error}`);
2561
- }
2562
- }
2563
- return { compiled, failed };
2564
- }
2565
- async function checkTemplates(sourceDir, options) {
2566
- const loader = createTemplateLoader(sourceDir, options.extensions);
2567
- let total = 0;
2568
- let canCompile = 0;
2569
- let cannotCompile = 0;
2570
- function walkDir(dir) {
2571
- const entries = fs.readdirSync(dir, { withFileTypes: true });
2572
- for (const entry of entries) {
2573
- const fullPath = path.join(dir, entry.name);
2574
- if (entry.isDirectory()) {
2575
- walkDir(fullPath);
2576
- } else if (entry.isFile()) {
2577
- const ext = path.extname(entry.name);
2578
- if (options.extensions.includes(ext)) {
2579
- total++;
2580
- try {
2581
- const source = fs.readFileSync(fullPath, "utf-8");
2582
- const ast = loader.parse(source);
2583
- const check = canFlatten(ast);
2584
- const relativePath = path.relative(sourceDir, fullPath);
2585
- if (check.canFlatten) {
2586
- canCompile++;
2587
- success(`${relativePath}`);
2588
- } else {
2589
- cannotCompile++;
2590
- warn(`${relativePath}: ${check.reason}`);
2591
- }
2592
- } catch (err) {
2593
- cannotCompile++;
2594
- error(`${path.relative(sourceDir, fullPath)}: ${err.message}`);
2595
- }
2596
- }
2597
- }
2598
- }
2599
- }
2600
- walkDir(sourceDir);
2601
- log("");
2602
- log(`Total: ${total} templates`);
2603
- log(`${colors2.green}AOT compatible: ${canCompile}${colors2.reset}`);
2604
- if (cannotCompile > 0) {
2605
- log(`${colors2.yellow}Require runtime: ${cannotCompile}${colors2.reset}`);
2606
- }
2607
- }
2608
- async function watchAndCompile(sourceDir, outputDir, options) {
2609
- log(`${colors2.cyan}Watching${colors2.reset} ${sourceDir} for changes...`);
2610
- log(`${colors2.dim}Press Ctrl+C to stop${colors2.reset}`);
2611
- log("");
2612
- const { compiled, failed } = await compileDirectory(sourceDir, outputDir, { ...options, verbose: true });
2613
- log("");
2614
- log(`Compiled ${compiled} templates${failed > 0 ? `, ${failed} failed` : ""}`);
2615
- log("");
2616
- const watcher = fs.watch(sourceDir, { recursive: true }, async (eventType, filename) => {
2617
- if (!filename)
2618
- return;
2619
- const ext = path.extname(filename);
2620
- if (!options.extensions.includes(ext))
2621
- return;
2622
- const fullPath = path.join(sourceDir, filename);
2623
- if (!fs.existsSync(fullPath))
2624
- return;
2625
- log(`${colors2.dim}[${new Date().toLocaleTimeString()}]${colors2.reset} ${filename} changed`);
2626
- const result = await compileFile(fullPath, outputDir, sourceDir, options);
2627
- if (result.success) {
2628
- success(`Compiled ${filename}`);
2629
- } else {
2630
- error(`${filename}: ${result.error}`);
2631
- }
2632
- });
2633
- process.on("SIGINT", () => {
2634
- watcher.close();
2635
- log(`
2636
- Stopped watching.`);
2637
- process.exit(0);
2638
- });
2639
- }
2640
- async function main() {
2641
- const args = process.argv.slice(2);
2642
- if (args.length === 0) {
2643
- printHelp();
2644
- process.exit(0);
2645
- }
2646
- const { command, source, options } = parseArgs(args);
2647
- if (!command) {
2648
- error('No command specified. Use "compile" or "check".');
2649
- printHelp();
2650
- process.exit(1);
2651
- }
2652
- if (!source) {
2653
- error("No source path specified.");
2654
- process.exit(1);
2655
- }
2656
- const sourcePath = path.resolve(source);
2657
- if (!fs.existsSync(sourcePath)) {
2658
- error(`Source not found: ${source}`);
2659
- process.exit(1);
2660
- }
2661
- const isDirectory = fs.statSync(sourcePath).isDirectory();
2662
- if (command === "check") {
2663
- if (isDirectory) {
2664
- await checkTemplates(sourcePath, options);
2665
- } else {
2666
- const loader = createTemplateLoader(path.dirname(sourcePath), options.extensions);
2667
- const src = fs.readFileSync(sourcePath, "utf-8");
2668
- const ast = loader.parse(src);
2669
- const check = canFlatten(ast);
2670
- if (check.canFlatten) {
2671
- success(`${source} can be AOT compiled`);
2672
- } else {
2673
- warn(`${source}: ${check.reason}`);
2674
- }
2675
- }
2676
- } else if (command === "compile") {
2677
- if (!options.output) {
2678
- error("Output directory required. Use -o <dir>");
2679
- process.exit(1);
2680
- }
2681
- const outputDir = path.resolve(options.output);
2682
- if (options.watch) {
2683
- if (!isDirectory) {
2684
- error("Watch mode requires a directory, not a single file.");
2685
- process.exit(1);
2686
- }
2687
- await watchAndCompile(sourcePath, outputDir, options);
2688
- } else if (isDirectory) {
2689
- const startTime = Date.now();
2690
- const { compiled, failed } = await compileDirectory(sourcePath, outputDir, options);
2691
- const elapsed = Date.now() - startTime;
2692
- log("");
2693
- if (failed === 0) {
2694
- success(`Compiled ${compiled} templates in ${elapsed}ms`);
2695
- } else {
2696
- warn(`Compiled ${compiled} templates, ${failed} failed (${elapsed}ms)`);
2697
- }
2698
- } else {
2699
- const result = await compileFile(sourcePath, outputDir, path.dirname(sourcePath), options);
2700
- if (result.success) {
2701
- success(`Compiled to ${result.outputPath}`);
2702
- } else {
2703
- error(result.error);
2704
- process.exit(1);
2705
- }
2706
- }
2707
- }
2708
- }
2709
- main().catch((err) => {
2710
- error(err.message);
2711
- process.exit(1);
2712
- });
95
+ export { ${B} as render };
96
+ export default ${B};
97
+ `}async function b(A,B,R,K){try{let O=X.readFileSync(A,"utf-8"),Q=V(R,K.extensions),U=Q.parse(O),M=N(U),Z=U;if(!M.canFlatten){if(K.verbose)_(`${Y.basename(A)}: ${M.reason} - compiling without inheritance resolution`)}else Z=k(U,{loader:Q});let $=Y.relative(R,A),G=K.name||"render"+$.replace(/\.[^.]+$/,"").replace(/[^a-zA-Z0-9]/g,"_").replace(/^_+|_+$/g,"").replace(/_([a-z])/g,(o,c)=>c.toUpperCase()),W=f(Z,{functionName:G,minify:K.minify}),z=$.replace(/\.[^.]+$/,".js"),E=Y.join(B,z),L=Y.dirname(E);if(!X.existsSync(L))X.mkdirSync(L,{recursive:!0});let j=s(W,G);return X.writeFileSync(E,j),{success:!0,outputPath:E}}catch(O){return{success:!1,error:O.message}}}async function T(A,B,R){let K=0,O=0,Q=[];function U(M){let Z=X.readdirSync(M,{withFileTypes:!0});for(let $ of Z){let G=Y.join(M,$.name);if($.isDirectory())U(G);else if($.isFile()){let W=Y.extname($.name);if(R.extensions.includes(W))Q.push(G)}}}U(A);for(let M of Q){let Z=await b(M,B,A,R);if(Z.success){if(K++,R.verbose)C(`${Y.relative(A,M)} \u2192 ${Y.relative(process.cwd(),Z.outputPath)}`)}else O++,I(`${Y.relative(A,M)}: ${Z.error}`)}return{compiled:K,failed:O}}async function r(A,B){let R=V(A,B.extensions),K=0,O=0,Q=0;function U(M){let Z=X.readdirSync(M,{withFileTypes:!0});for(let $ of Z){let G=Y.join(M,$.name);if($.isDirectory())U(G);else if($.isFile()){let W=Y.extname($.name);if(B.extensions.includes(W)){K++;try{let z=X.readFileSync(G,"utf-8"),E=R.parse(z),L=N(E),j=Y.relative(A,G);if(L.canFlatten)O++,C(`${j}`);else Q++,_(`${j}: ${L.reason}`)}catch(z){Q++,I(`${Y.relative(A,G)}: ${z.message}`)}}}}}if(U(A),F(""),F(`Total: ${K} templates`),F(`${J.green}AOT compatible: ${O}${J.reset}`),Q>0)F(`${J.yellow}Require runtime: ${Q}${J.reset}`)}async function l(A,B,R){F(`${J.cyan}Watching${J.reset} ${A} for changes...`),F(`${J.dim}Press Ctrl+C to stop${J.reset}`),F("");let{compiled:K,failed:O}=await T(A,B,{...R,verbose:!0});F(""),F(`Compiled ${K} templates${O>0?`, ${O} failed`:""}`),F("");let Q=X.watch(A,{recursive:!0},async(U,M)=>{if(!M)return;let Z=Y.extname(M);if(!R.extensions.includes(Z))return;let $=Y.join(A,M);if(!X.existsSync($))return;F(`${J.dim}[${new Date().toLocaleTimeString()}]${J.reset} ${M} changed`);let G=await b($,B,A,R);if(G.success)C(`Compiled ${M}`);else I(`${M}: ${G.error}`)});process.on("SIGINT",()=>{Q.close(),F(`
98
+ Stopped watching.`),process.exit(0)})}async function n(){let A=process.argv.slice(2);if(A.length===0)w(),process.exit(0);let{command:B,source:R,options:K}=u(A);if(!B)I('No command specified. Use "compile" or "check".'),w(),process.exit(1);if(!R)I("No source path specified."),process.exit(1);let O=Y.resolve(R);if(!X.existsSync(O))I(`Source not found: ${R}`),process.exit(1);let Q=X.statSync(O).isDirectory();if(B==="check")if(Q)await r(O,K);else{let U=V(Y.dirname(O),K.extensions),M=X.readFileSync(O,"utf-8"),Z=U.parse(M),$=N(Z);if($.canFlatten)C(`${R} can be AOT compiled`);else _(`${R}: ${$.reason}`)}else if(B==="compile"){if(!K.output)I("Output directory required. Use -o <dir>"),process.exit(1);let U=Y.resolve(K.output);if(K.watch){if(!Q)I("Watch mode requires a directory, not a single file."),process.exit(1);await l(O,U,K)}else if(Q){let M=Date.now(),{compiled:Z,failed:$}=await T(O,U,K),G=Date.now()-M;if(F(""),$===0)C(`Compiled ${Z} templates in ${G}ms`);else _(`Compiled ${Z} templates, ${$} failed (${G}ms)`)}else{let M=await b(O,U,Y.dirname(O),K);if(M.success)C(`Compiled to ${M.outputPath}`);else I(M.error),process.exit(1)}}}n().catch((A)=>{I(A.message),process.exit(1)});