File Coverage

lib/Jinja2/TT2/Parser.pm
Criterion Covered Total %
statement 336 558 60.2
branch 100 214 46.7
condition 47 163 28.8
subroutine 33 43 76.7
pod 0 2 0.0
total 516 980 52.6


line stmt bran cond sub pod time code
1             package Jinja2::TT2::Parser;
2              
3 2     2   786 use strict;
  2         5  
  2         85  
4 2     2   13 use warnings;
  2         5  
  2         103  
5 2     2   27 use v5.20;
  2         7  
6              
7             # AST Node types
8             use constant {
9 2         17317 NODE_ROOT => 'ROOT',
10             NODE_TEXT => 'TEXT',
11             NODE_OUTPUT => 'OUTPUT',
12             NODE_COMMENT => 'COMMENT',
13             NODE_IF => 'IF',
14             NODE_ELIF => 'ELIF',
15             NODE_ELSE => 'ELSE',
16             NODE_FOR => 'FOR',
17             NODE_BLOCK => 'BLOCK',
18             NODE_EXTENDS => 'EXTENDS',
19             NODE_INCLUDE => 'INCLUDE',
20             NODE_IMPORT => 'IMPORT',
21             NODE_FROM => 'FROM',
22             NODE_SET => 'SET',
23             NODE_MACRO => 'MACRO',
24             NODE_CALL => 'CALL',
25             NODE_FILTER => 'FILTER',
26             NODE_RAW => 'RAW',
27             NODE_WITH => 'WITH',
28             NODE_AUTOESCAPE => 'AUTOESCAPE',
29             NODE_EXPR => 'EXPR',
30 2     2   12 };
  2         3  
31              
32             sub new {
33 2     2 0 18 my ($class, %opts) = @_;
34 2         22 return bless {
35             tokens => [],
36             pos => 0,
37             }, $class;
38             }
39              
40             sub parse {
41 65     65 0 266 my ($self, $tokens) = @_;
42              
43 65         605 $self->{tokens} = $tokens;
44 65         132 $self->{pos} = 0;
45              
46 65         105 my @body;
47 65         203 while (!$self->_at_end()) {
48 71         204 my $node = $self->_parse_node();
49 71 50       346 push @body, $node if $node;
50             }
51              
52 65         443 return { type => NODE_ROOT, body => \@body };
53             }
54              
55             sub _parse_node {
56 100     100   195 my ($self) = @_;
57              
58 100         195 my $token = $self->_current();
59 100 50       250 return undef unless $token;
60              
61 100 100       296 if ($token->{type} eq 'TEXT') {
62 27         72 $self->_advance();
63 27         120 return { type => NODE_TEXT, value => $token->{value} };
64             }
65              
66 73 100       176 if ($token->{type} eq 'COMMENT') {
67 2         11 $self->_advance();
68 2         9 return { type => NODE_COMMENT, value => $token->{value} };
69             }
70              
71 71 100       186 if ($token->{type} eq 'VAR_START') {
72 46         152 return $self->_parse_output();
73             }
74              
75 25 50       73 if ($token->{type} eq 'STMT_START') {
76 25         78 return $self->_parse_statement();
77             }
78              
79             # Skip unknown tokens
80 0         0 $self->_advance();
81 0         0 return undef;
82             }
83              
84             sub _parse_output {
85 46     46   105 my ($self) = @_;
86              
87 46         123 my $start = $self->_expect('VAR_START');
88 46         140 my $expr = $self->_parse_expression();
89 46         112 my $end = $self->_expect('VAR_END');
90              
91             return {
92             type => NODE_OUTPUT,
93             expr => $expr,
94             strip_before => $start->{strip_before},
95             strip_after => $end->{strip_after},
96 46         335 };
97             }
98              
99             sub _parse_statement {
100 25     25   51 my ($self) = @_;
101              
102 25         71 my $start = $self->_expect('STMT_START');
103 25         53 my $strip_before = $start->{strip_before};
104              
105 25         56 my $keyword = $self->_current();
106              
107 25 50 33     132 unless ($keyword && $keyword->{type} eq 'NAME') {
108 0   0     0 die "Expected statement keyword at position " . ($keyword->{pos} // 'unknown');
109             }
110              
111 25         60 my $kw = $keyword->{value};
112              
113 25 100       113 if ($kw eq 'if') {
    100          
    100          
    50          
    100          
    50          
    50          
    100          
    50          
    0          
    0          
    0          
    0          
    0          
    0          
114 13         42 return $self->_parse_if($strip_before);
115             } elsif ($kw eq 'for') {
116 5         29 return $self->_parse_for($strip_before);
117             } elsif ($kw eq 'block') {
118 2         11 return $self->_parse_block($strip_before);
119             } elsif ($kw eq 'extends') {
120 0         0 return $self->_parse_extends($strip_before);
121             } elsif ($kw eq 'include') {
122 2         13 return $self->_parse_include($strip_before);
123             } elsif ($kw eq 'import') {
124 0         0 return $self->_parse_import($strip_before);
125             } elsif ($kw eq 'from') {
126 0         0 return $self->_parse_from($strip_before);
127             } elsif ($kw eq 'set') {
128 2         11 return $self->_parse_set($strip_before);
129             } elsif ($kw eq 'macro') {
130 1         6 return $self->_parse_macro($strip_before);
131             } elsif ($kw eq 'call') {
132 0         0 return $self->_parse_call_block($strip_before);
133             } elsif ($kw eq 'filter') {
134 0         0 return $self->_parse_filter_block($strip_before);
135             } elsif ($kw eq 'raw') {
136 0         0 return $self->_parse_raw($strip_before);
137             } elsif ($kw eq 'with') {
138 0         0 return $self->_parse_with($strip_before);
139             } elsif ($kw eq 'autoescape') {
140 0         0 return $self->_parse_autoescape($strip_before);
141             } elsif ($kw =~ /^(endif|endfor|endblock|endmacro|endcall|endfilter|endraw|endwith|endautoescape|elif|else)$/) {
142             # These are handled by their parent parsers
143 0         0 die "Unexpected '$kw' without matching opening tag";
144             } else {
145 0         0 die "Unknown statement keyword '$kw'";
146             }
147             }
148              
149             sub _parse_if {
150 13     13   32 my ($self, $strip_before) = @_;
151              
152 13         57 $self->_advance(); # consume 'if'
153 13         40 my $condition = $self->_parse_expression();
154 13         36 my $end = $self->_expect('STMT_END');
155              
156 13         31 my @body;
157             my @branches; # For elif/else
158              
159 13         44 while (!$self->_at_end()) {
160             # Check for elif, else, endif
161 36 100       90 if ($self->_is_stmt_keyword('elif')) {
162 2         8 my $elif_start = $self->_expect('STMT_START');
163 2         9 $self->_advance(); # consume 'elif'
164 2         6 my $elif_cond = $self->_parse_expression();
165 2         9 $self->_expect('STMT_END');
166              
167 2         12 push @branches, {
168             type => NODE_ELIF,
169             condition => $elif_cond,
170             body => [],
171             };
172 2         6 next;
173             }
174              
175 34 100       77 if ($self->_is_stmt_keyword('else')) {
176 5         18 my $else_start = $self->_expect('STMT_START');
177 5         16 $self->_advance(); # consume 'else'
178 5         14 $self->_expect('STMT_END');
179              
180 5         21 push @branches, {
181             type => NODE_ELSE,
182             body => [],
183             };
184 5         17 next;
185             }
186              
187 29 100       62 if ($self->_is_stmt_keyword('endif')) {
188 13         41 my $endif_start = $self->_expect('STMT_START');
189 13         36 $self->_advance(); # consume 'endif'
190 13         28 my $endif_end = $self->_expect('STMT_END');
191 13         30 last;
192             }
193              
194             # Parse body content
195 16         75 my $node = $self->_parse_node();
196 16 50       40 if ($node) {
197 16 100       39 if (@branches) {
198 7         11 push @{$branches[-1]{body}}, $node;
  7         29  
199             } else {
200 9         29 push @body, $node;
201             }
202             }
203             }
204              
205             return {
206             type => NODE_IF,
207             condition => $condition,
208             body => \@body,
209             branches => \@branches,
210             strip_before => $strip_before,
211             strip_after => $end->{strip_after},
212 13         165 };
213             }
214              
215             sub _parse_for {
216 5     5   14 my ($self, $strip_before) = @_;
217              
218 5         17 $self->_advance(); # consume 'for'
219              
220             # Parse loop variable(s)
221 5         38 my @loop_vars;
222 5         18 push @loop_vars, $self->_expect('NAME')->{value};
223              
224 5         18 while ($self->_check('COMMA')) {
225 2         8 $self->_advance();
226 2         6 push @loop_vars, $self->_expect('NAME')->{value};
227             }
228              
229             # Expect 'in' (tokenized as OPERATOR)
230 5         17 my $in_token = $self->_current();
231 5 50 33     70 if (!$in_token || ($in_token->{type} ne 'OPERATOR' && $in_token->{type} ne 'NAME') || $in_token->{value} ne 'in') {
      33        
      33        
232 0   0     0 die "Expected 'in' in for loop, got " . ($in_token->{type} // 'EOF') . " '$in_token->{value}'";
233             }
234 5         40 $self->_advance();
235              
236             # Parse iterable expression
237 5         19 my $iterable = $self->_parse_expression();
238              
239             # Check for optional 'if' filter
240 5         12 my $filter_cond;
241 5 50 33     15 if ($self->_check('NAME') && $self->_current()->{value} eq 'if') {
242 0         0 $self->_advance();
243 0         0 $filter_cond = $self->_parse_expression();
244             }
245              
246             # Check for 'recursive'
247 5         13 my $recursive = 0;
248 5 50 33     15 if ($self->_check('NAME') && $self->_current()->{value} eq 'recursive') {
249 0         0 $self->_advance();
250 0         0 $recursive = 1;
251             }
252              
253 5         14 my $end = $self->_expect('STMT_END');
254              
255 5         14 my @body;
256             my @else_body;
257 5         9 my $in_else = 0;
258              
259 5         16 while (!$self->_at_end()) {
260 14 50       39 if ($self->_is_stmt_keyword('else')) {
261 0         0 $self->_expect('STMT_START');
262 0         0 $self->_advance(); # consume 'else'
263 0         0 $self->_expect('STMT_END');
264 0         0 $in_else = 1;
265 0         0 next;
266             }
267              
268 14 100       131 if ($self->_is_stmt_keyword('endfor')) {
269 5         18 $self->_expect('STMT_START');
270 5         17 $self->_advance(); # consume 'endfor'
271 5         17 $self->_expect('STMT_END');
272 5         13 last;
273             }
274              
275 9         26 my $node = $self->_parse_node();
276 9 50       31 if ($node) {
277 9 50       25 if ($in_else) {
278 0         0 push @else_body, $node;
279             } else {
280 9         35 push @body, $node;
281             }
282             }
283             }
284              
285             return {
286 5         87 type => NODE_FOR,
287             loop_vars => \@loop_vars,
288             iterable => $iterable,
289             filter => $filter_cond,
290             recursive => $recursive,
291             body => \@body,
292             else_body => \@else_body,
293             strip_before => $strip_before,
294             };
295             }
296              
297             sub _parse_block {
298 2     2   7 my ($self, $strip_before) = @_;
299              
300 2         6 $self->_advance(); # consume 'block'
301 2         6 my $name = $self->_expect('NAME')->{value};
302              
303             # Check for 'scoped'
304 2         5 my $scoped = 0;
305 2 50 33     6 if ($self->_check('NAME') && $self->_current()->{value} eq 'scoped') {
306 0         0 $self->_advance();
307 0         0 $scoped = 1;
308             }
309              
310 2         24 $self->_expect('STMT_END');
311              
312 2         4 my @body;
313 2         9 while (!$self->_at_end()) {
314 4 100       13 if ($self->_is_stmt_keyword('endblock')) {
315 2         9 $self->_expect('STMT_START');
316 2         6 $self->_advance(); # consume 'endblock'
317             # Optional block name after endblock
318 2 50       7 if ($self->_check('NAME')) {
319 0         0 $self->_advance();
320             }
321 2         8 $self->_expect('STMT_END');
322 2         5 last;
323             }
324              
325 2         9 my $node = $self->_parse_node();
326 2 50       12 push @body, $node if $node;
327             }
328              
329             return {
330 2         20 type => NODE_BLOCK,
331             name => $name,
332             scoped => $scoped,
333             body => \@body,
334             strip_before => $strip_before,
335             };
336             }
337              
338             sub _parse_extends {
339 0     0   0 my ($self, $strip_before) = @_;
340              
341 0         0 $self->_advance(); # consume 'extends'
342 0         0 my $template = $self->_parse_expression();
343 0         0 my $end = $self->_expect('STMT_END');
344              
345             return {
346             type => NODE_EXTENDS,
347             template => $template,
348             strip_before => $strip_before,
349             strip_after => $end->{strip_after},
350 0         0 };
351             }
352              
353             sub _parse_include {
354 2     2   6 my ($self, $strip_before) = @_;
355              
356 2         9 $self->_advance(); # consume 'include'
357 2         8 my $template = $self->_parse_expression();
358              
359             # Check for 'ignore missing'
360 2         8 my $ignore_missing = 0;
361 2 50 33     8 if ($self->_check('NAME') && $self->_current()->{value} eq 'ignore') {
362 0         0 $self->_advance();
363 0 0 0     0 if ($self->_check('NAME') && $self->_current()->{value} eq 'missing') {
364 0         0 $self->_advance();
365 0         0 $ignore_missing = 1;
366             }
367             }
368              
369             # Check for 'with context' or 'without context'
370 2         7 my $with_context = 1; # default
371 2 50       7 if ($self->_check('NAME')) {
372 0         0 my $ctx = $self->_current()->{value};
373 0 0 0     0 if ($ctx eq 'with' || $ctx eq 'without') {
374 0         0 $self->_advance();
375 0 0 0     0 if ($self->_check('NAME') && $self->_current()->{value} eq 'context') {
376 0         0 $self->_advance();
377 0 0       0 $with_context = ($ctx eq 'with') ? 1 : 0;
378             }
379             }
380             }
381              
382 2         8 my $end = $self->_expect('STMT_END');
383              
384             return {
385             type => NODE_INCLUDE,
386             template => $template,
387             ignore_missing => $ignore_missing,
388             with_context => $with_context,
389             strip_before => $strip_before,
390             strip_after => $end->{strip_after},
391 2         23 };
392             }
393              
394             sub _parse_import {
395 0     0   0 my ($self, $strip_before) = @_;
396              
397 0         0 $self->_advance(); # consume 'import'
398 0         0 my $template = $self->_parse_expression();
399              
400             # Expect 'as'
401 0         0 $self->_expect_keyword('as');
402 0         0 my $alias = $self->_expect('NAME')->{value};
403              
404             # Check for 'with context' or 'without context'
405 0         0 my $with_context = 0; # default for import
406 0 0       0 if ($self->_check('NAME')) {
407 0         0 my $ctx = $self->_current()->{value};
408 0 0 0     0 if ($ctx eq 'with' || $ctx eq 'without') {
409 0         0 $self->_advance();
410 0 0 0     0 if ($self->_check('NAME') && $self->_current()->{value} eq 'context') {
411 0         0 $self->_advance();
412 0 0       0 $with_context = ($ctx eq 'with') ? 1 : 0;
413             }
414             }
415             }
416              
417 0         0 my $end = $self->_expect('STMT_END');
418              
419             return {
420             type => NODE_IMPORT,
421             template => $template,
422             alias => $alias,
423             with_context => $with_context,
424             strip_before => $strip_before,
425             strip_after => $end->{strip_after},
426 0         0 };
427             }
428              
429             sub _parse_from {
430 0     0   0 my ($self, $strip_before) = @_;
431              
432 0         0 $self->_advance(); # consume 'from'
433 0         0 my $template = $self->_parse_expression();
434              
435             # Expect 'import'
436 0         0 $self->_expect_keyword('import');
437              
438             # Parse imported names
439 0         0 my @imports;
440 0   0     0 do {
441 0         0 my $name = $self->_expect('NAME')->{value};
442 0         0 my $alias = $name;
443 0 0 0     0 if ($self->_check('NAME') && $self->_current()->{value} eq 'as') {
444 0         0 $self->_advance();
445 0         0 $alias = $self->_expect('NAME')->{value};
446             }
447 0         0 push @imports, { name => $name, alias => $alias };
448             } while ($self->_check('COMMA') && $self->_advance());
449              
450             # Check for 'with context' or 'without context'
451 0         0 my $with_context = 0;
452 0 0       0 if ($self->_check('NAME')) {
453 0         0 my $ctx = $self->_current()->{value};
454 0 0 0     0 if ($ctx eq 'with' || $ctx eq 'without') {
455 0         0 $self->_advance();
456 0 0 0     0 if ($self->_check('NAME') && $self->_current()->{value} eq 'context') {
457 0         0 $self->_advance();
458 0 0       0 $with_context = ($ctx eq 'with') ? 1 : 0;
459             }
460             }
461             }
462              
463 0         0 my $end = $self->_expect('STMT_END');
464              
465             return {
466             type => NODE_FROM,
467             template => $template,
468             imports => \@imports,
469             with_context => $with_context,
470             strip_before => $strip_before,
471             strip_after => $end->{strip_after},
472 0         0 };
473             }
474              
475             sub _parse_set {
476 2     2   5 my ($self, $strip_before) = @_;
477              
478 2         7 $self->_advance(); # consume 'set'
479              
480             # Parse variable name(s)
481 2         4 my @names;
482 2         6 push @names, $self->_expect('NAME')->{value};
483              
484 2         8 while ($self->_check('COMMA')) {
485 0         0 $self->_advance();
486 0         0 push @names, $self->_expect('NAME')->{value};
487             }
488              
489             # Check if it's a block set or inline set
490 2 50       6 if ($self->_check('ASSIGN')) {
491 2         6 $self->_advance();
492 2         7 my $value = $self->_parse_expression();
493 2         9 my $end = $self->_expect('STMT_END');
494              
495             return {
496             type => NODE_SET,
497             names => \@names,
498             value => $value,
499             strip_before => $strip_before,
500             strip_after => $end->{strip_after},
501 2         21 };
502             } else {
503             # Block set
504 0         0 my $end = $self->_expect('STMT_END');
505              
506 0         0 my @body;
507 0         0 while (!$self->_at_end()) {
508 0 0       0 if ($self->_is_stmt_keyword('endset')) {
509 0         0 $self->_expect('STMT_START');
510 0         0 $self->_advance(); # consume 'endset'
511 0         0 $self->_expect('STMT_END');
512 0         0 last;
513             }
514              
515 0         0 my $node = $self->_parse_node();
516 0 0       0 push @body, $node if $node;
517             }
518              
519             return {
520 0         0 type => NODE_SET,
521             names => \@names,
522             body => \@body,
523             strip_before => $strip_before,
524             };
525             }
526             }
527              
528             sub _parse_macro {
529 1     1   2 my ($self, $strip_before) = @_;
530              
531 1         4 $self->_advance(); # consume 'macro'
532 1         4 my $name = $self->_expect('NAME')->{value};
533              
534             # Parse arguments
535 1         5 $self->_expect('LPAREN');
536 1         2 my @args;
537 1 50       5 unless ($self->_check('RPAREN')) {
538 1   33     3 do {
539 1         4 my $arg_name = $self->_expect('NAME')->{value};
540 1         2 my $default;
541 1 50       5 if ($self->_check('ASSIGN')) {
542 0         0 $self->_advance();
543 0         0 $default = $self->_parse_expression();
544             }
545 1         7 push @args, { name => $arg_name, default => $default };
546             } while ($self->_check('COMMA') && $self->_advance());
547             }
548 1         4 $self->_expect('RPAREN');
549 1         4 $self->_expect('STMT_END');
550              
551 1         3 my @body;
552 1         3 while (!$self->_at_end()) {
553 3 100       9 if ($self->_is_stmt_keyword('endmacro')) {
554 1         5 $self->_expect('STMT_START');
555 1         3 $self->_advance(); # consume 'endmacro'
556 1         4 $self->_expect('STMT_END');
557 1         3 last;
558             }
559              
560 2         7 my $node = $self->_parse_node();
561 2 50       10 push @body, $node if $node;
562             }
563              
564             return {
565 1         10 type => NODE_MACRO,
566             name => $name,
567             args => \@args,
568             body => \@body,
569             strip_before => $strip_before,
570             };
571             }
572              
573             sub _parse_call_block {
574 0     0   0 my ($self, $strip_before) = @_;
575              
576 0         0 $self->_advance(); # consume 'call'
577              
578             # Optional arguments
579 0         0 my @args;
580 0 0       0 if ($self->_check('LPAREN')) {
581 0         0 $self->_advance();
582 0 0       0 unless ($self->_check('RPAREN')) {
583 0   0     0 do {
584 0         0 push @args, $self->_expect('NAME')->{value};
585             } while ($self->_check('COMMA') && $self->_advance());
586             }
587 0         0 $self->_expect('RPAREN');
588             }
589              
590             # Parse the macro call
591 0         0 my $call = $self->_parse_expression();
592 0         0 $self->_expect('STMT_END');
593              
594 0         0 my @body;
595 0         0 while (!$self->_at_end()) {
596 0 0       0 if ($self->_is_stmt_keyword('endcall')) {
597 0         0 $self->_expect('STMT_START');
598 0         0 $self->_advance(); # consume 'endcall'
599 0         0 $self->_expect('STMT_END');
600 0         0 last;
601             }
602              
603 0         0 my $node = $self->_parse_node();
604 0 0       0 push @body, $node if $node;
605             }
606              
607             return {
608 0         0 type => NODE_CALL,
609             args => \@args,
610             call => $call,
611             body => \@body,
612             strip_before => $strip_before,
613             };
614             }
615              
616             sub _parse_filter_block {
617 0     0   0 my ($self, $strip_before) = @_;
618              
619 0         0 $self->_advance(); # consume 'filter'
620 0         0 my $filter = $self->_parse_filter_chain();
621 0         0 $self->_expect('STMT_END');
622              
623 0         0 my @body;
624 0         0 while (!$self->_at_end()) {
625 0 0       0 if ($self->_is_stmt_keyword('endfilter')) {
626 0         0 $self->_expect('STMT_START');
627 0         0 $self->_advance(); # consume 'endfilter'
628 0         0 $self->_expect('STMT_END');
629 0         0 last;
630             }
631              
632 0         0 my $node = $self->_parse_node();
633 0 0       0 push @body, $node if $node;
634             }
635              
636             return {
637 0         0 type => NODE_FILTER,
638             filter => $filter,
639             body => \@body,
640             strip_before => $strip_before,
641             };
642             }
643              
644             sub _parse_raw {
645 0     0   0 my ($self, $strip_before) = @_;
646              
647 0         0 $self->_advance(); # consume 'raw'
648 0         0 $self->_expect('STMT_END');
649              
650             # Collect everything until {% endraw %}
651 0         0 my $raw_text = '';
652              
653 0         0 while (!$self->_at_end()) {
654 0 0       0 if ($self->_is_stmt_keyword('endraw')) {
655 0         0 $self->_expect('STMT_START');
656 0         0 $self->_advance(); # consume 'endraw'
657 0         0 $self->_expect('STMT_END');
658 0         0 last;
659             }
660              
661 0         0 my $token = $self->_current();
662 0   0     0 $raw_text .= $token->{value} // '';
663 0         0 $self->_advance();
664             }
665              
666             return {
667 0         0 type => NODE_RAW,
668             value => $raw_text,
669             strip_before => $strip_before,
670             };
671             }
672              
673             sub _parse_with {
674 0     0   0 my ($self, $strip_before) = @_;
675              
676 0         0 $self->_advance(); # consume 'with'
677              
678             # Parse variable assignments
679 0         0 my @assignments;
680 0 0       0 unless ($self->_check('STMT_END')) {
681 0   0     0 do {
682 0         0 my $name = $self->_expect('NAME')->{value};
683 0         0 $self->_expect('ASSIGN');
684 0         0 my $value = $self->_parse_expression();
685 0         0 push @assignments, { name => $name, value => $value };
686             } while ($self->_check('COMMA') && $self->_advance());
687             }
688              
689 0         0 $self->_expect('STMT_END');
690              
691 0         0 my @body;
692 0         0 while (!$self->_at_end()) {
693 0 0       0 if ($self->_is_stmt_keyword('endwith')) {
694 0         0 $self->_expect('STMT_START');
695 0         0 $self->_advance(); # consume 'endwith'
696 0         0 $self->_expect('STMT_END');
697 0         0 last;
698             }
699              
700 0         0 my $node = $self->_parse_node();
701 0 0       0 push @body, $node if $node;
702             }
703              
704             return {
705 0         0 type => NODE_WITH,
706             assignments => \@assignments,
707             body => \@body,
708             strip_before => $strip_before,
709             };
710             }
711              
712             sub _parse_autoescape {
713 0     0   0 my ($self, $strip_before) = @_;
714              
715 0         0 $self->_advance(); # consume 'autoescape'
716 0         0 my $enabled = $self->_parse_expression();
717 0         0 $self->_expect('STMT_END');
718              
719 0         0 my @body;
720 0         0 while (!$self->_at_end()) {
721 0 0       0 if ($self->_is_stmt_keyword('endautoescape')) {
722 0         0 $self->_expect('STMT_START');
723 0         0 $self->_advance(); # consume 'endautoescape'
724 0         0 $self->_expect('STMT_END');
725 0         0 last;
726             }
727              
728 0         0 my $node = $self->_parse_node();
729 0 0       0 push @body, $node if $node;
730             }
731              
732             return {
733 0         0 type => NODE_AUTOESCAPE,
734             enabled => $enabled,
735             body => \@body,
736             strip_before => $strip_before,
737             };
738             }
739              
740             # Expression parsing with precedence
741             sub _parse_expression {
742 85     85   190 my ($self) = @_;
743 85         227 return $self->_parse_ternary();
744             }
745              
746             sub _parse_ternary {
747 87     87   199 my ($self) = @_;
748              
749 87         264 my $expr = $self->_parse_or();
750              
751             # Check for ternary: expr if condition else other
752 87 100 66     184 if ($self->_check('NAME') && $self->_current()->{value} eq 'if') {
753 2         9 $self->_advance();
754 2         7 my $condition = $self->_parse_or();
755              
756 2 50 33     9 if ($self->_check('NAME') && $self->_current()->{value} eq 'else') {
757 2         8 $self->_advance();
758 2         8 my $else_expr = $self->_parse_ternary();
759             return {
760 2         16 type => 'TERNARY',
761             true_val => $expr,
762             condition => $condition,
763             false_val => $else_expr,
764             };
765             } else {
766             # Short form: value if condition (no else)
767             return {
768 0         0 type => 'TERNARY',
769             true_val => $expr,
770             condition => $condition,
771             false_val => undef,
772             };
773             }
774             }
775              
776 85         239 return $expr;
777             }
778              
779             sub _parse_or {
780 89     89   177 my ($self) = @_;
781              
782 89         227 my $left = $self->_parse_and();
783              
784 89   66     190 while ($self->_check('OPERATOR') && $self->_current()->{value} eq 'or') {
785 2         9 $self->_advance();
786 2         6 my $right = $self->_parse_and();
787 2         14 $left = { type => 'BINOP', op => 'or', left => $left, right => $right };
788             }
789              
790 89         193 return $left;
791             }
792              
793             sub _parse_and {
794 91     91   183 my ($self) = @_;
795              
796 91         327 my $left = $self->_parse_not();
797              
798 91   100     256 while ($self->_check('OPERATOR') && $self->_current()->{value} eq 'and') {
799 2         10 $self->_advance();
800 2         7 my $right = $self->_parse_not();
801 2         16 $left = { type => 'BINOP', op => 'and', left => $left, right => $right };
802             }
803              
804 91         185 return $left;
805             }
806              
807             sub _parse_not {
808 94     94   181 my ($self) = @_;
809              
810 94 100 66     230 if ($self->_check('OPERATOR') && $self->_current()->{value} eq 'not') {
811 1         4 $self->_advance();
812 1         4 my $operand = $self->_parse_not();
813 1         7 return { type => 'UNARYOP', op => 'not', operand => $operand };
814             }
815              
816 93         259 return $self->_parse_comparison();
817             }
818              
819             sub _parse_comparison {
820 93     93   170 my ($self) = @_;
821              
822 93         236 my $left = $self->_parse_additive();
823              
824 93         203 while ($self->_check('OPERATOR')) {
825 8         22 my $op = $self->_current()->{value};
826 8 100       75 if ($op =~ /^(==|!=|<|>|<=|>=|in|is)$/) {
827 4         136 $self->_advance();
828              
829             # Handle 'is not' and 'not in'
830 4 50 33     36 if ($op eq 'is' && $self->_check('OPERATOR') && $self->_current()->{value} eq 'not') {
    50 33        
      33        
      33        
831 0         0 $self->_advance();
832 0         0 $op = 'is not';
833             } elsif ($op eq 'not' && $self->_check('OPERATOR') && $self->_current()->{value} eq 'in') {
834 0         0 $self->_advance();
835 0         0 $op = 'not in';
836             }
837              
838 4         12 my $right = $self->_parse_additive();
839 4         59 $left = { type => 'BINOP', op => $op, left => $left, right => $right };
840             } else {
841 4         11 last;
842             }
843             }
844              
845 93         204 return $left;
846             }
847              
848             sub _parse_additive {
849 97     97   181 my ($self) = @_;
850              
851 97         238 my $left = $self->_parse_multiplicative();
852              
853 97   100     243 while ($self->_check('OPERATOR') || $self->_check('TILDE')) {
854 10         27 my $op = $self->_current()->{value};
855 10 100       46 if ($op =~ /^[+\-~]$/) {
856 2         10 $self->_advance();
857 2         10 my $right = $self->_parse_multiplicative();
858 2         17 $left = { type => 'BINOP', op => $op, left => $left, right => $right };
859             } else {
860 8         18 last;
861             }
862             }
863              
864 97         233 return $left;
865             }
866              
867             sub _parse_multiplicative {
868 99     99   204 my ($self) = @_;
869              
870 99         259 my $left = $self->_parse_unary();
871              
872 99         319 while ($self->_check('OPERATOR')) {
873 9         28 my $op = $self->_current()->{value};
874 9 50 33     105 if ($op =~ /^[*\/%]$/ || $op eq '//' || $op eq '**') {
      33        
875 0         0 $self->_advance();
876 0         0 my $right = $self->_parse_unary();
877 0         0 $left = { type => 'BINOP', op => $op, left => $left, right => $right };
878             } else {
879 9         24 last;
880             }
881             }
882              
883 99         1418 return $left;
884             }
885              
886             sub _parse_unary {
887 99     99   182 my ($self) = @_;
888              
889 99 50 33     203 if ($self->_check('OPERATOR') && $self->_current()->{value} =~ /^[+\-]$/) {
890 0         0 my $op = $self->_current()->{value};
891 0         0 $self->_advance();
892 0         0 my $operand = $self->_parse_unary();
893 0         0 return { type => 'UNARYOP', op => $op, operand => $operand };
894             }
895              
896 99         250 return $self->_parse_filter_chain();
897             }
898              
899             sub _parse_filter_chain {
900 99     99   183 my ($self) = @_;
901              
902 99         304 my $expr = $self->_parse_postfix();
903              
904 99         223 while ($self->_check('PIPE')) {
905 14         42 $self->_advance();
906 14         37 my $filter_name = $self->_expect('NAME')->{value};
907 14         46 my @args;
908              
909 14 100       32 if ($self->_check('LPAREN')) {
910 2         9 $self->_advance();
911 2 50       6 unless ($self->_check('RPAREN')) {
912 2   33     6 do {
913             # Named argument?
914 2 50 33     7 if ($self->_check('NAME') && $self->_peek() && $self->_peek()->{type} eq 'ASSIGN') {
      33        
915 0         0 my $name = $self->_expect('NAME')->{value};
916 0         0 $self->_advance(); # consume =
917 0         0 my $value = $self->_parse_expression();
918 0         0 push @args, { type => 'NAMED_ARG', name => $name, value => $value };
919             } else {
920 2         11 push @args, $self->_parse_expression();
921             }
922             } while ($self->_check('COMMA') && $self->_advance());
923             }
924 2         8 $self->_expect('RPAREN');
925             }
926              
927 14         83 $expr = { type => 'FILTER', name => $filter_name, expr => $expr, args => \@args };
928             }
929              
930 99         223 return $expr;
931             }
932              
933             sub _parse_postfix {
934 99     99   186 my ($self) = @_;
935              
936 99         223 my $expr = $self->_parse_primary();
937              
938 99         187 while (1) {
939 110 100       238 if ($self->_check('DOT')) {
    100          
    100          
940 8         24 $self->_advance();
941 8         30 my $attr = $self->_expect('NAME')->{value};
942 8         59 $expr = { type => 'GETATTR', expr => $expr, attr => $attr };
943             } elsif ($self->_check('LBRACKET')) {
944 1         4 $self->_advance();
945 1         5 my $index = $self->_parse_expression();
946 1         5 $self->_expect('RBRACKET');
947 1         6 $expr = { type => 'GETITEM', expr => $expr, index => $index };
948             } elsif ($self->_check('LPAREN')) {
949 2         8 $self->_advance();
950 2         6 my @args;
951             my @kwargs;
952 2 50       8 unless ($self->_check('RPAREN')) {
953 2   33     6 do {
954             # Named argument?
955 2 50 33     8 if ($self->_check('NAME') && $self->_peek() && $self->_peek()->{type} eq 'ASSIGN') {
      33        
956 0         0 my $name = $self->_expect('NAME')->{value};
957 0         0 $self->_advance(); # consume =
958 0         0 my $value = $self->_parse_expression();
959 0         0 push @kwargs, { name => $name, value => $value };
960             } else {
961 2         10 push @args, $self->_parse_expression();
962             }
963             } while ($self->_check('COMMA') && $self->_advance());
964             }
965 2         10 $self->_expect('RPAREN');
966 2         17 $expr = { type => 'CALL', expr => $expr, args => \@args, kwargs => \@kwargs };
967             } else {
968 99         192 last;
969             }
970             }
971              
972 99         231 return $expr;
973             }
974              
975             sub _parse_primary {
976 99     99   170 my ($self) = @_;
977              
978 99         194 my $token = $self->_current();
979              
980             # Name/identifier
981 99 100       274 if ($token->{type} eq 'NAME') {
982 76         197 my $name = $token->{value};
983 76         227 $self->_advance();
984              
985             # Handle boolean/none literals
986 76 100       437 if ($name =~ /^(true|True)$/) {
    100          
    50          
987 2         53 return { type => 'LITERAL', value => 1, subtype => 'BOOL' };
988             } elsif ($name =~ /^(false|False)$/) {
989 2         13 return { type => 'LITERAL', value => 0, subtype => 'BOOL' };
990             } elsif ($name =~ /^(none|None)$/) {
991 0         0 return { type => 'LITERAL', value => undef, subtype => 'NONE' };
992             }
993              
994 72         371 return { type => 'NAME', value => $name };
995             }
996              
997             # Number
998 23 100       73 if ($token->{type} eq 'NUMBER') {
999 13         40 $self->_advance();
1000 13         35 my $val = $token->{value};
1001 13         37 $val =~ s/_//g; # Remove underscores
1002 13         79 return { type => 'LITERAL', value => $val, subtype => 'NUMBER' };
1003             }
1004              
1005             # String
1006 10 100       38 if ($token->{type} eq 'STRING') {
1007 6         22 $self->_advance();
1008 6         19 my $val = $token->{value};
1009             # Remove quotes and handle escapes
1010 6         56 $val =~ s/^['"]|['"]$//g;
1011 6         19 $val =~ s/\\(['"])/$1/g;
1012 6         13 $val =~ s/\\n/\n/g;
1013 6         12 $val =~ s/\\t/\t/g;
1014 6         14 $val =~ s/\\\\/\\/g;
1015 6         39 return { type => 'LITERAL', value => $val, subtype => 'STRING' };
1016             }
1017              
1018             # Parenthesized expression or tuple
1019 4 50       19 if ($token->{type} eq 'LPAREN') {
1020 0         0 $self->_advance();
1021 0 0       0 if ($self->_check('RPAREN')) {
1022 0         0 $self->_advance();
1023 0         0 return { type => 'TUPLE', elements => [] };
1024             }
1025              
1026 0         0 my $expr = $self->_parse_expression();
1027              
1028 0 0       0 if ($self->_check('COMMA')) {
1029             # It's a tuple
1030 0         0 my @elements = ($expr);
1031 0         0 while ($self->_check('COMMA')) {
1032 0         0 $self->_advance();
1033 0 0       0 last if $self->_check('RPAREN');
1034 0         0 push @elements, $self->_parse_expression();
1035             }
1036 0         0 $self->_expect('RPAREN');
1037 0         0 return { type => 'TUPLE', elements => \@elements };
1038             }
1039              
1040 0         0 $self->_expect('RPAREN');
1041 0         0 return $expr;
1042             }
1043              
1044             # List
1045 4 100       18 if ($token->{type} eq 'LBRACKET') {
1046 2         8 $self->_advance();
1047 2         4 my @elements;
1048 2 50       6 unless ($self->_check('RBRACKET')) {
1049 2   66     4 do {
1050 6         18 push @elements, $self->_parse_expression();
1051             } while ($self->_check('COMMA') && $self->_advance());
1052             }
1053 2         11 $self->_expect('RBRACKET');
1054 2         12 return { type => 'LIST', elements => \@elements };
1055             }
1056              
1057             # Dict
1058 2 50       12 if ($token->{type} eq 'LBRACE') {
1059 2         8 $self->_advance();
1060 2         3 my @pairs;
1061 2 50       8 unless ($self->_check('RBRACE')) {
1062 2   33     6 do {
1063 2         8 my $key = $self->_parse_expression();
1064 2         12 $self->_expect('COLON');
1065 2         6 my $val = $self->_parse_expression();
1066 2         16 push @pairs, { key => $key, value => $val };
1067             } while ($self->_check('COMMA') && $self->_advance());
1068             }
1069 2         9 $self->_expect('RBRACE');
1070 2         11 return { type => 'DICT', pairs => \@pairs };
1071             }
1072              
1073 0   0     0 die "Unexpected token: " . ($token->{type} // 'undef') . " at position " . ($token->{pos} // 'unknown');
      0        
1074             }
1075              
1076             # Helper methods
1077             sub _current {
1078 2187     2187   3703 my ($self) = @_;
1079 2187         4466 return $self->{tokens}[$self->{pos}];
1080             }
1081              
1082             sub _peek {
1083 0     0   0 my ($self) = @_;
1084 0         0 return $self->{tokens}[$self->{pos} + 1];
1085             }
1086              
1087             sub _advance {
1088 482     482   919 my ($self) = @_;
1089 482         886 my $token = $self->{tokens}[$self->{pos}];
1090 482 50       818 $self->{pos}++ if $self->{pos} < @{$self->{tokens}};
  482         1312  
1091 482         1029 return $token;
1092             }
1093              
1094             sub _check {
1095 1347     1347   2516 my ($self, $type) = @_;
1096 1347         2489 my $token = $self->_current();
1097 1347   66     6270 return $token && $token->{type} eq $type;
1098             }
1099              
1100             sub _expect {
1101 246     246   576 my ($self, $type) = @_;
1102 246         548 my $token = $self->_current();
1103 246 50 33     1110 if (!$token || $token->{type} ne $type) {
1104             die "Expected $type but got " . ($token->{type} // 'EOF') .
1105 0   0     0 " at position " . ($token->{pos} // 'unknown');
      0        
1106             }
1107 246         532 return $self->_advance();
1108             }
1109              
1110             sub _expect_keyword {
1111 0     0   0 my ($self, $keyword) = @_;
1112 0         0 my $token = $self->_current();
1113 0 0 0     0 if (!$token || $token->{type} ne 'NAME' || $token->{value} ne $keyword) {
      0        
1114             die "Expected keyword '$keyword' but got " .
1115 0   0     0 ($token->{value} // $token->{type} // 'EOF');
      0        
1116             }
1117 0         0 return $self->_advance();
1118             }
1119              
1120             sub _at_end {
1121 193     193   421 my ($self) = @_;
1122 193         485 my $token = $self->_current();
1123 193   66     1133 return !$token || $token->{type} eq 'EOF';
1124             }
1125              
1126             sub _is_stmt_keyword {
1127 134     134   260 my ($self, $keyword) = @_;
1128              
1129 134         258 my $token = $self->_current();
1130 134 100 66     591 return 0 unless $token && $token->{type} eq 'STMT_START';
1131              
1132 64         146 my $next = $self->{tokens}[$self->{pos} + 1];
1133 64   66     367 return $next && $next->{type} eq 'NAME' && $next->{value} eq $keyword;
1134             }
1135              
1136             1;
1137              
1138             __END__