binja 0.1.1 → 0.2.0

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