File Coverage

blib/lib/Text/MustacheTemplate/Compiler.pm
Criterion Covered Total %
statement 252 257 100.0
branch 124 136 100.0
condition 15 15 100.0
subroutine 21 21 100.0
pod 1 1 100.0
total 413 430 100.0


line stmt bran cond sub pod time code
1             package Text::MustacheTemplate::Compiler;
2 13     13   241 use 5.022000;
  13         45  
3 13     13   66 use strict;
  13         29  
  13         346  
4 13     13   55 use warnings;
  13         26  
  13         642  
5              
6 13     13   93 use B ();
  13         36  
  13         415  
7 13     13   71 use List::Util qw/none/;
  13         24  
  13         1141  
8 13     13   99 use Carp qw/croak/;
  13         57  
  13         689  
9              
10 13     13   62 use Text::MustacheTemplate::Lexer;
  13         31  
  13         645  
11 13     13   62 use Text::MustacheTemplate::Parser qw/:syntaxes :variables :boxes :references/;
  13         21  
  13         2514  
12 13     13   6195 use Text::MustacheTemplate::Evaluator qw/retrieve_variable evaluate_section_variable evaluate_section/;
  13         40  
  13         1133  
13 13     13   5854 use Text::MustacheTemplate::HTML qw/escape_html/;
  13         40  
  13         1251  
14              
15             use constant {
16             DEBUG => !!$ENV{PERL_TEXT_MUSTACHE_TEMPLATE_COMPILER_DEBUG},
17 13         55354 DISCARD_RESULT => '##DISCARD##',
18 13     13   121 };
  13         24  
19              
20             our @CONTEXT_HINT; # for optimize
21              
22             our $_PADDING;
23             our $_PARENT;
24             our $_DEFAULT_OPEN_DELIMITER;
25             our $_DEFAULT_CLOSE_DELIMITER;
26             our $_CURRENT_OPEN_DELIMITER;
27             our $_CURRENT_CLOSE_DELIMITER;
28              
29             sub compile {
30 527     527 1 1353 my ($class, $ast) = @_;
31 527 50       1233 die "Invalid AST: empty AST" unless @$ast; # uncoverable branch true
32              
33 527         1220 my @ast = @$ast;
34 527         982 my $first_delimiter_syntax = shift @ast;
35 527         1326 my ($type, $open_delimiter, $close_delimiter) = @$first_delimiter_syntax;
36 527 50       1242 if ($type != SYNTAX_DELIMITER) { # uncoverable branch true
37 0         0 croak "Invalid AST: Delimiter should be first syntax"; # uncoverable statement
38             }
39              
40 527         896 @ast = do {
41 527         1027 local $_DEFAULT_OPEN_DELIMITER = $open_delimiter;
42 527         921 local $_DEFAULT_CLOSE_DELIMITER = $close_delimiter;
43 527         1421 _optimize(\@ast, 0);
44             };
45              
46             # Optimize: empty
47 527 100   1   1350 return sub { '' } if @ast == 0;
  1         30  
48              
49             # Optimize: raw text only
50 526 100 100     1821 if (@ast == 1 && $ast[0][0] == SYNTAX_RAW_TEXT) {
51 134         263 my (undef, $text) = @{ $ast[0] };
  134         820  
52 134 100 100     674 if (!@CONTEXT_HINT && $text =~ /[\r\n](?!\z)/mano) {
53             return sub {
54 17 100   17   687 defined $_PADDING ? $text =~ s/(\r\n?|\n)(?!\z)/${1}${_PADDING}/mgar : $text
55 17         261 };
56             }
57 117     107   1418 return sub { $text };
  107         1726  
58             }
59              
60 392         839 my $code = eval {
61 392         622 local $_PARENT;
62 392         717 local $_DEFAULT_OPEN_DELIMITER = $open_delimiter;
63 392         754 local $_DEFAULT_CLOSE_DELIMITER = $close_delimiter;
64 392         769 local $_CURRENT_OPEN_DELIMITER = $open_delimiter;
65 392         731 local $_CURRENT_CLOSE_DELIMITER = $close_delimiter;
66 392         975 _generate_code(\@ast, 4);
67             };
68 392 50       1179 die "Invalid AST: $@" if "$@"; # uncoverable branch true
69              
70             # wrap to define function global variables
71 392         1346 $code = <<__CODE__;
72             do {
73             our (\%_BLOCKS, \@_CTX, \$_OPEN_DELIMITER, \$_CLOSE_DELIMITER);
74             my (\$_name, \$_tmp, \@_section);
75             $code
76             };
77             __CODE__
78 392         686 warn $code if DEBUG; # uncoverable branch true
79              
80 392         153443 my $f = eval $code;
81 392 50       1772 die $@ if $@; # uncoverable branch true
82 392         9055 return $f;
83             }
84              
85             sub _optimize {
86 865     865   1667 my ($ast, $depth) = @_;
87              
88 865         1476 my $raw_text_syntax;
89             my @optimized_ast;
90 865         1669 for my $syntax (@$ast) {
91 2022 100       4954 if ($syntax->[0] == SYNTAX_RAW_TEXT) {
    100          
    100          
92 1155 100       2664 if ($raw_text_syntax) {
93 135         342 $raw_text_syntax->[1] .= $syntax->[1];
94             } else {
95 1020         2156 $raw_text_syntax = $syntax;
96             }
97             } elsif ($syntax->[0] == SYNTAX_COMMENT) {
98             # ignore
99             } elsif ($syntax->[0] == SYNTAX_DELIMITER) {
100             # keep it and keep raw text syntax context both
101 35         77 push @optimized_ast => $syntax;
102             } else {
103 802 100 100     2507 if (@CONTEXT_HINT && $depth == 0) {
104 209 100       676 if ($syntax->[0] == SYNTAX_VARIABLE) {
    100          
105 68         162 my (undef, $type, $name) = @$syntax;
106 68         153 local our $_OPEN_DELIMITER = $_DEFAULT_OPEN_DELIMITER;
107 68         110 local our $_CLOSE_DELIMITER = $_DEFAULT_CLOSE_DELIMITER;
108 68         159 local our @_CTX = @CONTEXT_HINT;
109 68 100       176 local $Text::MustacheTemplate::Evaluator::LAMBDA_RENDERER = \&_render_template_in_context if $Text::MustacheTemplate::LAMBDA_TEMPLATE_RENDERING;
110              
111 68 100       381 my $value = $name eq '.' ? $_CTX[-1] : retrieve_variable(\@_CTX, split /\./ano, $name);
112 68 100       200 next unless $value;
113 60 100       155 if ($type == VARIABLE_HTML_ESCAPE) { # uncoverable branch false count:2
    50          
114 40         154 $value = escape_html($value);
115             } elsif ($type == VARIABLE_RAW) {
116             # nothing to do
117             } else {
118 0         0 die "Unknown variable type: $type"; # uncoverable statement
119             }
120              
121 60 100       357 if ($raw_text_syntax) {
122 54         133 $raw_text_syntax->[1] .= $value;
123             } else {
124 6         16 $raw_text_syntax = [SYNTAX_RAW_TEXT, $value];
125             }
126 60         189 next;
127             } elsif ($syntax->[0] == SYNTAX_BOX) {
128 107         286 my (undef, $type, $name) = @$syntax;
129 107 100       377 if ($type == BOX_SECTION) {
    100          
130 55         126 local our $_OPEN_DELIMITER = $_DEFAULT_OPEN_DELIMITER;
131 55         148 local our $_CLOSE_DELIMITER = $_DEFAULT_CLOSE_DELIMITER;
132 55         191 local our @_CTX = @CONTEXT_HINT;
133 55 100       192 local $Text::MustacheTemplate::Evaluator::LAMBDA_RENDERER = \&_render_template_in_context if $Text::MustacheTemplate::LAMBDA_TEMPLATE_RENDERING;
134 55 100       375 next unless $name eq '.' ? evaluate_section($_CTX[-1]) : evaluate_section_variable(\@_CTX, split /\./ano, $name);
    100          
135             } elsif ($type == BOX_INVERTED_SECTION) {
136 28         57 local our $_OPEN_DELIMITER = $_DEFAULT_OPEN_DELIMITER;
137 28         57 local our $_CLOSE_DELIMITER = $_DEFAULT_CLOSE_DELIMITER;
138 28         138 local our @_CTX = @CONTEXT_HINT;
139 28 100       82 local $Text::MustacheTemplate::Evaluator::LAMBDA_RENDERER = \&_render_template_in_context if $Text::MustacheTemplate::LAMBDA_TEMPLATE_RENDERING;
140 28 100       190 next if $name eq '.' ? evaluate_section($_CTX[-1]) : evaluate_section_variable(\@_CTX, split /\./ano, $name);
    100          
141             }
142             }
143             }
144              
145 720 100       2162 if ($raw_text_syntax) {
146 387         749 push @optimized_ast => $raw_text_syntax;
147 387         701 $raw_text_syntax = undef;
148             }
149 720 100       1689 if ($syntax->[0] == SYNTAX_BOX) {
150 338         1071 my @children = _optimize($syntax->[-1], $depth+1);
151 338         878 $syntax = [@$syntax]; # shallow copy
152 338         707 $syntax->[-1] = \@children;
153             }
154 720         1457 push @optimized_ast => $syntax;
155             }
156             }
157 865 100       1790 if ($raw_text_syntax) {
158 639         1542 push @optimized_ast => $raw_text_syntax;
159             }
160 865         2955 return @optimized_ast;
161             }
162              
163             sub _generate_code {
164 430     430   964 my ($ast, $indent) = @_;
165              
166 430         851 my $initial_text = '';
167             # Optimize: remove first raw text and fill initial text if no contains new lines
168 430 100 100     2113 if ($ast->[0]->[0] == SYNTAX_RAW_TEXT && $ast->[0]->[1] !~ /[\r\n]/mano) {
169 244         407 my (undef, $text) = @{ shift @$ast };
  244         685  
170 244         565 $initial_text = $text;
171             }
172              
173 430         1937 my $initial_text_perl = B::perlstring($initial_text);
174 430         1262 my $default_open_delimiter_perl = B::perlstring($_DEFAULT_OPEN_DELIMITER);
175 430         976 my $default_close_delimiter_perl = B::perlstring($_DEFAULT_CLOSE_DELIMITER);
176              
177 430         859 my $code = '';
178 430         1201 $code .= (' ' x $indent)."sub {\n";
179 430         927 $code .= (' ' x $indent)." local \@_CTX = \@_;\n";
180 430         1025 $code .= (' ' x $indent)." local (\$_OPEN_DELIMITER, \$_CLOSE_DELIMITER) = ($default_open_delimiter_perl, $default_close_delimiter_perl);\n";
181 430 100       1263 $code .= (' ' x $indent)." local \$Text::MustacheTemplate::Evaluator::LAMBDA_RENDERER = \\&_render_template_in_context;\n" if $indent == 4;
182 430         812 $code .= "\n";
183 430         1008 $code .= (' ' x $indent)." my \$_result = $initial_text_perl;\n";
184 430         1195 $code .= _generate_body($ast, $indent+4, '$_result');
185 430         1116 $code .= (' ' x $indent)." return \$_result;\n";
186 430         845 $code .= (' ' x $indent)."};\n";
187 430         1865 return $code;
188             }
189              
190             sub _generate_body {
191 730     730   1542 my ($ast, $indent, $result) = @_;
192              
193 730         1173 my $code = '';
194 730         1986 for my $i (keys @$ast) {
195 1403         2702 my $syntax = $ast->[$i];
196 1403         2423 my ($type) = @$syntax;
197              
198             # uncoverable branch true count:6
199             # uncoverable branch false count:5..6
200 1403 100       3594 if ($type == SYNTAX_RAW_TEXT) {
    100          
    100          
    100          
    50          
    0          
201 648         1276 my (undef, $text) = @$syntax;
202 648 100       1496 next if $result eq DISCARD_RESULT;
203 637 100       1012 if ($i == $#{$ast} ? $text =~ /[\r\n](?!\z)/mano : $text =~ /[\r\n]/mano) {
  637 100       2691  
204 109         305 my $regex = '(\r\n?|\n)';
205 109 100       198 $regex .= '(?!\z)' if $i == $#{$ast};
  109         329  
206 109         563 $code .= (' ' x $indent).'$_tmp = '.B::perlstring($text).";\n";
207 109         342 $code .= (' ' x $indent)."\$_tmp =~ s/$regex/\${1}\${_PADDING}/mag if defined \$_PADDING;\n";
208 109         393 $code .= (' ' x $indent)."$result .= \$_tmp;\n";
209             } else {
210 528         2637 $code .= (' ' x $indent).$result.' .= '.B::perlstring($text).";\n";
211             }
212             } elsif ($type == SYNTAX_VARIABLE) {
213 303         826 $code .= _generate_variable($syntax, $indent, $result);
214             } elsif ($type == SYNTAX_DELIMITER) {
215 35         96 my (undef, $open_delimiter, $close_delimiter) = @$syntax;
216 35         112 ($_CURRENT_OPEN_DELIMITER, $_CURRENT_CLOSE_DELIMITER) = ($open_delimiter, $close_delimiter);
217             } elsif ($type == SYNTAX_BOX) {
218 338         1223 $code .= _generate_box($syntax, $indent, $result);
219             } elsif ($type == SYNTAX_PARTIAL) {
220 79         252 my (undef, $reference, $name, $padding) = @$syntax;
221 79 100       256 $padding = B::perlstring($padding) if $padding;
222              
223             # uncoverable branch false count:2
224 79 100       597 my $retriever = $reference == REFERENCE_DYNAMIC ? ($name eq '.' ? '$_CTX[-1]' : 'retrieve_variable(\@_CTX, '.(join ', ', map B::perlstring($_), split /\./ano, $name).')')
    50          
    100          
225             : $reference == REFERENCE_STATIC ? B::perlstring($name)
226             : die "Unknown reference: $reference";
227              
228 79         318 $code .= (' ' x $indent)."\$_name = $retriever;\n";
229 79         257 $code .= (' ' x $indent)."$result .= do {\n";
230 79 100       270 $code .= (' ' x $indent)." local \$_PADDING;\n" unless $padding;
231 79 100       252 $code .= (' ' x $indent)." local \$_PADDING = $padding;\n" if $padding;
232 79         222 $code .= (' ' x $indent)." \$Text::MustacheTemplate::REFERENCES{\$_name}->(\@_CTX);\n";
233 79         276 $code .= (' ' x $indent)."} if exists \$Text::MustacheTemplate::REFERENCES{\$_name};\n";
234             } elsif ($type == SYNTAX_COMMENT) {
235             # ignore
236             } else {
237 0         0 die "Unknown syntax: $type"; # uncoverable statement
238             }
239             }
240 730         2185 return $code;
241             }
242              
243             sub _generate_variable {
244 303     303   690 my ($syntax, $indent, $result) = @_;
245              
246 303         700 my (undef, $type, $name) = @$syntax;
247 303 100       767 if ($type == VARIABLE_HTML_ESCAPE) { # uncoverable branch false count:2
    50          
248 264 100       1714 my $retriever = $name eq '.' ? '$_CTX[-1]' : 'retrieve_variable(\@_CTX, '.(join ', ', map B::perlstring($_), split /\./ano, $name).')';
249 264         1499 return (' ' x $indent)."$result .= escape_html($retriever // '');\n";
250             } elsif ($type == VARIABLE_RAW) {
251 39 100       319 my $retriever = $name eq '.' ? '$_CTX[-1]' : 'retrieve_variable(\@_CTX, '.(join ', ', map B::perlstring($_), split /\./ano, $name).')';
252 39         219 return (' ' x $indent)."$result .= $retriever // '';\n";
253             } else {
254 0         0 die "Unknown variable: $type"; # uncoverable statement
255             }
256             }
257              
258             sub _generate_box {
259 338     338   672 my ($syntax, $indent, $result) = @_;
260              
261 338         695 my (undef, $type) = @$syntax;
262 338 100       866 if ($type == BOX_SECTION) { # uncoverable branch false count:4
    100          
    100          
    50          
263 135         400 my (undef, undef, $name, $inner_template, $children) = @$syntax;
264 135   100     533 my $no_lambda = @CONTEXT_HINT && !$Text::MustacheTemplate::LAMBDA_TEMPLATE_RENDERING;
265              
266 135 100       719 my $inner_code = _generate_body($children, $no_lambda ? $indent+4 : $indent+8, $result);
267 135 100       1068 my $evaluator = $name eq '.'
268             ? 'evaluate_section($_CTX[-1])'
269             : 'evaluate_section_variable(\@_CTX, '.(join ', ', map B::perlstring($_), split /\./ano, $name).')';
270 135 100       438 if ($no_lambda) {
271 51 100       153 $evaluator = 'evaluate_section($_CTX[-2]) 'if $name eq '.';
272 51         145 my $code = (' ' x $indent)."push \@_CTX => {};\n";
273 51         155 $code .= (' ' x $indent)."for my \$ctx ($evaluator) {\n";
274 51         128 $code .= (' ' x $indent)." \$_CTX[-1] = \$ctx;\n";
275 51         176 $code .= $inner_code;
276 51         152 $code .= (' ' x $indent)."}\n";
277 51         177 $code .= (' ' x $indent)."pop \@_CTX;\n";
278 51         289 return $code;
279             }
280              
281 84         381 my ($open_delimiter, $close_delimiter) = map B::perlstring($_), ($_CURRENT_OPEN_DELIMITER, $_CURRENT_CLOSE_DELIMITER);
282 84         386 $inner_template = B::perlstring($inner_template);
283 84         248 my $code = (' ' x $indent)."\@_section = $evaluator;\n";
284 84         192 $code .= (' ' x $indent)."if (\$Text::MustacheTemplate::LAMBDA_TEMPLATE_RENDERING && \@_section == 1 && ref \$_section[0] eq 'CODE') {\n";
285 84         196 $code .= (' ' x $indent)." my \$code = \$_section[0];\n";
286 84         250 $code .= (' ' x $indent)." \$_tmp = \$code->($inner_template);\n";
287 84         232 $code .= (' ' x $indent)." local (\$_OPEN_DELIMITER, \$_CLOSE_DELIMITER) = ($open_delimiter, $close_delimiter);\n";
288 84         249 $code .= (' ' x $indent)." $result .= _render_template_in_context(\$_tmp);\n";
289 84         176 $code .= (' ' x $indent)."} else {\n";
290 84         170 $code .= (' ' x $indent)." my \@section = \@_section;\n"; # copy to avoid rewrite same varialbe in recurse
291 84         171 $code .= (' ' x $indent)." push \@_CTX => {};\n";
292 84         160 $code .= (' ' x $indent)." for my \$ctx (\@section) {\n";
293 84         181 $code .= (' ' x $indent)." \$_CTX[-1] = \$ctx;\n";
294 84         235 $code .= $inner_code;
295 84         270 $code .= (' ' x $indent)." }\n";
296 84         210 $code .= (' ' x $indent)." pop \@_CTX;\n";
297 84         173 $code .= (' ' x $indent)."}\n";
298 84         699 return $code;
299             } elsif ($type == BOX_INVERTED_SECTION) {
300 55         162 my (undef, undef, $name, $children) = @$syntax;
301 55 100       487 my $evaluator = $name eq '.'
302             ? 'evaluate_section($_CTX[-1])'
303             : 'evaluate_section_variable(\@_CTX, '.(join ', ', map B::perlstring($_), split /\./ano, $name).')';
304 55         231 my $code = (' ' x $indent)."if (!$evaluator) {\n";
305 55         183 $code .= _generate_body($children, $indent+4, $result);
306 55         173 $code .= (' ' x $indent)."}\n";
307 55         238 return $code;
308             } elsif ($type == BOX_BLOCK) {
309 98         174 my (undef, undef, $name, $children) = @$syntax;
310 98         207 $name = B::perlstring($name);
311 98 100       182 unless ($_PARENT) {
312 60         137 my $code = (' ' x $indent)."if (exists \$_BLOCKS{$name}) {\n";
313 60         137 $code .= (' ' x $indent)." $result .= \$_BLOCKS{$name}->(\@_CTX);\n";
314 60         104 $code .= (' ' x $indent)."} else {\n";
315 60         162 $code .= _generate_body($children, $indent+4, $result);
316 60         127 $code .= (' ' x $indent)."}\n";
317 60         156 return $code;
318             }
319              
320 38         83 my ($open_delimiter, $close_delimiter) = ($_CURRENT_OPEN_DELIMITER, $_CURRENT_CLOSE_DELIMITER);
321 38         102 my $sub_code = _generate_code($children, $indent+4);
322 38         112 $sub_code = substr $sub_code, $indent+4; # remove first indent
323 38         124 my $code = (' ' x $indent)."unless (exists \$_BLOCKS{$name}) {\n";
324 38         115 $code .= (' ' x $indent)." \$_BLOCKS{$name} = $sub_code";
325 38         66 $code .= (' ' x $indent)."}\n";
326 38         122 return $code;
327             } elsif ($type == BOX_PARENT) {
328 50         76 local $_PARENT = $syntax;
329 50         133 my (undef, undef, $reference, $name, $children) = @$syntax;
330              
331             # uncoverable branch false count:2
332 50 100       207 my $retriever = $reference == REFERENCE_DYNAMIC ? ($name eq '.' ? '$_CTX[-1]' : 'retrieve_variable(\@_CTX, '.(join ', ', map B::perlstring($_), split /\./ano, $name).')')
    50          
    100          
333             : $reference == REFERENCE_STATIC ? B::perlstring($name)
334             : die "Unknown reference: $type";
335              
336 50         105 my $code = (' ' x $indent)."{\n";
337 50         121 $code .= (' ' x $indent)." \$_name = $retriever;\n";
338 50         102 $code .= (' ' x $indent)." my \$_parent = \$Text::MustacheTemplate::REFERENCES{\$_name} or croak \"Unknown parent template: \$_name\";\n";
339 50         96 $code .= (' ' x $indent)." local \%_BLOCKS = \%_BLOCKS;\n";
340 50         110 $code .= _generate_body($children, $indent+4, DISCARD_RESULT);
341 50         135 $code .= (' ' x $indent)." $result .= do {\n";
342 50         79 $code .= (' ' x $indent)." local \$_PADDING;\n";
343 50         73 $code .= (' ' x $indent)." \$_parent->(\@_CTX);\n";
344 50         82 $code .= (' ' x $indent)." };\n";
345 50         88 $code .= (' ' x $indent)."}\n";
346 50         142 return $code;
347             } else {
348 0         0 die "Unknown box: $type"; # uncoverable statement
349             }
350             }
351              
352             sub _render_template_in_context {
353 31     31   53 my $source = shift;
354 31         36 our ($_OPEN_DELIMITER, $_CLOSE_DELIMITER);
355 31 100       420 if ($source !~ /(?:\Q$_OPEN_DELIMITER\E|\Q$_CLOSE_DELIMITER\E)/man) {
356 18         240 return $source;
357             }
358              
359 13         34 local $_PADDING;
360 13         23 local $Text::MustacheTemplate::Lexer::OPEN_DELIMITER = $_OPEN_DELIMITER;
361 13         25 local $Text::MustacheTemplate::Lexer::CLOSE_DELIMITER = $_CLOSE_DELIMITER;
362 13         56 my @tokens = Text::MustacheTemplate::Lexer->tokenize($source);
363              
364 13         29 local $Text::MustacheTemplate::Parser::SOURCE = $source;
365 13         45 my $ast = Text::MustacheTemplate::Parser->parse(@tokens);
366              
367 13         36 local @CONTEXT_HINT = our @_CTX;
368 13         50 my $template = __PACKAGE__->compile($ast);
369 13         39 return $template->(@_CTX);
370             }
371              
372             1;
373             __END__