File Coverage

lib/Jinja2/TT2/Emitter.pm
Criterion Covered Total %
statement 176 272 64.7
branch 60 108 55.5
condition 4 8 50.0
subroutine 17 26 65.3
pod 0 2 0.0
total 257 416 61.7


line stmt bran cond sub pod time code
1             package Jinja2::TT2::Emitter;
2              
3 1     1   16 use strict;
  1         3  
  1         43  
4 1     1   5 use warnings;
  1         2  
  1         50  
5 1     1   14 use v5.20;
  1         4  
6              
7 1     1   614 use Jinja2::TT2::Filters;
  1         3  
  1         4910  
8              
9             sub new {
10 1     1 0 3 my ($class, %opts) = @_;
11             return bless {
12             indent_level => 0,
13 1   50     13 indent_str => $opts{indent_str} // ' ',
14             filters => Jinja2::TT2::Filters->new(),
15             }, $class;
16             }
17              
18             sub emit {
19 112     112 0 201 my ($self, $ast) = @_;
20              
21 112 100       281 if ($ast->{type} eq 'ROOT') {
22 43         77 return join('', map { $self->emit($_) } @{$ast->{body}});
  49         125  
  43         121  
23             }
24              
25 69         192 my $method = '_emit_' . lc($ast->{type});
26 69 50       295 if ($self->can($method)) {
27 69         240 return $self->$method($ast);
28             }
29              
30 0         0 die "Unknown AST node type: $ast->{type}";
31             }
32              
33             sub _emit_text {
34 19     19   36 my ($self, $node) = @_;
35 19         129 return $node->{value};
36             }
37              
38             sub _emit_comment {
39 1     1   4 my ($self, $node) = @_;
40 1         14 return "[%# $node->{value} %]";
41             }
42              
43             sub _emit_output {
44 32     32   99 my ($self, $node) = @_;
45              
46 32 100       84 my $strip_before = $node->{strip_before} ? '-' : '';
47 32 100       94 my $strip_after = $node->{strip_after} ? '-' : '';
48              
49 32         102 my $expr = $self->_emit_expr($node->{expr});
50 32         545 return "[%$strip_before $expr $strip_after%]";
51             }
52              
53             sub _emit_if {
54 10     10   26 my ($self, $node) = @_;
55              
56 10 50       30 my $strip_before = $node->{strip_before} ? '-' : '';
57 10 50       29 my $strip_after = $node->{strip_after} ? '-' : '';
58              
59 10         34 my $condition = $self->_emit_expr($node->{condition});
60 10         58 my $output = "[%$strip_before IF $condition $strip_after%]";
61              
62 10         21 for my $child (@{$node->{body}}) {
  10         32  
63 6         43 $output .= $self->emit($child);
64             }
65              
66 10         20 for my $branch (@{$node->{branches}}) {
  10         23  
67 4 100       19 if ($branch->{type} eq 'ELIF') {
    50          
68 1         6 my $elif_cond = $self->_emit_expr($branch->{condition});
69 1         4 $output .= "[% ELSIF $elif_cond %]";
70 1         2 for my $child (@{$branch->{body}}) {
  1         4  
71 1         4 $output .= $self->emit($child);
72             }
73             } elsif ($branch->{type} eq 'ELSE') {
74 3         8 $output .= "[% ELSE %]";
75 3         6 for my $child (@{$branch->{body}}) {
  3         9  
76 3         10 $output .= $self->emit($child);
77             }
78             }
79             }
80              
81 10         27 $output .= "[% END %]";
82 10         164 return $output;
83             }
84              
85             sub _emit_for {
86 3     3   7 my ($self, $node) = @_;
87              
88 3         8 my $loop_var = join(', ', @{$node->{loop_vars}});
  3         11  
89 3         28 my $iterable = $self->_emit_expr($node->{iterable});
90              
91 3         9 my $output = "[% FOREACH $loop_var IN $iterable %]";
92              
93 3         5 for my $child (@{$node->{body}}) {
  3         11  
94 7         16 $output .= $self->emit($child);
95             }
96              
97             # Handle else clause (TT2 doesn't have for/else, need WRAPPER or conditional)
98 3 50       6 if (@{$node->{else_body}}) {
  3         10  
99             # TT2 workaround: use IF for empty check
100 0         0 $output = "[% IF $iterable.size %]" . $output . "[% ELSE %]";
101 0         0 for my $child (@{$node->{else_body}}) {
  0         0  
102 0         0 $output .= $self->emit($child);
103             }
104 0         0 $output .= "[% END %]";
105             }
106              
107 3 50       6 $output .= "[% END %]" unless @{$node->{else_body}};
  3         10  
108              
109 3         39 return $output;
110             }
111              
112             sub _emit_block {
113 1     1   3 my ($self, $node) = @_;
114              
115 1         4 my $output = "[% BLOCK $node->{name} %]";
116              
117 1         2 for my $child (@{$node->{body}}) {
  1         4  
118 1         4 $output .= $self->emit($child);
119             }
120              
121 1         2 $output .= "[% END %]";
122 1         13 return $output;
123             }
124              
125             sub _emit_extends {
126 0     0   0 my ($self, $node) = @_;
127              
128 0         0 my $template = $self->_emit_expr($node->{template});
129             # Remove quotes for TT2 PROCESS/WRAPPER
130 0         0 $template =~ s/^['"]|['"]$//g;
131              
132             # TT2 doesn't have direct extends; use WRAPPER or PROCESS
133             # For now, emit as a comment + PROCESS
134 0         0 return "[%# EXTENDS: $template - use WRAPPER in TT2 %]\n[% PROCESS $template %]";
135             }
136              
137             sub _emit_include {
138 1     1   4 my ($self, $node) = @_;
139              
140 1 50       7 my $strip_before = $node->{strip_before} ? '-' : '';
141 1 50       4 my $strip_after = $node->{strip_after} ? '-' : '';
142              
143 1         5 my $template = $self->_emit_expr($node->{template});
144 1         9 $template =~ s/^['"]|['"]$//g;
145              
146 1         15 return "[%$strip_before INCLUDE $template $strip_after%]";
147             }
148              
149             sub _emit_import {
150 0     0   0 my ($self, $node) = @_;
151              
152 0         0 my $template = $self->_emit_expr($node->{template});
153 0         0 $template =~ s/^['"]|['"]$//g;
154              
155             # TT2 uses PROCESS for imports; macros become available
156 0         0 return "[%# IMPORT $template AS $node->{alias} %]\n[% USE $node->{alias} = $template %]";
157             }
158              
159             sub _emit_from {
160 0     0   0 my ($self, $node) = @_;
161              
162 0         0 my $template = $self->_emit_expr($node->{template});
163 0         0 $template =~ s/^['"]|['"]$//g;
164              
165 0 0       0 my @imports = map { $_->{name} . ($_->{alias} ne $_->{name} ? " AS $_->{alias}" : '') }
166 0         0 @{$node->{imports}};
  0         0  
167              
168 0         0 return "[%# FROM $template IMPORT " . join(', ', @imports) . " %]";
169             }
170              
171             sub _emit_set {
172 1     1   4 my ($self, $node) = @_;
173              
174 1         3 my $names = join(', ', @{$node->{names}});
  1         5  
175              
176 1 50       4 if ($node->{value}) {
177             # Inline set
178 1         5 my $value = $self->_emit_expr($node->{value});
179 1         15 return "[% $names = $value %]";
180             } else {
181             # Block set - use FILTER to capture content
182 0         0 my $output = "[% FILTER \$set_$names %]";
183 0         0 for my $child (@{$node->{body}}) {
  0         0  
184 0         0 $output .= $self->emit($child);
185             }
186 0         0 $output .= "[% END %][% $names = set_$names %]";
187 0         0 return $output;
188             }
189             }
190              
191             sub _emit_macro {
192 1     1   4 my ($self, $node) = @_;
193              
194             my @args = map {
195 1         3 my $arg = $_->{name};
196 1 50       4 if (defined $_->{default}) {
197 0         0 $arg .= " = " . $self->_emit_expr($_->{default});
198             }
199 1         5 $arg;
200 1         2 } @{$node->{args}};
  1         3  
201              
202 1         6 my $output = "[% MACRO $node->{name}(" . join(', ', @args) . ") BLOCK %]";
203              
204 1         2 for my $child (@{$node->{body}}) {
  1         3  
205 2         24 $output .= $self->emit($child);
206             }
207              
208 1         3 $output .= "[% END %]";
209 1         15 return $output;
210             }
211              
212             sub _emit_call {
213 0     0   0 my ($self, $node) = @_;
214              
215 0         0 my $call = $self->_emit_expr($node->{call});
216              
217             # TT2 doesn't have direct call blocks; emit as WRAPPER
218 0         0 my $output = "[%# CALL $call %]\n[% WRAPPER $call %]";
219              
220 0         0 for my $child (@{$node->{body}}) {
  0         0  
221 0         0 $output .= $self->emit($child);
222             }
223              
224 0         0 $output .= "[% END %]";
225 0         0 return $output;
226             }
227              
228             sub _emit_filter {
229 0     0   0 my ($self, $node) = @_;
230              
231 0         0 my $filter = $self->_emit_filter_single($node->{filter});
232              
233 0         0 my $output = "[% FILTER $filter %]";
234              
235 0         0 for my $child (@{$node->{body}}) {
  0         0  
236 0         0 $output .= $self->emit($child);
237             }
238              
239 0         0 $output .= "[% END %]";
240 0         0 return $output;
241             }
242              
243             sub _emit_raw {
244 0     0   0 my ($self, $node) = @_;
245             # TT2 has no raw tag; content is already literal
246             # Could use [% RAWPERL %] but that's different
247 0         0 return $node->{value};
248             }
249              
250             sub _emit_with {
251 0     0   0 my ($self, $node) = @_;
252              
253             # TT2 doesn't have 'with'; simulate with SET in a wrapper
254 0         0 my $output = "[%# WITH scope %]";
255              
256 0         0 for my $assign (@{$node->{assignments}}) {
  0         0  
257 0         0 $output .= "[% SET $assign->{name} = " . $self->_emit_expr($assign->{value}) . " %]";
258             }
259              
260 0         0 for my $child (@{$node->{body}}) {
  0         0  
261 0         0 $output .= $self->emit($child);
262             }
263              
264 0         0 return $output;
265             }
266              
267             sub _emit_autoescape {
268 0     0   0 my ($self, $node) = @_;
269              
270             # TT2 handles escaping differently; emit as comment
271 0         0 my $output = "[%# AUTOESCAPE " . $self->_emit_expr($node->{enabled}) . " %]";
272              
273 0         0 for my $child (@{$node->{body}}) {
  0         0  
274 0         0 $output .= $self->emit($child);
275             }
276              
277 0         0 $output .= "[%# END AUTOESCAPE %]";
278 0         0 return $output;
279             }
280              
281             # Expression emitters
282             sub _emit_expr {
283 91     91   222 my ($self, $node) = @_;
284              
285 91 50       192 return '' unless $node;
286              
287 91         205 my $type = $node->{type};
288              
289 91 100       207 if ($type eq 'NAME') {
290 48         91 my $name = $node->{value};
291             # Map Jinja2 loop variables to TT2
292 48 100       105 if ($name eq 'loop') {
293 5         15 return 'loop';
294             }
295 43         179 return $name;
296             }
297              
298 43 100       108 if ($type eq 'LITERAL') {
299 14 100       77 if ($node->{subtype} eq 'STRING') {
    100          
    50          
300 3         9 my $val = $node->{value};
301 3         10 $val =~ s/'/\\'/g;
302 3         14 return "'$val'";
303             } elsif ($node->{subtype} eq 'BOOL') {
304 4 100       18 return $node->{value} ? '1' : '0';
305             } elsif ($node->{subtype} eq 'NONE') {
306 0         0 return 'undef';
307             } else {
308 7         28 return $node->{value};
309             }
310             }
311              
312 29 100       70 if ($type eq 'BINOP') {
313 6         20 my $left = $self->_emit_expr($node->{left});
314 6         19 my $right = $self->_emit_expr($node->{right});
315 6         15 my $op = $node->{op};
316              
317             # Map operators
318 6         53 my %op_map = (
319             'and' => 'AND',
320             'or' => 'OR',
321             '~' => '_', # String concatenation
322             'in' => 'IN',
323             'not in' => 'NOT IN',
324             '==' => '==',
325             '!=' => '!=',
326             '//' => 'div', # Integer division
327             );
328              
329 6   66     28 $op = $op_map{$op} // $op;
330              
331 6         34 return "($left $op $right)";
332             }
333              
334 23 100       55 if ($type eq 'UNARYOP') {
335 1         5 my $operand = $self->_emit_expr($node->{operand});
336 1         4 my $op = $node->{op};
337              
338 1 50       4 if ($op eq 'not') {
339 1         4 return "NOT $operand";
340             }
341              
342 0         0 return "$op$operand";
343             }
344              
345 22 100       54 if ($type eq 'TERNARY') {
346 1         5 my $true_val = $self->_emit_expr($node->{true_val});
347 1         4 my $condition = $self->_emit_expr($node->{condition});
348 1 50       7 my $false_val = $node->{false_val} ? $self->_emit_expr($node->{false_val}) : "''";
349              
350 1         6 return "($condition ? $true_val : $false_val)";
351             }
352              
353 21 100       66 if ($type eq 'GETATTR') {
354 7         35 my $expr = $self->_emit_expr($node->{expr});
355 7         17 my $attr = $node->{attr};
356              
357             # Map loop attributes
358 7 100       20 if ($expr eq 'loop') {
359 5         46 my %loop_map = (
360             'index' => 'count',
361             'index0' => 'index',
362             'revindex' => 'max - loop.index + 1',
363             'first' => 'first',
364             'last' => 'last',
365             'length' => 'size',
366             'cycle' => 'cycle',
367             );
368 5   33     36 return 'loop.' . ($loop_map{$attr} // $attr);
369             }
370              
371 2         8 return "$expr.$attr";
372             }
373              
374 14 100       34 if ($type eq 'GETITEM') {
375 1         23 my $expr = $self->_emit_expr($node->{expr});
376 1         4 my $index = $self->_emit_expr($node->{index});
377              
378 1 50       36 return "$expr.\$index" if $index =~ /^[a-zA-Z]/;
379 1         6 return "$expr.$index";
380             }
381              
382 13 100       35 if ($type eq 'CALL') {
383 1         6 my $expr = $self->_emit_expr($node->{expr});
384 1         3 my @args = map { $self->_emit_expr($_) } @{$node->{args}};
  1         4  
  1         3  
385              
386 1         3 for my $kwarg (@{$node->{kwargs}}) {
  1         6  
387 0         0 push @args, "$kwarg->{name} = " . $self->_emit_expr($kwarg->{value});
388             }
389              
390             # Handle built-in functions
391 1 50       5 if ($expr eq 'range') {
392 1 50       4 if (@args == 1) {
    0          
    0          
393 1         5 return "[0 .. $args[0] - 1]";
394             } elsif (@args == 2) {
395 0         0 return "[$args[0] .. $args[1] - 1]";
396             } elsif (@args == 3) {
397             # range with step - complex in TT2
398 0         0 return "[% # range($args[0], $args[1], $args[2]) %]";
399             }
400             }
401              
402 0 0       0 if ($expr eq 'super') {
403 0         0 return 'content'; # TT2's WRAPPER content
404             }
405              
406 0         0 return "$expr(" . join(', ', @args) . ")";
407             }
408              
409 12 100       33 if ($type eq 'FILTER') {
410 10         90 my $base = $self->_emit_expr($node->{expr});
411 10         34 return $self->_emit_filter_application($base, $node);
412             }
413              
414 2 100       8 if ($type eq 'LIST') {
415 1         3 my @elements = map { $self->_emit_expr($_) } @{$node->{elements}};
  3         98  
  1         4  
416 1         7 return '[' . join(', ', @elements) . ']';
417             }
418              
419 1 50       4 if ($type eq 'TUPLE') {
420 0         0 my @elements = map { $self->_emit_expr($_) } @{$node->{elements}};
  0         0  
  0         0  
421 0         0 return '[' . join(', ', @elements) . ']'; # TT2 uses arrays
422             }
423              
424 1 50       5 if ($type eq 'DICT') {
425             my @pairs = map {
426 1         5 my $key = $self->_emit_expr($_->{key});
427 1         5 my $val = $self->_emit_expr($_->{value});
428 1         6 "$key => $val";
429 1         2 } @{$node->{pairs}};
  1         4  
430 1         6 return '{ ' . join(', ', @pairs) . ' }';
431             }
432              
433 0 0       0 if ($type eq 'NAMED_ARG') {
434 0         0 my $value = $self->_emit_expr($node->{value});
435 0         0 return "$node->{name} = $value";
436             }
437              
438 0         0 die "Unknown expression type: $type";
439             }
440              
441             sub _emit_filter_application {
442 10     10   25 my ($self, $base, $node) = @_;
443              
444 10         20 my $filter_name = $node->{name};
445 10         21 my @args = map { $self->_emit_expr($_) } @{$node->{args}};
  1         4  
  10         24  
446              
447             # Get the TT2 equivalent
448 10         49 my $tt2_filter = $self->{filters}->map_filter($filter_name, \@args);
449              
450 10 50       29 if ($tt2_filter->{type} eq 'vmethod') {
    0          
    0          
451 10 100       24 if ($tt2_filter->{args}) {
452 1         8 return "$base.$tt2_filter->{name}($tt2_filter->{args})";
453             } else {
454 9         46 return "$base.$tt2_filter->{name}";
455             }
456             } elsif ($tt2_filter->{type} eq 'filter') {
457 0 0         if ($tt2_filter->{args}) {
458 0           return "$base | $tt2_filter->{name}($tt2_filter->{args})";
459             } else {
460 0           return "$base | $tt2_filter->{name}";
461             }
462             } elsif ($tt2_filter->{type} eq 'custom') {
463 0           return $tt2_filter->{code}->($base, @args);
464             } else {
465             # Passthrough - keep original name
466 0 0         my $args_str = @args ? '(' . join(', ', @args) . ')' : '';
467 0           return "$base | $filter_name$args_str";
468             }
469             }
470              
471             sub _emit_filter_single {
472 0     0     my ($self, $node) = @_;
473              
474 0 0         if ($node->{type} eq 'FILTER') {
    0          
475 0 0         my $inner = $node->{expr} ? $self->_emit_filter_single($node->{expr}) : '';
476 0           my $filter_name = $node->{name};
477 0           my @args = map { $self->_emit_expr($_) } @{$node->{args}};
  0            
  0            
478              
479 0 0         my $args_str = @args ? '(' . join(', ', @args) . ')' : '';
480 0           my $this_filter = "$filter_name$args_str";
481              
482 0 0         return $inner ? "$inner | $this_filter" : $this_filter;
483             } elsif ($node->{type} eq 'NAME') {
484 0           return $node->{value};
485             }
486              
487 0           die "Unexpected filter node type: $node->{type}";
488             }
489              
490             1;
491              
492             __END__