binja 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2499 @@
1
+ // @bun
2
+ // src/lexer/tokens.ts
3
+ var TokenType;
4
+ ((TokenType2) => {
5
+ TokenType2["TEXT"] = "TEXT";
6
+ TokenType2["EOF"] = "EOF";
7
+ TokenType2["VARIABLE_START"] = "VARIABLE_START";
8
+ TokenType2["VARIABLE_END"] = "VARIABLE_END";
9
+ TokenType2["BLOCK_START"] = "BLOCK_START";
10
+ TokenType2["BLOCK_END"] = "BLOCK_END";
11
+ TokenType2["COMMENT_START"] = "COMMENT_START";
12
+ TokenType2["COMMENT_END"] = "COMMENT_END";
13
+ TokenType2["NAME"] = "NAME";
14
+ TokenType2["STRING"] = "STRING";
15
+ TokenType2["NUMBER"] = "NUMBER";
16
+ TokenType2["DOT"] = "DOT";
17
+ TokenType2["COMMA"] = "COMMA";
18
+ TokenType2["COLON"] = "COLON";
19
+ TokenType2["PIPE"] = "PIPE";
20
+ TokenType2["LPAREN"] = "LPAREN";
21
+ TokenType2["RPAREN"] = "RPAREN";
22
+ TokenType2["LBRACKET"] = "LBRACKET";
23
+ TokenType2["RBRACKET"] = "RBRACKET";
24
+ TokenType2["LBRACE"] = "LBRACE";
25
+ TokenType2["RBRACE"] = "RBRACE";
26
+ TokenType2["EQ"] = "EQ";
27
+ TokenType2["NE"] = "NE";
28
+ TokenType2["LT"] = "LT";
29
+ TokenType2["GT"] = "GT";
30
+ TokenType2["LE"] = "LE";
31
+ TokenType2["GE"] = "GE";
32
+ TokenType2["ADD"] = "ADD";
33
+ TokenType2["SUB"] = "SUB";
34
+ TokenType2["MUL"] = "MUL";
35
+ TokenType2["DIV"] = "DIV";
36
+ TokenType2["MOD"] = "MOD";
37
+ TokenType2["AND"] = "AND";
38
+ TokenType2["OR"] = "OR";
39
+ TokenType2["NOT"] = "NOT";
40
+ TokenType2["ASSIGN"] = "ASSIGN";
41
+ TokenType2["TILDE"] = "TILDE";
42
+ })(TokenType ||= {});
43
+ var KEYWORDS = {
44
+ and: "AND" /* AND */,
45
+ or: "OR" /* OR */,
46
+ not: "NOT" /* NOT */,
47
+ true: "NAME" /* NAME */,
48
+ false: "NAME" /* NAME */,
49
+ True: "NAME" /* NAME */,
50
+ False: "NAME" /* NAME */,
51
+ None: "NAME" /* NAME */,
52
+ none: "NAME" /* NAME */,
53
+ is: "NAME" /* NAME */,
54
+ in: "NAME" /* NAME */
55
+ };
56
+
57
+ // src/lexer/index.ts
58
+ class Lexer {
59
+ state;
60
+ variableStart;
61
+ variableEnd;
62
+ blockStart;
63
+ blockEnd;
64
+ commentStart;
65
+ commentEnd;
66
+ constructor(source, options = {}) {
67
+ this.state = {
68
+ source,
69
+ pos: 0,
70
+ line: 1,
71
+ column: 1,
72
+ tokens: []
73
+ };
74
+ this.variableStart = options.variableStart ?? "{{";
75
+ this.variableEnd = options.variableEnd ?? "}}";
76
+ this.blockStart = options.blockStart ?? "{%";
77
+ this.blockEnd = options.blockEnd ?? "%}";
78
+ this.commentStart = options.commentStart ?? "{#";
79
+ this.commentEnd = options.commentEnd ?? "#}";
80
+ }
81
+ tokenize() {
82
+ while (!this.isAtEnd()) {
83
+ this.scanToken();
84
+ }
85
+ this.addToken("EOF" /* EOF */, "");
86
+ return this.state.tokens;
87
+ }
88
+ scanToken() {
89
+ if (this.match(this.variableStart)) {
90
+ this.addToken("VARIABLE_START" /* VARIABLE_START */, this.variableStart);
91
+ this.scanExpression(this.variableEnd, "VARIABLE_END" /* VARIABLE_END */);
92
+ return;
93
+ }
94
+ if (this.match(this.blockStart)) {
95
+ const wsControl = this.peek() === "-";
96
+ if (wsControl)
97
+ this.advance();
98
+ this.addToken("BLOCK_START" /* BLOCK_START */, this.blockStart + (wsControl ? "-" : ""));
99
+ this.scanExpression(this.blockEnd, "BLOCK_END" /* BLOCK_END */);
100
+ return;
101
+ }
102
+ if (this.match(this.commentStart)) {
103
+ this.scanComment();
104
+ return;
105
+ }
106
+ this.scanText();
107
+ }
108
+ scanText() {
109
+ const start = this.state.pos;
110
+ const startLine = this.state.line;
111
+ const startColumn = this.state.column;
112
+ while (!this.isAtEnd()) {
113
+ if (this.check(this.variableStart) || this.check(this.blockStart) || this.check(this.commentStart)) {
114
+ break;
115
+ }
116
+ if (this.peek() === `
117
+ `) {
118
+ this.state.line++;
119
+ this.state.column = 0;
120
+ }
121
+ this.advance();
122
+ }
123
+ if (this.state.pos > start) {
124
+ const text = this.state.source.slice(start, this.state.pos);
125
+ this.state.tokens.push({
126
+ type: "TEXT" /* TEXT */,
127
+ value: text,
128
+ line: startLine,
129
+ column: startColumn
130
+ });
131
+ }
132
+ }
133
+ scanExpression(endDelimiter, endTokenType) {
134
+ this.skipWhitespace();
135
+ while (!this.isAtEnd()) {
136
+ this.skipWhitespace();
137
+ if (this.peek() === "-" && this.check(endDelimiter, 1)) {
138
+ this.advance();
139
+ }
140
+ if (this.match(endDelimiter)) {
141
+ this.addToken(endTokenType, endDelimiter);
142
+ return;
143
+ }
144
+ this.scanExpressionToken();
145
+ }
146
+ throw new Error(`Unclosed template tag at line ${this.state.line}`);
147
+ }
148
+ scanExpressionToken() {
149
+ this.skipWhitespace();
150
+ if (this.isAtEnd())
151
+ return;
152
+ const c = this.peek();
153
+ if (c === '"' || c === "'") {
154
+ this.scanString(c);
155
+ return;
156
+ }
157
+ if (this.isDigit(c)) {
158
+ this.scanNumber();
159
+ return;
160
+ }
161
+ if (this.isAlpha(c) || c === "_") {
162
+ this.scanIdentifier();
163
+ return;
164
+ }
165
+ this.scanOperator();
166
+ }
167
+ scanString(quote) {
168
+ this.advance();
169
+ const start = this.state.pos;
170
+ while (!this.isAtEnd() && this.peek() !== quote) {
171
+ if (this.peek() === "\\" && this.peekNext() === quote) {
172
+ this.advance();
173
+ }
174
+ if (this.peek() === `
175
+ `) {
176
+ this.state.line++;
177
+ this.state.column = 0;
178
+ }
179
+ this.advance();
180
+ }
181
+ if (this.isAtEnd()) {
182
+ throw new Error(`Unterminated string at line ${this.state.line}`);
183
+ }
184
+ const value = this.state.source.slice(start, this.state.pos);
185
+ this.advance();
186
+ this.addToken("STRING" /* STRING */, value);
187
+ }
188
+ scanNumber() {
189
+ const start = this.state.pos;
190
+ while (this.isDigit(this.peek())) {
191
+ this.advance();
192
+ }
193
+ if (this.peek() === "." && this.isDigit(this.peekNext())) {
194
+ this.advance();
195
+ while (this.isDigit(this.peek())) {
196
+ this.advance();
197
+ }
198
+ }
199
+ const value = this.state.source.slice(start, this.state.pos);
200
+ this.addToken("NUMBER" /* NUMBER */, value);
201
+ }
202
+ scanIdentifier() {
203
+ const start = this.state.pos;
204
+ while (this.isAlphaNumeric(this.peek()) || this.peek() === "_") {
205
+ this.advance();
206
+ }
207
+ const value = this.state.source.slice(start, this.state.pos);
208
+ const type = KEYWORDS[value] ?? "NAME" /* NAME */;
209
+ this.addToken(type, value);
210
+ }
211
+ scanOperator() {
212
+ const c = this.advance();
213
+ switch (c) {
214
+ case ".":
215
+ this.addToken("DOT" /* DOT */, c);
216
+ break;
217
+ case ",":
218
+ this.addToken("COMMA" /* COMMA */, c);
219
+ break;
220
+ case ":":
221
+ this.addToken("COLON" /* COLON */, c);
222
+ break;
223
+ case "|":
224
+ this.addToken("PIPE" /* PIPE */, c);
225
+ break;
226
+ case "(":
227
+ this.addToken("LPAREN" /* LPAREN */, c);
228
+ break;
229
+ case ")":
230
+ this.addToken("RPAREN" /* RPAREN */, c);
231
+ break;
232
+ case "[":
233
+ this.addToken("LBRACKET" /* LBRACKET */, c);
234
+ break;
235
+ case "]":
236
+ this.addToken("RBRACKET" /* RBRACKET */, c);
237
+ break;
238
+ case "{":
239
+ this.addToken("LBRACE" /* LBRACE */, c);
240
+ break;
241
+ case "}":
242
+ this.addToken("RBRACE" /* RBRACE */, c);
243
+ break;
244
+ case "+":
245
+ this.addToken("ADD" /* ADD */, c);
246
+ break;
247
+ case "-":
248
+ this.addToken("SUB" /* SUB */, c);
249
+ break;
250
+ case "*":
251
+ this.addToken("MUL" /* MUL */, c);
252
+ break;
253
+ case "/":
254
+ this.addToken("DIV" /* DIV */, c);
255
+ break;
256
+ case "%":
257
+ this.addToken("MOD" /* MOD */, c);
258
+ break;
259
+ case "~":
260
+ this.addToken("TILDE" /* TILDE */, c);
261
+ break;
262
+ case "=":
263
+ if (this.match("=")) {
264
+ this.addToken("EQ" /* EQ */, "==");
265
+ } else {
266
+ this.addToken("ASSIGN" /* ASSIGN */, "=");
267
+ }
268
+ break;
269
+ case "!":
270
+ if (this.match("=")) {
271
+ this.addToken("NE" /* NE */, "!=");
272
+ } else {
273
+ throw new Error(`Unexpected character '!' at line ${this.state.line}`);
274
+ }
275
+ break;
276
+ case "<":
277
+ if (this.match("=")) {
278
+ this.addToken("LE" /* LE */, "<=");
279
+ } else {
280
+ this.addToken("LT" /* LT */, "<");
281
+ }
282
+ break;
283
+ case ">":
284
+ if (this.match("=")) {
285
+ this.addToken("GE" /* GE */, ">=");
286
+ } else {
287
+ this.addToken("GT" /* GT */, ">");
288
+ }
289
+ break;
290
+ default:
291
+ if (!this.isWhitespace(c)) {
292
+ throw new Error(`Unexpected character '${c}' at line ${this.state.line}`);
293
+ }
294
+ }
295
+ }
296
+ scanComment() {
297
+ while (!this.isAtEnd() && !this.check(this.commentEnd)) {
298
+ if (this.peek() === `
299
+ `) {
300
+ this.state.line++;
301
+ this.state.column = 0;
302
+ }
303
+ this.advance();
304
+ }
305
+ if (!this.isAtEnd()) {
306
+ this.match(this.commentEnd);
307
+ }
308
+ }
309
+ isAtEnd() {
310
+ return this.state.pos >= this.state.source.length;
311
+ }
312
+ peek() {
313
+ if (this.isAtEnd())
314
+ return "\x00";
315
+ return this.state.source[this.state.pos];
316
+ }
317
+ peekNext() {
318
+ if (this.state.pos + 1 >= this.state.source.length)
319
+ return "\x00";
320
+ return this.state.source[this.state.pos + 1];
321
+ }
322
+ advance() {
323
+ const c = this.state.source[this.state.pos];
324
+ this.state.pos++;
325
+ this.state.column++;
326
+ return c;
327
+ }
328
+ match(expected, offset = 0) {
329
+ const start = this.state.pos + offset;
330
+ if (start + expected.length > this.state.source.length)
331
+ return false;
332
+ if (this.state.source.slice(start, start + expected.length) === expected) {
333
+ if (offset === 0) {
334
+ this.state.pos += expected.length;
335
+ this.state.column += expected.length;
336
+ }
337
+ return true;
338
+ }
339
+ return false;
340
+ }
341
+ check(expected, offset = 0) {
342
+ const start = this.state.pos + offset;
343
+ if (start + expected.length > this.state.source.length)
344
+ return false;
345
+ return this.state.source.slice(start, start + expected.length) === expected;
346
+ }
347
+ skipWhitespace() {
348
+ while (!this.isAtEnd() && this.isWhitespace(this.peek())) {
349
+ if (this.peek() === `
350
+ `) {
351
+ this.state.line++;
352
+ this.state.column = 0;
353
+ }
354
+ this.advance();
355
+ }
356
+ }
357
+ isWhitespace(c) {
358
+ return c === " " || c === "\t" || c === `
359
+ ` || c === "\r";
360
+ }
361
+ isDigit(c) {
362
+ return c >= "0" && c <= "9";
363
+ }
364
+ isAlpha(c) {
365
+ return c >= "a" && c <= "z" || c >= "A" && c <= "Z";
366
+ }
367
+ isAlphaNumeric(c) {
368
+ return this.isAlpha(c) || this.isDigit(c);
369
+ }
370
+ addToken(type, value) {
371
+ this.state.tokens.push({
372
+ type,
373
+ value,
374
+ line: this.state.line,
375
+ column: this.state.column - value.length
376
+ });
377
+ }
378
+ }
379
+
380
+ // src/parser/index.ts
381
+ class Parser {
382
+ tokens;
383
+ current = 0;
384
+ constructor(tokens) {
385
+ this.tokens = tokens;
386
+ }
387
+ parse() {
388
+ const body = [];
389
+ while (!this.isAtEnd()) {
390
+ const node = this.parseStatement();
391
+ if (node)
392
+ body.push(node);
393
+ }
394
+ return {
395
+ type: "Template",
396
+ body,
397
+ line: 1,
398
+ column: 1
399
+ };
400
+ }
401
+ parseStatement() {
402
+ const token = this.peek();
403
+ switch (token.type) {
404
+ case "TEXT" /* TEXT */:
405
+ return this.parseText();
406
+ case "VARIABLE_START" /* VARIABLE_START */:
407
+ return this.parseOutput();
408
+ case "BLOCK_START" /* BLOCK_START */:
409
+ return this.parseBlock();
410
+ case "EOF" /* EOF */:
411
+ return null;
412
+ default:
413
+ this.advance();
414
+ return null;
415
+ }
416
+ }
417
+ parseText() {
418
+ const token = this.advance();
419
+ return {
420
+ type: "Text",
421
+ value: token.value,
422
+ line: token.line,
423
+ column: token.column
424
+ };
425
+ }
426
+ parseOutput() {
427
+ const start = this.advance();
428
+ const expression = this.parseExpression();
429
+ this.expect("VARIABLE_END" /* VARIABLE_END */);
430
+ return {
431
+ type: "Output",
432
+ expression,
433
+ line: start.line,
434
+ column: start.column
435
+ };
436
+ }
437
+ parseBlock() {
438
+ const start = this.advance();
439
+ const tagName = this.expect("NAME" /* NAME */);
440
+ switch (tagName.value) {
441
+ case "if":
442
+ return this.parseIf(start);
443
+ case "for":
444
+ return this.parseFor(start);
445
+ case "block":
446
+ return this.parseBlockTag(start);
447
+ case "extends":
448
+ return this.parseExtends(start);
449
+ case "include":
450
+ return this.parseInclude(start);
451
+ case "set":
452
+ return this.parseSet(start);
453
+ case "with":
454
+ return this.parseWith(start);
455
+ case "load":
456
+ return this.parseLoad(start);
457
+ case "url":
458
+ return this.parseUrl(start);
459
+ case "static":
460
+ return this.parseStatic(start);
461
+ case "comment":
462
+ return this.parseComment(start);
463
+ case "spaceless":
464
+ case "autoescape":
465
+ case "verbatim":
466
+ return this.parseSimpleBlock(start, tagName.value);
467
+ default:
468
+ this.skipToBlockEnd();
469
+ return null;
470
+ }
471
+ }
472
+ parseIf(start) {
473
+ const test = this.parseExpression();
474
+ this.expect("BLOCK_END" /* BLOCK_END */);
475
+ const body = [];
476
+ const elifs = [];
477
+ let else_ = [];
478
+ while (!this.isAtEnd()) {
479
+ if (this.checkBlockTag("elif") || this.checkBlockTag("else") || this.checkBlockTag("endif")) {
480
+ break;
481
+ }
482
+ const node = this.parseStatement();
483
+ if (node)
484
+ body.push(node);
485
+ }
486
+ while (this.checkBlockTag("elif")) {
487
+ this.advance();
488
+ this.advance();
489
+ const elifTest = this.parseExpression();
490
+ this.expect("BLOCK_END" /* BLOCK_END */);
491
+ const elifBody = [];
492
+ while (!this.isAtEnd()) {
493
+ if (this.checkBlockTag("elif") || this.checkBlockTag("else") || this.checkBlockTag("endif")) {
494
+ break;
495
+ }
496
+ const node = this.parseStatement();
497
+ if (node)
498
+ elifBody.push(node);
499
+ }
500
+ elifs.push({ test: elifTest, body: elifBody });
501
+ }
502
+ if (this.checkBlockTag("else")) {
503
+ this.advance();
504
+ this.advance();
505
+ this.expect("BLOCK_END" /* BLOCK_END */);
506
+ while (!this.isAtEnd()) {
507
+ if (this.checkBlockTag("endif"))
508
+ break;
509
+ const node = this.parseStatement();
510
+ if (node)
511
+ else_.push(node);
512
+ }
513
+ }
514
+ this.expectBlockTag("endif");
515
+ return {
516
+ type: "If",
517
+ test,
518
+ body,
519
+ elifs,
520
+ else_,
521
+ line: start.line,
522
+ column: start.column
523
+ };
524
+ }
525
+ parseFor(start) {
526
+ let target;
527
+ const firstName = this.expect("NAME" /* NAME */).value;
528
+ if (this.check("COMMA" /* COMMA */)) {
529
+ const targets = [firstName];
530
+ while (this.match("COMMA" /* COMMA */)) {
531
+ targets.push(this.expect("NAME" /* NAME */).value);
532
+ }
533
+ target = targets;
534
+ } else {
535
+ target = firstName;
536
+ }
537
+ const inToken = this.expect("NAME" /* NAME */);
538
+ if (inToken.value !== "in") {
539
+ throw this.error(`Expected 'in' in for loop, got '${inToken.value}'`);
540
+ }
541
+ const iter = this.parseExpression();
542
+ const recursive = this.check("NAME" /* NAME */) && this.peek().value === "recursive";
543
+ if (recursive)
544
+ this.advance();
545
+ this.expect("BLOCK_END" /* BLOCK_END */);
546
+ const body = [];
547
+ let else_ = [];
548
+ while (!this.isAtEnd()) {
549
+ if (this.checkBlockTag("empty") || this.checkBlockTag("else") || this.checkBlockTag("endfor")) {
550
+ break;
551
+ }
552
+ const node = this.parseStatement();
553
+ if (node)
554
+ body.push(node);
555
+ }
556
+ if (this.checkBlockTag("empty") || this.checkBlockTag("else")) {
557
+ this.advance();
558
+ this.advance();
559
+ this.expect("BLOCK_END" /* BLOCK_END */);
560
+ while (!this.isAtEnd()) {
561
+ if (this.checkBlockTag("endfor"))
562
+ break;
563
+ const node = this.parseStatement();
564
+ if (node)
565
+ else_.push(node);
566
+ }
567
+ }
568
+ this.expectBlockTag("endfor");
569
+ return {
570
+ type: "For",
571
+ target,
572
+ iter,
573
+ body,
574
+ else_,
575
+ recursive,
576
+ line: start.line,
577
+ column: start.column
578
+ };
579
+ }
580
+ parseBlockTag(start) {
581
+ const name = this.expect("NAME" /* NAME */).value;
582
+ const scoped = this.check("NAME" /* NAME */) && this.peek().value === "scoped";
583
+ if (scoped)
584
+ this.advance();
585
+ this.expect("BLOCK_END" /* BLOCK_END */);
586
+ const body = [];
587
+ while (!this.isAtEnd()) {
588
+ if (this.checkBlockTag("endblock"))
589
+ break;
590
+ const node = this.parseStatement();
591
+ if (node)
592
+ body.push(node);
593
+ }
594
+ this.advance();
595
+ this.advance();
596
+ if (this.check("NAME" /* NAME */))
597
+ this.advance();
598
+ this.expect("BLOCK_END" /* BLOCK_END */);
599
+ return {
600
+ type: "Block",
601
+ name,
602
+ body,
603
+ scoped,
604
+ line: start.line,
605
+ column: start.column
606
+ };
607
+ }
608
+ parseExtends(start) {
609
+ const template = this.parseExpression();
610
+ this.expect("BLOCK_END" /* BLOCK_END */);
611
+ return {
612
+ type: "Extends",
613
+ template,
614
+ line: start.line,
615
+ column: start.column
616
+ };
617
+ }
618
+ parseInclude(start) {
619
+ const template = this.parseExpression();
620
+ let context = null;
621
+ let only = false;
622
+ let ignoreMissing = false;
623
+ while (this.check("NAME" /* NAME */)) {
624
+ const modifier = this.peek().value;
625
+ if (modifier === "ignore" && this.peekNext()?.value === "missing") {
626
+ this.advance();
627
+ this.advance();
628
+ ignoreMissing = true;
629
+ } else if (modifier === "with") {
630
+ this.advance();
631
+ context = this.parseKeywordArgs();
632
+ } else if (modifier === "only") {
633
+ this.advance();
634
+ only = true;
635
+ } else if (modifier === "without") {
636
+ this.advance();
637
+ if (this.check("NAME" /* NAME */) && this.peek().value === "context") {
638
+ this.advance();
639
+ only = true;
640
+ }
641
+ } else {
642
+ break;
643
+ }
644
+ }
645
+ this.expect("BLOCK_END" /* BLOCK_END */);
646
+ return {
647
+ type: "Include",
648
+ template,
649
+ context,
650
+ only,
651
+ ignoreMissing,
652
+ line: start.line,
653
+ column: start.column
654
+ };
655
+ }
656
+ parseSet(start) {
657
+ const target = this.expect("NAME" /* NAME */).value;
658
+ this.expect("ASSIGN" /* ASSIGN */);
659
+ const value = this.parseExpression();
660
+ this.expect("BLOCK_END" /* BLOCK_END */);
661
+ return {
662
+ type: "Set",
663
+ target,
664
+ value,
665
+ line: start.line,
666
+ column: start.column
667
+ };
668
+ }
669
+ parseWith(start) {
670
+ const assignments = [];
671
+ do {
672
+ const target = this.expect("NAME" /* NAME */).value;
673
+ this.expect("ASSIGN" /* ASSIGN */);
674
+ const value = this.parseExpression();
675
+ assignments.push({ target, value });
676
+ } while (this.match("COMMA" /* COMMA */) || this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */);
677
+ this.expect("BLOCK_END" /* BLOCK_END */);
678
+ const body = [];
679
+ while (!this.isAtEnd()) {
680
+ if (this.checkBlockTag("endwith"))
681
+ break;
682
+ const node = this.parseStatement();
683
+ if (node)
684
+ body.push(node);
685
+ }
686
+ this.expectBlockTag("endwith");
687
+ return {
688
+ type: "With",
689
+ assignments,
690
+ body,
691
+ line: start.line,
692
+ column: start.column
693
+ };
694
+ }
695
+ parseLoad(start) {
696
+ const names = [];
697
+ while (this.check("NAME" /* NAME */)) {
698
+ names.push(this.advance().value);
699
+ }
700
+ this.expect("BLOCK_END" /* BLOCK_END */);
701
+ return {
702
+ type: "Load",
703
+ names,
704
+ line: start.line,
705
+ column: start.column
706
+ };
707
+ }
708
+ parseUrl(start) {
709
+ const name = this.parseExpression();
710
+ const args = [];
711
+ const kwargs = {};
712
+ let asVar = null;
713
+ while (!this.check("BLOCK_END" /* BLOCK_END */)) {
714
+ if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
715
+ this.advance();
716
+ asVar = this.expect("NAME" /* NAME */).value;
717
+ break;
718
+ }
719
+ if (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
720
+ const key = this.advance().value;
721
+ this.advance();
722
+ kwargs[key] = this.parseExpression();
723
+ } else {
724
+ args.push(this.parseExpression());
725
+ }
726
+ }
727
+ this.expect("BLOCK_END" /* BLOCK_END */);
728
+ return {
729
+ type: "Url",
730
+ name,
731
+ args,
732
+ kwargs,
733
+ asVar,
734
+ line: start.line,
735
+ column: start.column
736
+ };
737
+ }
738
+ parseStatic(start) {
739
+ const path = this.parseExpression();
740
+ let asVar = null;
741
+ if (this.check("NAME" /* NAME */) && this.peek().value === "as") {
742
+ this.advance();
743
+ asVar = this.expect("NAME" /* NAME */).value;
744
+ }
745
+ this.expect("BLOCK_END" /* BLOCK_END */);
746
+ return {
747
+ type: "Static",
748
+ path,
749
+ asVar,
750
+ line: start.line,
751
+ column: start.column
752
+ };
753
+ }
754
+ parseComment(start) {
755
+ this.expect("BLOCK_END" /* BLOCK_END */);
756
+ while (!this.isAtEnd()) {
757
+ if (this.checkBlockTag("endcomment"))
758
+ break;
759
+ this.advance();
760
+ }
761
+ this.expectBlockTag("endcomment");
762
+ return null;
763
+ }
764
+ parseSimpleBlock(start, tagName) {
765
+ this.skipToBlockEnd();
766
+ const endTag = `end${tagName}`;
767
+ while (!this.isAtEnd()) {
768
+ if (this.checkBlockTag(endTag))
769
+ break;
770
+ this.advance();
771
+ }
772
+ if (this.checkBlockTag(endTag)) {
773
+ this.advance();
774
+ this.advance();
775
+ this.expect("BLOCK_END" /* BLOCK_END */);
776
+ }
777
+ return null;
778
+ }
779
+ parseExpression() {
780
+ return this.parseConditional();
781
+ }
782
+ parseConditional() {
783
+ let expr = this.parseOr();
784
+ if (this.check("NAME" /* NAME */) && this.peek().value === "if") {
785
+ this.advance();
786
+ const test = this.parseOr();
787
+ this.expectName("else");
788
+ const falseExpr = this.parseConditional();
789
+ expr = {
790
+ type: "Conditional",
791
+ test,
792
+ trueExpr: expr,
793
+ falseExpr,
794
+ line: expr.line,
795
+ column: expr.column
796
+ };
797
+ }
798
+ return expr;
799
+ }
800
+ parseOr() {
801
+ let left = this.parseAnd();
802
+ while (this.check("OR" /* OR */) || this.check("NAME" /* NAME */) && this.peek().value === "or") {
803
+ this.advance();
804
+ const right = this.parseAnd();
805
+ left = {
806
+ type: "BinaryOp",
807
+ operator: "or",
808
+ left,
809
+ right,
810
+ line: left.line,
811
+ column: left.column
812
+ };
813
+ }
814
+ return left;
815
+ }
816
+ parseAnd() {
817
+ let left = this.parseNot();
818
+ while (this.check("AND" /* AND */) || this.check("NAME" /* NAME */) && this.peek().value === "and") {
819
+ this.advance();
820
+ const right = this.parseNot();
821
+ left = {
822
+ type: "BinaryOp",
823
+ operator: "and",
824
+ left,
825
+ right,
826
+ line: left.line,
827
+ column: left.column
828
+ };
829
+ }
830
+ return left;
831
+ }
832
+ parseNot() {
833
+ if (this.check("NOT" /* NOT */) || this.check("NAME" /* NAME */) && this.peek().value === "not") {
834
+ const op = this.advance();
835
+ const operand = this.parseNot();
836
+ return {
837
+ type: "UnaryOp",
838
+ operator: "not",
839
+ operand,
840
+ line: op.line,
841
+ column: op.column
842
+ };
843
+ }
844
+ return this.parseCompare();
845
+ }
846
+ parseCompare() {
847
+ let left = this.parseAddSub();
848
+ const ops = [];
849
+ while (true) {
850
+ let operator = null;
851
+ if (this.match("EQ" /* EQ */))
852
+ operator = "==";
853
+ else if (this.match("NE" /* NE */))
854
+ operator = "!=";
855
+ else if (this.match("LT" /* LT */))
856
+ operator = "<";
857
+ else if (this.match("GT" /* GT */))
858
+ operator = ">";
859
+ else if (this.match("LE" /* LE */))
860
+ operator = "<=";
861
+ else if (this.match("GE" /* GE */))
862
+ operator = ">=";
863
+ else if (this.check("NAME" /* NAME */)) {
864
+ const name = this.peek().value;
865
+ if (name === "in") {
866
+ this.advance();
867
+ operator = "in";
868
+ } else if (name === "not" && this.peekNext()?.value === "in") {
869
+ this.advance();
870
+ this.advance();
871
+ operator = "not in";
872
+ } else if (name === "is") {
873
+ this.advance();
874
+ const negated = this.check("NOT" /* NOT */);
875
+ if (negated) {
876
+ this.advance();
877
+ }
878
+ const testToken = this.expect("NAME" /* NAME */);
879
+ const testName = testToken.value;
880
+ const args = [];
881
+ if (this.match("LPAREN" /* LPAREN */)) {
882
+ while (!this.check("RPAREN" /* RPAREN */)) {
883
+ args.push(this.parseExpression());
884
+ if (!this.check("RPAREN" /* RPAREN */)) {
885
+ this.expect("COMMA" /* COMMA */);
886
+ }
887
+ }
888
+ this.expect("RPAREN" /* RPAREN */);
889
+ }
890
+ left = {
891
+ type: "TestExpr",
892
+ node: left,
893
+ test: testName,
894
+ args,
895
+ negated,
896
+ line: left.line,
897
+ column: left.column
898
+ };
899
+ continue;
900
+ }
901
+ }
902
+ if (!operator)
903
+ break;
904
+ const right = this.parseAddSub();
905
+ ops.push({ operator, right });
906
+ }
907
+ if (ops.length === 0)
908
+ return left;
909
+ return {
910
+ type: "Compare",
911
+ left,
912
+ ops,
913
+ line: left.line,
914
+ column: left.column
915
+ };
916
+ }
917
+ parseAddSub() {
918
+ let left = this.parseMulDiv();
919
+ while (this.check("ADD" /* ADD */) || this.check("SUB" /* SUB */) || this.check("TILDE" /* TILDE */)) {
920
+ const op = this.advance();
921
+ const right = this.parseMulDiv();
922
+ left = {
923
+ type: "BinaryOp",
924
+ operator: op.value,
925
+ left,
926
+ right,
927
+ line: left.line,
928
+ column: left.column
929
+ };
930
+ }
931
+ return left;
932
+ }
933
+ parseMulDiv() {
934
+ let left = this.parseUnary();
935
+ while (this.check("MUL" /* MUL */) || this.check("DIV" /* DIV */) || this.check("MOD" /* MOD */)) {
936
+ const op = this.advance();
937
+ const right = this.parseUnary();
938
+ left = {
939
+ type: "BinaryOp",
940
+ operator: op.value,
941
+ left,
942
+ right,
943
+ line: left.line,
944
+ column: left.column
945
+ };
946
+ }
947
+ return left;
948
+ }
949
+ parseUnary() {
950
+ if (this.check("SUB" /* SUB */) || this.check("ADD" /* ADD */)) {
951
+ const op = this.advance();
952
+ const operand = this.parseUnary();
953
+ return {
954
+ type: "UnaryOp",
955
+ operator: op.value,
956
+ operand,
957
+ line: op.line,
958
+ column: op.column
959
+ };
960
+ }
961
+ return this.parseFilter();
962
+ }
963
+ parseFilter() {
964
+ let node = this.parsePostfix();
965
+ while (this.match("PIPE" /* PIPE */)) {
966
+ const filterName = this.expect("NAME" /* NAME */).value;
967
+ const args = [];
968
+ const kwargs = {};
969
+ if (this.match("COLON" /* COLON */)) {
970
+ if (this.check("SUB" /* SUB */) || this.check("ADD" /* ADD */)) {
971
+ const op = this.advance();
972
+ const operand = this.parsePostfix();
973
+ args.push({
974
+ type: "UnaryOp",
975
+ operator: op.value,
976
+ operand,
977
+ line: op.line,
978
+ column: op.column
979
+ });
980
+ } else {
981
+ args.push(this.parsePostfix());
982
+ }
983
+ } else if (this.match("LPAREN" /* LPAREN */)) {
984
+ while (!this.check("RPAREN" /* RPAREN */)) {
985
+ if (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
986
+ const key = this.advance().value;
987
+ this.advance();
988
+ kwargs[key] = this.parseExpression();
989
+ } else {
990
+ args.push(this.parseExpression());
991
+ }
992
+ if (!this.check("RPAREN" /* RPAREN */)) {
993
+ this.expect("COMMA" /* COMMA */);
994
+ }
995
+ }
996
+ this.expect("RPAREN" /* RPAREN */);
997
+ }
998
+ node = {
999
+ type: "FilterExpr",
1000
+ node,
1001
+ filter: filterName,
1002
+ args,
1003
+ kwargs,
1004
+ line: node.line,
1005
+ column: node.column
1006
+ };
1007
+ }
1008
+ return node;
1009
+ }
1010
+ parsePostfix() {
1011
+ let node = this.parsePrimary();
1012
+ while (true) {
1013
+ if (this.match("DOT" /* DOT */)) {
1014
+ let attr;
1015
+ if (this.check("NUMBER" /* NUMBER */)) {
1016
+ attr = this.advance().value;
1017
+ } else {
1018
+ attr = this.expect("NAME" /* NAME */).value;
1019
+ }
1020
+ node = {
1021
+ type: "GetAttr",
1022
+ object: node,
1023
+ attribute: attr,
1024
+ line: node.line,
1025
+ column: node.column
1026
+ };
1027
+ } else if (this.match("LBRACKET" /* LBRACKET */)) {
1028
+ const index = this.parseExpression();
1029
+ this.expect("RBRACKET" /* RBRACKET */);
1030
+ node = {
1031
+ type: "GetItem",
1032
+ object: node,
1033
+ index,
1034
+ line: node.line,
1035
+ column: node.column
1036
+ };
1037
+ } else if (this.match("LPAREN" /* LPAREN */)) {
1038
+ const args = [];
1039
+ const kwargs = {};
1040
+ while (!this.check("RPAREN" /* RPAREN */)) {
1041
+ if (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
1042
+ const key = this.advance().value;
1043
+ this.advance();
1044
+ kwargs[key] = this.parseExpression();
1045
+ } else {
1046
+ args.push(this.parseExpression());
1047
+ }
1048
+ if (!this.check("RPAREN" /* RPAREN */)) {
1049
+ this.expect("COMMA" /* COMMA */);
1050
+ }
1051
+ }
1052
+ this.expect("RPAREN" /* RPAREN */);
1053
+ node = {
1054
+ type: "FunctionCall",
1055
+ callee: node,
1056
+ args,
1057
+ kwargs,
1058
+ line: node.line,
1059
+ column: node.column
1060
+ };
1061
+ } else {
1062
+ break;
1063
+ }
1064
+ }
1065
+ return node;
1066
+ }
1067
+ parsePrimary() {
1068
+ const token = this.peek();
1069
+ if (this.match("STRING" /* STRING */)) {
1070
+ return {
1071
+ type: "Literal",
1072
+ value: token.value,
1073
+ line: token.line,
1074
+ column: token.column
1075
+ };
1076
+ }
1077
+ if (this.match("NUMBER" /* NUMBER */)) {
1078
+ const value = token.value.includes(".") ? parseFloat(token.value) : parseInt(token.value, 10);
1079
+ return {
1080
+ type: "Literal",
1081
+ value,
1082
+ line: token.line,
1083
+ column: token.column
1084
+ };
1085
+ }
1086
+ if (this.check("NAME" /* NAME */)) {
1087
+ const name = this.advance().value;
1088
+ if (name === "true" || name === "True") {
1089
+ return { type: "Literal", value: true, line: token.line, column: token.column };
1090
+ }
1091
+ if (name === "false" || name === "False") {
1092
+ return { type: "Literal", value: false, line: token.line, column: token.column };
1093
+ }
1094
+ if (name === "none" || name === "None" || name === "null") {
1095
+ return { type: "Literal", value: null, line: token.line, column: token.column };
1096
+ }
1097
+ return { type: "Name", name, line: token.line, column: token.column };
1098
+ }
1099
+ if (this.match("LPAREN" /* LPAREN */)) {
1100
+ const expr = this.parseExpression();
1101
+ this.expect("RPAREN" /* RPAREN */);
1102
+ return expr;
1103
+ }
1104
+ if (this.match("LBRACKET" /* LBRACKET */)) {
1105
+ const elements = [];
1106
+ while (!this.check("RBRACKET" /* RBRACKET */)) {
1107
+ elements.push(this.parseExpression());
1108
+ if (!this.check("RBRACKET" /* RBRACKET */)) {
1109
+ this.expect("COMMA" /* COMMA */);
1110
+ }
1111
+ }
1112
+ this.expect("RBRACKET" /* RBRACKET */);
1113
+ return { type: "Array", elements, line: token.line, column: token.column };
1114
+ }
1115
+ if (this.match("LBRACE" /* LBRACE */)) {
1116
+ const pairs = [];
1117
+ while (!this.check("RBRACE" /* RBRACE */)) {
1118
+ const key = this.parseExpression();
1119
+ this.expect("COLON" /* COLON */);
1120
+ const value = this.parseExpression();
1121
+ pairs.push({ key, value });
1122
+ if (!this.check("RBRACE" /* RBRACE */)) {
1123
+ this.expect("COMMA" /* COMMA */);
1124
+ }
1125
+ }
1126
+ this.expect("RBRACE" /* RBRACE */);
1127
+ return { type: "Object", pairs, line: token.line, column: token.column };
1128
+ }
1129
+ throw this.error(`Unexpected token: ${token.type} (${token.value})`);
1130
+ }
1131
+ parseKeywordArgs() {
1132
+ const kwargs = {};
1133
+ while (this.check("NAME" /* NAME */) && this.peekNext()?.type === "ASSIGN" /* ASSIGN */) {
1134
+ const key = this.advance().value;
1135
+ this.advance();
1136
+ kwargs[key] = this.parseExpression();
1137
+ }
1138
+ return kwargs;
1139
+ }
1140
+ checkBlockTag(name) {
1141
+ if (this.peek().type !== "BLOCK_START" /* BLOCK_START */)
1142
+ return false;
1143
+ const saved = this.current;
1144
+ this.advance();
1145
+ const isMatch = this.check("NAME" /* NAME */) && this.peek().value === name;
1146
+ this.current = saved;
1147
+ return isMatch;
1148
+ }
1149
+ expectBlockTag(name) {
1150
+ this.advance();
1151
+ const tag = this.expect("NAME" /* NAME */);
1152
+ if (tag.value !== name) {
1153
+ throw this.error(`Expected '${name}', got '${tag.value}'`);
1154
+ }
1155
+ this.expect("BLOCK_END" /* BLOCK_END */);
1156
+ }
1157
+ expectName(name) {
1158
+ const token = this.expect("NAME" /* NAME */);
1159
+ if (token.value !== name) {
1160
+ throw this.error(`Expected '${name}', got '${token.value}'`);
1161
+ }
1162
+ }
1163
+ skipToBlockEnd() {
1164
+ while (!this.isAtEnd() && !this.check("BLOCK_END" /* BLOCK_END */)) {
1165
+ this.advance();
1166
+ }
1167
+ if (this.check("BLOCK_END" /* BLOCK_END */)) {
1168
+ this.advance();
1169
+ }
1170
+ }
1171
+ isAtEnd() {
1172
+ return this.peek().type === "EOF" /* EOF */;
1173
+ }
1174
+ peek() {
1175
+ return this.tokens[this.current];
1176
+ }
1177
+ peekNext() {
1178
+ if (this.current + 1 >= this.tokens.length)
1179
+ return null;
1180
+ return this.tokens[this.current + 1];
1181
+ }
1182
+ advance() {
1183
+ if (!this.isAtEnd())
1184
+ this.current++;
1185
+ return this.tokens[this.current - 1];
1186
+ }
1187
+ check(type) {
1188
+ if (this.isAtEnd())
1189
+ return false;
1190
+ return this.peek().type === type;
1191
+ }
1192
+ match(type) {
1193
+ if (this.check(type)) {
1194
+ this.advance();
1195
+ return true;
1196
+ }
1197
+ return false;
1198
+ }
1199
+ expect(type) {
1200
+ if (this.check(type))
1201
+ return this.advance();
1202
+ const token = this.peek();
1203
+ throw this.error(`Expected ${type}, got ${token.type} (${token.value})`);
1204
+ }
1205
+ error(message) {
1206
+ const token = this.peek();
1207
+ return new Error(`Parse error at line ${token.line}, column ${token.column}: ${message}`);
1208
+ }
1209
+ }
1210
+
1211
+ // src/runtime/context.ts
1212
+ class Context {
1213
+ scopes = [];
1214
+ parent = null;
1215
+ _forloopStack = [];
1216
+ _lastCycleValue = null;
1217
+ constructor(data = {}, parent = null) {
1218
+ this.parent = parent;
1219
+ this.scopes.push(new Map(Object.entries(data)));
1220
+ }
1221
+ get(name) {
1222
+ if (name === "forloop" || name === "loop") {
1223
+ return this._forloopStack[this._forloopStack.length - 1] || null;
1224
+ }
1225
+ for (let i = this.scopes.length - 1;i >= 0; i--) {
1226
+ if (this.scopes[i].has(name)) {
1227
+ return this.scopes[i].get(name);
1228
+ }
1229
+ }
1230
+ if (this.parent) {
1231
+ return this.parent.get(name);
1232
+ }
1233
+ return;
1234
+ }
1235
+ set(name, value) {
1236
+ this.scopes[this.scopes.length - 1].set(name, value);
1237
+ }
1238
+ has(name) {
1239
+ for (let i = this.scopes.length - 1;i >= 0; i--) {
1240
+ if (this.scopes[i].has(name))
1241
+ return true;
1242
+ }
1243
+ return this.parent ? this.parent.has(name) : false;
1244
+ }
1245
+ push(data = {}) {
1246
+ this.scopes.push(new Map(Object.entries(data)));
1247
+ }
1248
+ pop() {
1249
+ if (this.scopes.length > 1) {
1250
+ this.scopes.pop();
1251
+ }
1252
+ }
1253
+ derived(data = {}) {
1254
+ return new Context(data, this);
1255
+ }
1256
+ pushForLoop(items, index) {
1257
+ const length = items.length;
1258
+ const depth = this._forloopStack.length + 1;
1259
+ const forloop = {
1260
+ counter: index + 1,
1261
+ counter0: index,
1262
+ revcounter: length - index,
1263
+ revcounter0: length - index - 1,
1264
+ first: index === 0,
1265
+ last: index === length - 1,
1266
+ length,
1267
+ index: index + 1,
1268
+ index0: index,
1269
+ revindex: length - index,
1270
+ revindex0: length - index - 1,
1271
+ depth,
1272
+ depth0: depth - 1,
1273
+ previtem: index > 0 ? items[index - 1] : undefined,
1274
+ nextitem: index < length - 1 ? items[index + 1] : undefined,
1275
+ cycle: (...args) => args[index % args.length],
1276
+ changed: (value) => {
1277
+ const changed = value !== this._lastCycleValue;
1278
+ this._lastCycleValue = value;
1279
+ return changed;
1280
+ }
1281
+ };
1282
+ if (this._forloopStack.length > 0) {
1283
+ forloop.parentloop = this._forloopStack[this._forloopStack.length - 1];
1284
+ }
1285
+ this._forloopStack.push(forloop);
1286
+ return forloop;
1287
+ }
1288
+ popForLoop() {
1289
+ this._forloopStack.pop();
1290
+ }
1291
+ updateForLoop(index, items) {
1292
+ const forloop = this._forloopStack[this._forloopStack.length - 1];
1293
+ if (!forloop)
1294
+ return;
1295
+ const length = items.length;
1296
+ forloop.counter = index + 1;
1297
+ forloop.counter0 = index;
1298
+ forloop.revcounter = length - index;
1299
+ forloop.revcounter0 = length - index - 1;
1300
+ forloop.first = index === 0;
1301
+ forloop.last = index === length - 1;
1302
+ forloop.index = index + 1;
1303
+ forloop.index0 = index;
1304
+ forloop.revindex = length - index;
1305
+ forloop.revindex0 = length - index - 1;
1306
+ forloop.previtem = index > 0 ? items[index - 1] : undefined;
1307
+ forloop.nextitem = index < length - 1 ? items[index + 1] : undefined;
1308
+ }
1309
+ toObject() {
1310
+ const result = {};
1311
+ if (this.parent) {
1312
+ Object.assign(result, this.parent.toObject());
1313
+ }
1314
+ for (const scope of this.scopes) {
1315
+ for (const [key, value] of scope) {
1316
+ result[key] = value;
1317
+ }
1318
+ }
1319
+ return result;
1320
+ }
1321
+ }
1322
+
1323
+ // src/filters/index.ts
1324
+ var upper = (value) => String(value).toUpperCase();
1325
+ var lower = (value) => String(value).toLowerCase();
1326
+ var capitalize = (value) => {
1327
+ const str = String(value);
1328
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1329
+ };
1330
+ var capfirst = (value) => {
1331
+ const str = String(value);
1332
+ return str.charAt(0).toUpperCase() + str.slice(1);
1333
+ };
1334
+ var title = (value) => String(value).replace(/\b\w/g, (c) => c.toUpperCase());
1335
+ var trim = (value) => String(value).trim();
1336
+ var striptags = (value) => String(value).replace(/<[^>]*>/g, "");
1337
+ var escape = (value) => {
1338
+ if (value?.__safe__)
1339
+ return value;
1340
+ const escaped = String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
1341
+ const safeString = new String(escaped);
1342
+ safeString.__safe__ = true;
1343
+ return safeString;
1344
+ };
1345
+ var safe = (value) => {
1346
+ const safeString = new String(value);
1347
+ safeString.__safe__ = true;
1348
+ return safeString;
1349
+ };
1350
+ var escapejs = (value) => JSON.stringify(String(value)).slice(1, -1);
1351
+ var linebreaks = (value) => {
1352
+ const str = String(value);
1353
+ const paragraphs = str.split(/\n\n+/);
1354
+ const html = paragraphs.map((p) => `<p>${p.replace(/\n/g, "<br>")}</p>`).join(`
1355
+ `);
1356
+ const safeString = new String(html);
1357
+ safeString.__safe__ = true;
1358
+ return safeString;
1359
+ };
1360
+ var linebreaksbr = (value) => {
1361
+ const html = String(value).replace(/\n/g, "<br>");
1362
+ const safeString = new String(html);
1363
+ safeString.__safe__ = true;
1364
+ return safeString;
1365
+ };
1366
+ var truncatechars = (value, length = 30) => {
1367
+ const str = String(value);
1368
+ if (str.length <= length)
1369
+ return str;
1370
+ return str.slice(0, length - 3) + "...";
1371
+ };
1372
+ var truncatewords = (value, count = 15) => {
1373
+ const words = String(value).split(/\s+/);
1374
+ if (words.length <= count)
1375
+ return value;
1376
+ return words.slice(0, count).join(" ") + "...";
1377
+ };
1378
+ var wordcount = (value) => String(value).split(/\s+/).filter(Boolean).length;
1379
+ var center = (value, width = 80) => {
1380
+ const str = String(value);
1381
+ const padding = Math.max(0, width - str.length);
1382
+ const left = Math.floor(padding / 2);
1383
+ const right = padding - left;
1384
+ return " ".repeat(left) + str + " ".repeat(right);
1385
+ };
1386
+ var ljust = (value, width = 80) => String(value).padEnd(width);
1387
+ var rjust = (value, width = 80) => String(value).padStart(width);
1388
+ var cut = (value, arg = "") => String(value).split(arg).join("");
1389
+ var slugify = (value) => String(value).toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1390
+ var abs = (value) => Math.abs(Number(value));
1391
+ var round = (value, precision = 0) => Number(Number(value).toFixed(precision));
1392
+ var int = (value) => parseInt(String(value), 10) || 0;
1393
+ var float = (value) => parseFloat(String(value)) || 0;
1394
+ var floatformat = (value, decimals = -1) => {
1395
+ const num = parseFloat(String(value));
1396
+ if (isNaN(num))
1397
+ return "";
1398
+ if (decimals === -1) {
1399
+ const formatted = num.toFixed(1);
1400
+ return formatted.endsWith(".0") ? Math.round(num).toString() : formatted;
1401
+ }
1402
+ return num.toFixed(Math.abs(decimals));
1403
+ };
1404
+ var add = (value, arg) => {
1405
+ const numValue = Number(value);
1406
+ const numArg = Number(arg);
1407
+ if (!isNaN(numValue) && !isNaN(numArg)) {
1408
+ return numValue + numArg;
1409
+ }
1410
+ return String(value) + String(arg);
1411
+ };
1412
+ var divisibleby = (value, arg) => Number(value) % Number(arg) === 0;
1413
+ var filesizeformat = (value) => {
1414
+ const bytes = Number(value);
1415
+ const units = ["bytes", "KB", "MB", "GB", "TB", "PB"];
1416
+ let i = 0;
1417
+ let size = bytes;
1418
+ while (size >= 1024 && i < units.length - 1) {
1419
+ size /= 1024;
1420
+ i++;
1421
+ }
1422
+ return `${size.toFixed(1)} ${units[i]}`;
1423
+ };
1424
+ var length = (value) => {
1425
+ if (value == null)
1426
+ return 0;
1427
+ if (typeof value === "string" || Array.isArray(value))
1428
+ return value.length;
1429
+ if (typeof value === "object")
1430
+ return Object.keys(value).length;
1431
+ return 0;
1432
+ };
1433
+ var length_is = (value, len) => length(value) === Number(len);
1434
+ var first = (value) => {
1435
+ if (Array.isArray(value))
1436
+ return value[0];
1437
+ if (typeof value === "string")
1438
+ return value[0];
1439
+ return value;
1440
+ };
1441
+ var last = (value) => {
1442
+ if (Array.isArray(value))
1443
+ return value[value.length - 1];
1444
+ if (typeof value === "string")
1445
+ return value[value.length - 1];
1446
+ return value;
1447
+ };
1448
+ var join = (value, separator = "") => {
1449
+ if (Array.isArray(value))
1450
+ return value.join(separator);
1451
+ return String(value);
1452
+ };
1453
+ var slice = (value, arg) => {
1454
+ if (!value)
1455
+ return value;
1456
+ const [startStr, endStr] = String(arg).split(":");
1457
+ const start = startStr ? parseInt(startStr, 10) : 0;
1458
+ const end = endStr ? parseInt(endStr, 10) : undefined;
1459
+ if (Array.isArray(value) || typeof value === "string") {
1460
+ return value.slice(start, end);
1461
+ }
1462
+ return value;
1463
+ };
1464
+ var reverse = (value) => {
1465
+ if (Array.isArray(value))
1466
+ return [...value].reverse();
1467
+ if (typeof value === "string")
1468
+ return value.split("").reverse().join("");
1469
+ return value;
1470
+ };
1471
+ var sort = (value, reverse2 = false) => {
1472
+ if (!Array.isArray(value))
1473
+ return value;
1474
+ const sorted = [...value].sort();
1475
+ return reverse2 ? sorted.reverse() : sorted;
1476
+ };
1477
+ var unique = (value) => {
1478
+ if (Array.isArray(value))
1479
+ return [...new Set(value)];
1480
+ return value;
1481
+ };
1482
+ var make_list = (value) => {
1483
+ if (Array.isArray(value))
1484
+ return value;
1485
+ return String(value).split("");
1486
+ };
1487
+ var dictsort = (value, key) => {
1488
+ if (!Array.isArray(value))
1489
+ return value;
1490
+ return [...value].sort((a, b) => {
1491
+ const aVal = key ? a[key] : a;
1492
+ const bVal = key ? b[key] : b;
1493
+ return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
1494
+ });
1495
+ };
1496
+ var dictsortreversed = (value, key) => {
1497
+ const sorted = dictsort(value, key);
1498
+ return Array.isArray(sorted) ? sorted.reverse() : sorted;
1499
+ };
1500
+ var columns = (value, cols) => {
1501
+ if (!Array.isArray(value))
1502
+ return [[value]];
1503
+ const result = [];
1504
+ const numCols = Number(cols) || 2;
1505
+ for (let i = 0;i < value.length; i += numCols) {
1506
+ result.push(value.slice(i, i + numCols));
1507
+ }
1508
+ return result;
1509
+ };
1510
+ var date = (value, format = "N j, Y") => {
1511
+ const d = value instanceof Date ? value : new Date(value);
1512
+ if (isNaN(d.getTime()))
1513
+ return "";
1514
+ const formatMap = {
1515
+ d: () => String(d.getDate()).padStart(2, "0"),
1516
+ j: () => String(d.getDate()),
1517
+ D: () => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()],
1518
+ l: () => ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][d.getDay()],
1519
+ m: () => String(d.getMonth() + 1).padStart(2, "0"),
1520
+ n: () => String(d.getMonth() + 1),
1521
+ M: () => ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()],
1522
+ F: () => [
1523
+ "January",
1524
+ "February",
1525
+ "March",
1526
+ "April",
1527
+ "May",
1528
+ "June",
1529
+ "July",
1530
+ "August",
1531
+ "September",
1532
+ "October",
1533
+ "November",
1534
+ "December"
1535
+ ][d.getMonth()],
1536
+ N: () => [
1537
+ "Jan.",
1538
+ "Feb.",
1539
+ "March",
1540
+ "April",
1541
+ "May",
1542
+ "June",
1543
+ "July",
1544
+ "Aug.",
1545
+ "Sept.",
1546
+ "Oct.",
1547
+ "Nov.",
1548
+ "Dec."
1549
+ ][d.getMonth()],
1550
+ y: () => String(d.getFullYear()).slice(-2),
1551
+ Y: () => String(d.getFullYear()),
1552
+ H: () => String(d.getHours()).padStart(2, "0"),
1553
+ G: () => String(d.getHours()),
1554
+ i: () => String(d.getMinutes()).padStart(2, "0"),
1555
+ s: () => String(d.getSeconds()).padStart(2, "0"),
1556
+ a: () => d.getHours() < 12 ? "a.m." : "p.m.",
1557
+ A: () => d.getHours() < 12 ? "AM" : "PM",
1558
+ g: () => String(d.getHours() % 12 || 12),
1559
+ h: () => String(d.getHours() % 12 || 12).padStart(2, "0")
1560
+ };
1561
+ return format.replace(/[a-zA-Z]/g, (char) => formatMap[char]?.() ?? char);
1562
+ };
1563
+ var time = (value, format = "H:i") => date(value, format);
1564
+ var timesince = (value, now = new Date) => {
1565
+ const d = value instanceof Date ? value : new Date(value);
1566
+ const diff = (new Date(now).getTime() - d.getTime()) / 1000;
1567
+ const intervals = [
1568
+ [31536000, "year", "years"],
1569
+ [2592000, "month", "months"],
1570
+ [604800, "week", "weeks"],
1571
+ [86400, "day", "days"],
1572
+ [3600, "hour", "hours"],
1573
+ [60, "minute", "minutes"]
1574
+ ];
1575
+ for (const [seconds, singular, plural] of intervals) {
1576
+ const count = Math.floor(diff / seconds);
1577
+ if (count >= 1) {
1578
+ return `${count} ${count === 1 ? singular : plural}`;
1579
+ }
1580
+ }
1581
+ return "just now";
1582
+ };
1583
+ var timeuntil = (value, now = new Date) => {
1584
+ const d = value instanceof Date ? value : new Date(value);
1585
+ const diff = (d.getTime() - new Date(now).getTime()) / 1000;
1586
+ const intervals = [
1587
+ [31536000, "year", "years"],
1588
+ [2592000, "month", "months"],
1589
+ [604800, "week", "weeks"],
1590
+ [86400, "day", "days"],
1591
+ [3600, "hour", "hours"],
1592
+ [60, "minute", "minutes"]
1593
+ ];
1594
+ for (const [seconds, singular, plural] of intervals) {
1595
+ const count = Math.floor(diff / seconds);
1596
+ if (count >= 1) {
1597
+ return `${count} ${count === 1 ? singular : plural}`;
1598
+ }
1599
+ }
1600
+ return "now";
1601
+ };
1602
+ var defaultFilter = (value, defaultValue = "") => {
1603
+ if (value === undefined || value === null || value === "" || value === false) {
1604
+ return defaultValue;
1605
+ }
1606
+ return value;
1607
+ };
1608
+ var default_if_none = (value, defaultValue = "") => value === null || value === undefined ? defaultValue : value;
1609
+ var yesno = (value, arg = "yes,no,maybe") => {
1610
+ const [yes, no, maybe] = arg.split(",");
1611
+ if (value === true)
1612
+ return yes;
1613
+ if (value === false)
1614
+ return no;
1615
+ return maybe ?? no;
1616
+ };
1617
+ var pluralize = (value, arg = "s") => {
1618
+ const [singular, plural] = arg.includes(",") ? arg.split(",") : ["", arg];
1619
+ const count = Number(value);
1620
+ return count === 1 ? singular : plural;
1621
+ };
1622
+ var urlencode = (value) => encodeURIComponent(String(value));
1623
+ var urlize = (value) => {
1624
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
1625
+ const html = String(value).replace(urlRegex, '<a href="$1">$1</a>');
1626
+ const safeString = new String(html);
1627
+ safeString.__safe__ = true;
1628
+ return safeString;
1629
+ };
1630
+ var json = (value, indent) => {
1631
+ try {
1632
+ const jsonStr = JSON.stringify(value, null, indent);
1633
+ const safeString = new String(jsonStr);
1634
+ safeString.__safe__ = true;
1635
+ return safeString;
1636
+ } catch {
1637
+ return "";
1638
+ }
1639
+ };
1640
+ var random = (value) => {
1641
+ if (Array.isArray(value)) {
1642
+ return value[Math.floor(Math.random() * value.length)];
1643
+ }
1644
+ return value;
1645
+ };
1646
+ var batch = (value, size, fillWith = null) => {
1647
+ if (!Array.isArray(value))
1648
+ return [[value]];
1649
+ const result = [];
1650
+ const numSize = Number(size) || 1;
1651
+ for (let i = 0;i < value.length; i += numSize) {
1652
+ const batch2 = value.slice(i, i + numSize);
1653
+ while (fillWith !== null && batch2.length < numSize) {
1654
+ batch2.push(fillWith);
1655
+ }
1656
+ result.push(batch2);
1657
+ }
1658
+ return result;
1659
+ };
1660
+ var groupby = (value, attribute) => {
1661
+ if (!Array.isArray(value))
1662
+ return [];
1663
+ const groups = new Map;
1664
+ for (const item of value) {
1665
+ const key = attribute ? item[attribute] : item;
1666
+ if (!groups.has(key)) {
1667
+ groups.set(key, []);
1668
+ }
1669
+ groups.get(key).push(item);
1670
+ }
1671
+ return Array.from(groups.entries()).map(([grouper, list]) => ({
1672
+ grouper,
1673
+ list
1674
+ }));
1675
+ };
1676
+ var builtinFilters = {
1677
+ upper,
1678
+ lower,
1679
+ capitalize,
1680
+ capfirst,
1681
+ title,
1682
+ trim,
1683
+ striptags,
1684
+ escape,
1685
+ e: escape,
1686
+ safe,
1687
+ escapejs,
1688
+ linebreaks,
1689
+ linebreaksbr,
1690
+ truncatechars,
1691
+ truncatewords,
1692
+ wordcount,
1693
+ center,
1694
+ ljust,
1695
+ rjust,
1696
+ cut,
1697
+ slugify,
1698
+ abs,
1699
+ round,
1700
+ int,
1701
+ float,
1702
+ floatformat,
1703
+ add,
1704
+ divisibleby,
1705
+ filesizeformat,
1706
+ length,
1707
+ length_is,
1708
+ first,
1709
+ last,
1710
+ join,
1711
+ slice,
1712
+ reverse,
1713
+ sort,
1714
+ unique,
1715
+ make_list,
1716
+ dictsort,
1717
+ dictsortreversed,
1718
+ columns,
1719
+ date,
1720
+ time,
1721
+ timesince,
1722
+ timeuntil,
1723
+ default: defaultFilter,
1724
+ d: defaultFilter,
1725
+ default_if_none,
1726
+ yesno,
1727
+ pluralize,
1728
+ urlencode,
1729
+ urlize,
1730
+ json,
1731
+ tojson: json,
1732
+ random,
1733
+ batch,
1734
+ groupby
1735
+ };
1736
+
1737
+ // src/tests/index.ts
1738
+ var divisibleby2 = (value, num) => {
1739
+ const n = Number(value);
1740
+ const d = Number(num);
1741
+ if (d === 0)
1742
+ return false;
1743
+ return n % d === 0;
1744
+ };
1745
+ var even = (value) => {
1746
+ const n = Number(value);
1747
+ return Number.isInteger(n) && n % 2 === 0;
1748
+ };
1749
+ var odd = (value) => {
1750
+ const n = Number(value);
1751
+ return Number.isInteger(n) && n % 2 !== 0;
1752
+ };
1753
+ var number = (value) => {
1754
+ return typeof value === "number" && !isNaN(value);
1755
+ };
1756
+ var integer = (value) => {
1757
+ return Number.isInteger(value);
1758
+ };
1759
+ var float2 = (value) => {
1760
+ return typeof value === "number" && !Number.isInteger(value) && !isNaN(value);
1761
+ };
1762
+ var gt = (value, other) => Number(value) > Number(other);
1763
+ var ge = (value, other) => Number(value) >= Number(other);
1764
+ var lt = (value, other) => Number(value) < Number(other);
1765
+ var le = (value, other) => Number(value) <= Number(other);
1766
+ var greaterthan = gt;
1767
+ var lessthan = lt;
1768
+ var defined = (value) => {
1769
+ return value !== undefined2;
1770
+ };
1771
+ var undefined2 = (value) => {
1772
+ return value === undefined2;
1773
+ };
1774
+ var none = (value) => {
1775
+ return value === null;
1776
+ };
1777
+ var boolean = (value) => {
1778
+ return typeof value === "boolean";
1779
+ };
1780
+ var string = (value) => {
1781
+ return typeof value === "string";
1782
+ };
1783
+ var mapping = (value) => {
1784
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
1785
+ };
1786
+ var iterable = (value) => {
1787
+ if (value == null)
1788
+ return false;
1789
+ return typeof value === "string" || Array.isArray(value) || typeof value[Symbol.iterator] === "function";
1790
+ };
1791
+ var sequence = (value) => {
1792
+ return Array.isArray(value) || typeof value === "string";
1793
+ };
1794
+ var callable = (value) => {
1795
+ return typeof value === "function";
1796
+ };
1797
+ var lower2 = (value) => {
1798
+ if (typeof value !== "string")
1799
+ return false;
1800
+ return value === value.toLowerCase() && value !== value.toUpperCase();
1801
+ };
1802
+ var upper2 = (value) => {
1803
+ if (typeof value !== "string")
1804
+ return false;
1805
+ return value === value.toUpperCase() && value !== value.toLowerCase();
1806
+ };
1807
+ var empty = (value) => {
1808
+ if (value == null)
1809
+ return true;
1810
+ if (typeof value === "string" || Array.isArray(value))
1811
+ return value.length === 0;
1812
+ if (typeof value === "object")
1813
+ return Object.keys(value).length === 0;
1814
+ return false;
1815
+ };
1816
+ var in_ = (value, container) => {
1817
+ if (Array.isArray(container))
1818
+ return container.includes(value);
1819
+ if (typeof container === "string")
1820
+ return container.includes(String(value));
1821
+ if (typeof container === "object" && container !== null)
1822
+ return value in container;
1823
+ return false;
1824
+ };
1825
+ var eq = (value, other) => value === other;
1826
+ var ne = (value, other) => value !== other;
1827
+ var sameas = (value, other) => value === other;
1828
+ var equalto = eq;
1829
+ var truthy = (value) => {
1830
+ if (value == null)
1831
+ return false;
1832
+ if (typeof value === "boolean")
1833
+ return value;
1834
+ if (typeof value === "number")
1835
+ return value !== 0;
1836
+ if (typeof value === "string")
1837
+ return value.length > 0;
1838
+ if (Array.isArray(value))
1839
+ return value.length > 0;
1840
+ if (typeof value === "object")
1841
+ return Object.keys(value).length > 0;
1842
+ return true;
1843
+ };
1844
+ var falsy = (value) => !truthy(value);
1845
+ var true_ = (value) => value === true;
1846
+ var false_ = (value) => value === false;
1847
+ var builtinTests = {
1848
+ divisibleby: divisibleby2,
1849
+ even,
1850
+ odd,
1851
+ number,
1852
+ integer,
1853
+ float: float2,
1854
+ gt,
1855
+ ge,
1856
+ lt,
1857
+ le,
1858
+ greaterthan,
1859
+ lessthan,
1860
+ defined,
1861
+ undefined: undefined2,
1862
+ none,
1863
+ boolean,
1864
+ string,
1865
+ mapping,
1866
+ iterable,
1867
+ sequence,
1868
+ callable,
1869
+ lower: lower2,
1870
+ upper: upper2,
1871
+ empty,
1872
+ in: in_,
1873
+ eq,
1874
+ ne,
1875
+ sameas,
1876
+ equalto,
1877
+ truthy,
1878
+ falsy,
1879
+ true: true_,
1880
+ false: false_
1881
+ };
1882
+
1883
+ // src/runtime/index.ts
1884
+ class Runtime {
1885
+ options;
1886
+ filters;
1887
+ tests;
1888
+ blocks = new Map;
1889
+ parentTemplate = null;
1890
+ constructor(options = {}) {
1891
+ this.options = {
1892
+ autoescape: options.autoescape ?? true,
1893
+ filters: options.filters ?? {},
1894
+ tests: options.tests ?? {},
1895
+ globals: options.globals ?? {},
1896
+ urlResolver: options.urlResolver ?? ((name) => `#${name}`),
1897
+ staticResolver: options.staticResolver ?? ((path) => `/static/${path}`),
1898
+ templateLoader: options.templateLoader ?? (async () => {
1899
+ throw new Error("Template loader not configured");
1900
+ })
1901
+ };
1902
+ this.filters = { ...builtinFilters, ...this.options.filters };
1903
+ this.tests = { ...builtinTests, ...this.options.tests };
1904
+ }
1905
+ async render(ast, context = {}) {
1906
+ const ctx = new Context({ ...this.options.globals, ...context });
1907
+ this.blocks.clear();
1908
+ this.parentTemplate = null;
1909
+ await this.collectBlocks(ast, ctx);
1910
+ if (this.parentTemplate) {
1911
+ return this.renderTemplate(this.parentTemplate, ctx);
1912
+ }
1913
+ return this.renderTemplate(ast, ctx);
1914
+ }
1915
+ async collectBlocks(ast, ctx) {
1916
+ for (const node of ast.body) {
1917
+ if (node.type === "Extends") {
1918
+ const templateName = await this.evaluate(node.template, ctx);
1919
+ this.parentTemplate = await this.options.templateLoader(String(templateName));
1920
+ await this.collectBlocks(this.parentTemplate, ctx);
1921
+ } else if (node.type === "Block") {
1922
+ this.blocks.set(node.name, node);
1923
+ }
1924
+ }
1925
+ }
1926
+ async renderTemplate(ast, ctx) {
1927
+ const parts = [];
1928
+ for (const node of ast.body) {
1929
+ const result = await this.renderNode(node, ctx);
1930
+ if (result !== null)
1931
+ parts.push(result);
1932
+ }
1933
+ return parts.join("");
1934
+ }
1935
+ async renderNode(node, ctx) {
1936
+ switch (node.type) {
1937
+ case "Text":
1938
+ return node.value;
1939
+ case "Output":
1940
+ return this.renderOutput(node, ctx);
1941
+ case "If":
1942
+ return this.renderIf(node, ctx);
1943
+ case "For":
1944
+ return this.renderFor(node, ctx);
1945
+ case "Block":
1946
+ return this.renderBlock(node, ctx);
1947
+ case "Extends":
1948
+ return null;
1949
+ case "Include":
1950
+ return this.renderInclude(node, ctx);
1951
+ case "Set":
1952
+ return this.renderSet(node, ctx);
1953
+ case "With":
1954
+ return this.renderWith(node, ctx);
1955
+ case "Load":
1956
+ return null;
1957
+ case "Url":
1958
+ return this.renderUrl(node, ctx);
1959
+ case "Static":
1960
+ return this.renderStatic(node, ctx);
1961
+ default:
1962
+ return null;
1963
+ }
1964
+ }
1965
+ async renderOutput(node, ctx) {
1966
+ const value = await this.evaluate(node.expression, ctx);
1967
+ return this.stringify(value);
1968
+ }
1969
+ async renderIf(node, ctx) {
1970
+ if (this.isTruthy(await this.evaluate(node.test, ctx))) {
1971
+ return this.renderNodes(node.body, ctx);
1972
+ }
1973
+ for (const elif of node.elifs) {
1974
+ if (this.isTruthy(await this.evaluate(elif.test, ctx))) {
1975
+ return this.renderNodes(elif.body, ctx);
1976
+ }
1977
+ }
1978
+ if (node.else_.length > 0) {
1979
+ return this.renderNodes(node.else_, ctx);
1980
+ }
1981
+ return "";
1982
+ }
1983
+ async renderFor(node, ctx) {
1984
+ const iterable2 = await this.evaluate(node.iter, ctx);
1985
+ const items = this.toIterable(iterable2);
1986
+ if (items.length === 0) {
1987
+ return this.renderNodes(node.else_, ctx);
1988
+ }
1989
+ const parts = [];
1990
+ ctx.push();
1991
+ for (let i = 0;i < items.length; i++) {
1992
+ const item = items[i];
1993
+ if (Array.isArray(node.target)) {
1994
+ let values;
1995
+ if (Array.isArray(item)) {
1996
+ values = item;
1997
+ } else if (item && typeof item === "object" && (("0" in item) || ("key" in item))) {
1998
+ values = [item[0] ?? item.key, item[1] ?? item.value];
1999
+ } else {
2000
+ values = [item, item];
2001
+ }
2002
+ node.target.forEach((name, idx) => {
2003
+ ctx.set(name, values[idx]);
2004
+ });
2005
+ } else {
2006
+ ctx.set(node.target, item);
2007
+ }
2008
+ ctx.pushForLoop(items, i);
2009
+ const result = await this.renderNodes(node.body, ctx);
2010
+ parts.push(result);
2011
+ ctx.popForLoop();
2012
+ }
2013
+ ctx.pop();
2014
+ return parts.join("");
2015
+ }
2016
+ async renderBlock(node, ctx) {
2017
+ const blockToRender = this.blocks.get(node.name) || node;
2018
+ ctx.push();
2019
+ ctx.set("block", {
2020
+ super: async () => {
2021
+ return this.renderNodes(node.body, ctx);
2022
+ }
2023
+ });
2024
+ const result = await this.renderNodes(blockToRender.body, ctx);
2025
+ ctx.pop();
2026
+ return result;
2027
+ }
2028
+ async renderInclude(node, ctx) {
2029
+ try {
2030
+ const templateName = await this.evaluate(node.template, ctx);
2031
+ const template = await this.options.templateLoader(String(templateName));
2032
+ let includeCtx;
2033
+ if (node.only) {
2034
+ includeCtx = new Context(node.context ? await this.evaluateObject(node.context, ctx) : {});
2035
+ } else {
2036
+ const additional = node.context ? await this.evaluateObject(node.context, ctx) : {};
2037
+ includeCtx = ctx.derived(additional);
2038
+ }
2039
+ return this.renderTemplate(template, includeCtx);
2040
+ } catch (error) {
2041
+ if (node.ignoreMissing)
2042
+ return "";
2043
+ throw error;
2044
+ }
2045
+ }
2046
+ async renderSet(node, ctx) {
2047
+ const value = await this.evaluate(node.value, ctx);
2048
+ ctx.set(node.target, value);
2049
+ return "";
2050
+ }
2051
+ async renderWith(node, ctx) {
2052
+ ctx.push();
2053
+ for (const { target, value } of node.assignments) {
2054
+ ctx.set(target, await this.evaluate(value, ctx));
2055
+ }
2056
+ const result = await this.renderNodes(node.body, ctx);
2057
+ ctx.pop();
2058
+ return result;
2059
+ }
2060
+ async renderUrl(node, ctx) {
2061
+ const name = await this.evaluate(node.name, ctx);
2062
+ const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2063
+ const kwargs = await this.evaluateObject(node.kwargs, ctx);
2064
+ const url = this.options.urlResolver(String(name), args, kwargs);
2065
+ if (node.asVar) {
2066
+ ctx.set(node.asVar, url);
2067
+ return "";
2068
+ }
2069
+ return url;
2070
+ }
2071
+ async renderStatic(node, ctx) {
2072
+ const path = await this.evaluate(node.path, ctx);
2073
+ const url = this.options.staticResolver(String(path));
2074
+ if (node.asVar) {
2075
+ ctx.set(node.asVar, url);
2076
+ return "";
2077
+ }
2078
+ return url;
2079
+ }
2080
+ async renderNodes(nodes2, ctx) {
2081
+ const parts = [];
2082
+ for (const node of nodes2) {
2083
+ const result = await this.renderNode(node, ctx);
2084
+ if (result !== null)
2085
+ parts.push(result);
2086
+ }
2087
+ return parts.join("");
2088
+ }
2089
+ async evaluate(node, ctx) {
2090
+ switch (node.type) {
2091
+ case "Literal":
2092
+ return node.value;
2093
+ case "Name":
2094
+ return ctx.get(node.name);
2095
+ case "GetAttr":
2096
+ return this.evaluateGetAttr(node, ctx);
2097
+ case "GetItem":
2098
+ return this.evaluateGetItem(node, ctx);
2099
+ case "FilterExpr":
2100
+ return this.evaluateFilter(node, ctx);
2101
+ case "BinaryOp":
2102
+ return this.evaluateBinaryOp(node, ctx);
2103
+ case "UnaryOp":
2104
+ return this.evaluateUnaryOp(node, ctx);
2105
+ case "Compare":
2106
+ return this.evaluateCompare(node, ctx);
2107
+ case "Conditional":
2108
+ return this.evaluateConditional(node, ctx);
2109
+ case "Array":
2110
+ return Promise.all(node.elements.map((el) => this.evaluate(el, ctx)));
2111
+ case "Object":
2112
+ return this.evaluateObjectLiteral(node, ctx);
2113
+ case "FunctionCall":
2114
+ return this.evaluateFunctionCall(node, ctx);
2115
+ case "TestExpr":
2116
+ return this.evaluateTest(node, ctx);
2117
+ default:
2118
+ return;
2119
+ }
2120
+ }
2121
+ async evaluateTest(node, ctx) {
2122
+ if (node.test === "defined" || node.test === "undefined") {
2123
+ let isDefined = false;
2124
+ if (node.node.type === "Name") {
2125
+ isDefined = ctx.has(node.node.name);
2126
+ } else {
2127
+ const value2 = await this.evaluate(node.node, ctx);
2128
+ isDefined = value2 !== undefined;
2129
+ }
2130
+ const result2 = node.test === "defined" ? isDefined : !isDefined;
2131
+ return node.negated ? !result2 : result2;
2132
+ }
2133
+ const value = await this.evaluate(node.node, ctx);
2134
+ const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2135
+ const test = this.tests[node.test];
2136
+ if (!test) {
2137
+ throw new Error(`Unknown test: ${node.test}`);
2138
+ }
2139
+ const result = test(value, ...args);
2140
+ return node.negated ? !result : result;
2141
+ }
2142
+ async evaluateGetAttr(node, ctx) {
2143
+ const obj = await this.evaluate(node.object, ctx);
2144
+ if (obj == null)
2145
+ return;
2146
+ const numIndex = parseInt(node.attribute, 10);
2147
+ if (!isNaN(numIndex) && Array.isArray(obj)) {
2148
+ return obj[numIndex];
2149
+ }
2150
+ if (typeof obj === "object" && node.attribute in obj) {
2151
+ const value = obj[node.attribute];
2152
+ if (typeof value === "function") {
2153
+ return value.call(obj);
2154
+ }
2155
+ return value;
2156
+ }
2157
+ if (typeof obj[node.attribute] === "function") {
2158
+ return obj[node.attribute].bind(obj);
2159
+ }
2160
+ return;
2161
+ }
2162
+ async evaluateGetItem(node, ctx) {
2163
+ const obj = await this.evaluate(node.object, ctx);
2164
+ const index = await this.evaluate(node.index, ctx);
2165
+ if (obj == null)
2166
+ return;
2167
+ return obj[index];
2168
+ }
2169
+ async evaluateFilter(node, ctx) {
2170
+ const value = await this.evaluate(node.node, ctx);
2171
+ const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2172
+ const kwargs = await this.evaluateObject(node.kwargs, ctx);
2173
+ const filter = this.filters[node.filter];
2174
+ if (!filter) {
2175
+ throw new Error(`Unknown filter: ${node.filter}`);
2176
+ }
2177
+ return filter(value, ...args, ...Object.values(kwargs));
2178
+ }
2179
+ async evaluateBinaryOp(node, ctx) {
2180
+ const left = await this.evaluate(node.left, ctx);
2181
+ if (node.operator === "and") {
2182
+ return this.isTruthy(left) ? await this.evaluate(node.right, ctx) : left;
2183
+ }
2184
+ if (node.operator === "or") {
2185
+ return this.isTruthy(left) ? left : await this.evaluate(node.right, ctx);
2186
+ }
2187
+ const right = await this.evaluate(node.right, ctx);
2188
+ switch (node.operator) {
2189
+ case "+":
2190
+ return typeof left === "string" || typeof right === "string" ? String(left) + String(right) : Number(left) + Number(right);
2191
+ case "-":
2192
+ return Number(left) - Number(right);
2193
+ case "*":
2194
+ return Number(left) * Number(right);
2195
+ case "/":
2196
+ const divisor = Number(right);
2197
+ if (divisor === 0)
2198
+ return 0;
2199
+ return Number(left) / divisor;
2200
+ case "%":
2201
+ return Number(left) % Number(right);
2202
+ case "~":
2203
+ return String(left) + String(right);
2204
+ default:
2205
+ return;
2206
+ }
2207
+ }
2208
+ async evaluateUnaryOp(node, ctx) {
2209
+ const operand = await this.evaluate(node.operand, ctx);
2210
+ switch (node.operator) {
2211
+ case "not":
2212
+ return !this.isTruthy(operand);
2213
+ case "-":
2214
+ return -Number(operand);
2215
+ case "+":
2216
+ return +Number(operand);
2217
+ default:
2218
+ return operand;
2219
+ }
2220
+ }
2221
+ async evaluateCompare(node, ctx) {
2222
+ let left = await this.evaluate(node.left, ctx);
2223
+ for (const { operator, right: rightNode } of node.ops) {
2224
+ const right = await this.evaluate(rightNode, ctx);
2225
+ let result;
2226
+ switch (operator) {
2227
+ case "==":
2228
+ result = left === right;
2229
+ break;
2230
+ case "!=":
2231
+ result = left !== right;
2232
+ break;
2233
+ case "<":
2234
+ result = left < right;
2235
+ break;
2236
+ case ">":
2237
+ result = left > right;
2238
+ break;
2239
+ case "<=":
2240
+ result = left <= right;
2241
+ break;
2242
+ case ">=":
2243
+ result = left >= right;
2244
+ break;
2245
+ case "in":
2246
+ result = this.isIn(left, right);
2247
+ break;
2248
+ case "not in":
2249
+ result = !this.isIn(left, right);
2250
+ break;
2251
+ case "is":
2252
+ result = left === right;
2253
+ break;
2254
+ case "is not":
2255
+ result = left !== right;
2256
+ break;
2257
+ default:
2258
+ result = false;
2259
+ }
2260
+ if (!result)
2261
+ return false;
2262
+ left = right;
2263
+ }
2264
+ return true;
2265
+ }
2266
+ async evaluateConditional(node, ctx) {
2267
+ const test = await this.evaluate(node.test, ctx);
2268
+ return this.isTruthy(test) ? await this.evaluate(node.trueExpr, ctx) : await this.evaluate(node.falseExpr, ctx);
2269
+ }
2270
+ async evaluateObjectLiteral(node, ctx) {
2271
+ const result = {};
2272
+ for (const { key, value } of node.pairs) {
2273
+ const k = await this.evaluate(key, ctx);
2274
+ result[String(k)] = await this.evaluate(value, ctx);
2275
+ }
2276
+ return result;
2277
+ }
2278
+ async evaluateFunctionCall(node, ctx) {
2279
+ const callee = await this.evaluate(node.callee, ctx);
2280
+ const args = await Promise.all(node.args.map((arg) => this.evaluate(arg, ctx)));
2281
+ const kwargs = await this.evaluateObject(node.kwargs, ctx);
2282
+ if (typeof callee === "function") {
2283
+ return callee(...args, kwargs);
2284
+ }
2285
+ return;
2286
+ }
2287
+ async evaluateObject(obj, ctx) {
2288
+ const result = {};
2289
+ for (const [key, value] of Object.entries(obj)) {
2290
+ result[key] = await this.evaluate(value, ctx);
2291
+ }
2292
+ return result;
2293
+ }
2294
+ stringify(value) {
2295
+ if (value == null)
2296
+ return "";
2297
+ if (typeof value === "boolean")
2298
+ return value ? "True" : "False";
2299
+ const str = String(value);
2300
+ if (value.__safe__)
2301
+ return str;
2302
+ if (this.options.autoescape) {
2303
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
2304
+ }
2305
+ return str;
2306
+ }
2307
+ isTruthy(value) {
2308
+ if (value == null)
2309
+ return false;
2310
+ if (typeof value === "boolean")
2311
+ return value;
2312
+ if (typeof value === "number")
2313
+ return value !== 0;
2314
+ if (typeof value === "string")
2315
+ return value.length > 0;
2316
+ if (Array.isArray(value))
2317
+ return value.length > 0;
2318
+ if (typeof value === "object")
2319
+ return Object.keys(value).length > 0;
2320
+ return true;
2321
+ }
2322
+ isIn(needle, haystack) {
2323
+ if (Array.isArray(haystack))
2324
+ return haystack.includes(needle);
2325
+ if (typeof haystack === "string")
2326
+ return haystack.includes(String(needle));
2327
+ if (typeof haystack === "object" && haystack !== null)
2328
+ return needle in haystack;
2329
+ return false;
2330
+ }
2331
+ toIterable(value) {
2332
+ if (Array.isArray(value))
2333
+ return value;
2334
+ if (value == null)
2335
+ return [];
2336
+ if (typeof value === "string")
2337
+ return value.split("");
2338
+ if (typeof value === "object") {
2339
+ if (typeof value[Symbol.iterator] === "function") {
2340
+ return Array.from(value);
2341
+ }
2342
+ return Object.entries(value).map(([k, v]) => ({ key: k, value: v, 0: k, 1: v }));
2343
+ }
2344
+ return [value];
2345
+ }
2346
+ addFilter(name, fn) {
2347
+ this.filters[name] = fn;
2348
+ }
2349
+ addTest(name, fn) {
2350
+ this.tests[name] = fn;
2351
+ }
2352
+ addGlobal(name, value) {
2353
+ this.options.globals[name] = value;
2354
+ }
2355
+ }
2356
+
2357
+ // src/index.ts
2358
+ import * as fs from "fs";
2359
+ import * as path from "path";
2360
+
2361
+ class Environment {
2362
+ options;
2363
+ runtime;
2364
+ templateCache = new Map;
2365
+ routes = new Map;
2366
+ constructor(options = {}) {
2367
+ this.options = {
2368
+ templates: options.templates ?? "./templates",
2369
+ autoescape: options.autoescape ?? true,
2370
+ filters: options.filters ?? {},
2371
+ globals: options.globals ?? {},
2372
+ urlResolver: options.urlResolver ?? this.defaultUrlResolver.bind(this),
2373
+ staticResolver: options.staticResolver ?? this.defaultStaticResolver.bind(this),
2374
+ cache: options.cache ?? true,
2375
+ extensions: options.extensions ?? [".html", ".jinja", ".jinja2", ""]
2376
+ };
2377
+ this.runtime = new Runtime({
2378
+ autoescape: this.options.autoescape,
2379
+ filters: this.options.filters,
2380
+ globals: this.options.globals,
2381
+ urlResolver: this.options.urlResolver,
2382
+ staticResolver: this.options.staticResolver,
2383
+ templateLoader: this.loadTemplate.bind(this)
2384
+ });
2385
+ }
2386
+ async render(templateName, context = {}) {
2387
+ const ast = await this.loadTemplate(templateName);
2388
+ return this.runtime.render(ast, context);
2389
+ }
2390
+ async renderString(source, context = {}) {
2391
+ const ast = this.compile(source);
2392
+ return this.runtime.render(ast, context);
2393
+ }
2394
+ compile(source) {
2395
+ const lexer = new Lexer(source);
2396
+ const tokens = lexer.tokenize();
2397
+ const parser = new Parser(tokens);
2398
+ return parser.parse();
2399
+ }
2400
+ async loadTemplate(templateName) {
2401
+ if (this.options.cache && this.templateCache.has(templateName)) {
2402
+ return this.templateCache.get(templateName);
2403
+ }
2404
+ const templatePath = await this.resolveTemplatePath(templateName);
2405
+ if (!templatePath) {
2406
+ throw new Error(`Template not found: ${templateName}`);
2407
+ }
2408
+ const source = await fs.promises.readFile(templatePath, "utf-8");
2409
+ const ast = this.compile(source);
2410
+ if (this.options.cache) {
2411
+ this.templateCache.set(templateName, ast);
2412
+ }
2413
+ return ast;
2414
+ }
2415
+ clearCache() {
2416
+ this.templateCache.clear();
2417
+ }
2418
+ addFilter(name, fn) {
2419
+ this.runtime.addFilter(name, fn);
2420
+ }
2421
+ addGlobal(name, value) {
2422
+ this.runtime.addGlobal(name, value);
2423
+ }
2424
+ addUrl(name, pattern) {
2425
+ this.routes.set(name, pattern);
2426
+ }
2427
+ addUrls(routes) {
2428
+ for (const [name, pattern] of Object.entries(routes)) {
2429
+ this.routes.set(name, pattern);
2430
+ }
2431
+ }
2432
+ async resolveTemplatePath(templateName) {
2433
+ const basePath = path.resolve(this.options.templates, templateName);
2434
+ for (const ext of this.options.extensions) {
2435
+ const fullPath = basePath + ext;
2436
+ try {
2437
+ await fs.promises.access(fullPath, fs.constants.R_OK);
2438
+ return fullPath;
2439
+ } catch {}
2440
+ }
2441
+ return null;
2442
+ }
2443
+ defaultUrlResolver(name, args, kwargs) {
2444
+ const pattern = this.routes.get(name);
2445
+ if (!pattern) {
2446
+ console.warn(`URL pattern not found: ${name}`);
2447
+ return `#${name}`;
2448
+ }
2449
+ let url = pattern;
2450
+ for (const [key, value] of Object.entries(kwargs)) {
2451
+ url = url.replace(`:${key}`, encodeURIComponent(String(value)));
2452
+ url = url.replace(`<${key}>`, encodeURIComponent(String(value)));
2453
+ url = url.replace(`(?P<${key}>[^/]+)`, encodeURIComponent(String(value)));
2454
+ }
2455
+ let argIndex = 0;
2456
+ url = url.replace(/<[^>]+>|:[a-zA-Z_]+|\(\?P<[^>]+>\[[^\]]+\]\)/g, () => {
2457
+ if (argIndex < args.length) {
2458
+ return encodeURIComponent(String(args[argIndex++]));
2459
+ }
2460
+ return "";
2461
+ });
2462
+ return url;
2463
+ }
2464
+ defaultStaticResolver(filePath) {
2465
+ return `/static/${filePath}`;
2466
+ }
2467
+ }
2468
+ async function render(source, context = {}, options = {}) {
2469
+ const env = new Environment(options);
2470
+ return env.renderString(source, context);
2471
+ }
2472
+ function Template(source, options = {}) {
2473
+ const env = new Environment(options);
2474
+ const ast = env.compile(source);
2475
+ return {
2476
+ async render(context = {}) {
2477
+ const runtime = new Runtime({
2478
+ autoescape: options.autoescape ?? true,
2479
+ filters: options.filters ?? {},
2480
+ globals: options.globals ?? {},
2481
+ urlResolver: options.urlResolver,
2482
+ staticResolver: options.staticResolver,
2483
+ templateLoader: async () => ast
2484
+ });
2485
+ return runtime.render(ast, context);
2486
+ }
2487
+ };
2488
+ }
2489
+ export {
2490
+ render,
2491
+ builtinFilters,
2492
+ TokenType,
2493
+ Template,
2494
+ Runtime,
2495
+ Parser,
2496
+ Lexer,
2497
+ Environment,
2498
+ Context
2499
+ };