binja 0.6.1 → 0.7.1

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