File Coverage

blib/lib/Text/Xslate/Syntax/Handlebars.pm
Criterion Covered Total %
statement 290 309 93.8
branch 88 106 83.0
condition 31 39 79.4
subroutine 37 37 100.0
pod 0 23 0.0
total 446 514 86.7


line stmt bran cond sub pod time code
1             package Text::Xslate::Syntax::Handlebars;
2             BEGIN {
3 12     12   22454 $Text::Xslate::Syntax::Handlebars::AUTHORITY = 'cpan:DOY';
4             }
5             $Text::Xslate::Syntax::Handlebars::VERSION = '0.04';
6 12     12   121 use Mouse;
  12         32  
  12         549  
7              
8 12     12   5784 use Carp 'confess';
  12         28  
  12         2020  
9 12     12   86 use Text::Xslate::Util qw($DEBUG $NUMBER neat p);
  12         27  
  12         6828  
10              
11 12     12   20805 use Text::Handlebars::Symbol;
  12         34  
  12         1263  
12              
13             extends 'Text::Xslate::Parser';
14              
15 12     12   96 use constant _DUMP_PROTO => scalar($DEBUG =~ /\b dump=proto \b/xmsi);
  12         25  
  12         114549  
16              
17             my $nl = qr/\x0d?\x0a/;
18              
19             my $bracket_string = qr/\[ [^\]]* \]/xms;
20             my $STRING = qr/(?: $Text::Xslate::Util::STRING | $bracket_string )/xms;
21              
22             my $single_char = '[.#^/>&;=]';
23             my $OPERATOR_TOKEN = sprintf(
24             "(?:%s|$single_char)",
25             join('|', map{ quotemeta } qw(..))
26             );
27              
28 80     80   28856 sub _build_identity_pattern { qr/\@?[A-Za-z_][A-Za-z0-9_?-]*/ }
29 80     80   891 sub _build_comment_pattern { qr/\![^;]*/ }
30              
31 80     80   3050 sub _build_line_start { undef }
32 80     80   833 sub _build_tag_start { '{{' }
33 80     80   721 sub _build_tag_end { '}}' }
34              
35 80     80   1361 sub _build_shortcut_table { +{} }
36              
37 4569     4569 0 170098 sub symbol_class { 'Text::Handlebars::Symbol' }
38              
39             sub split_tags {
40 142     142 0 260 my $self = shift;
41 142         348 my ($input) = @_;
42              
43 142         552 my $tag_start = $self->tag_start;
44 142         625 my $tag_end = $self->tag_end;
45              
46 142         489 my $lex_comment = $self->comment_pattern;
47 142         3173 my $lex_code = qr/(?: $lex_comment | (?: $STRING | [^\['"] ) )/xms;
48              
49 142         307 my @chunks;
50              
51             my @raw_text;
52 0         0 my @delimiters;
53              
54 0         0 my $close_tag;
55 142         424 my $standalone = 1;
56 142         1037 while ($input) {
57 1126 100       10394 if ($close_tag) {
    100          
    50          
58 353         532 my $start = 0;
59 353         384 my $pos;
60 353         1195 while(($pos = index $input, $close_tag, $start) >= 0) {
61 353         726 my $code = substr $input, 0, $pos;
62 353         3763 $code =~ s/$lex_code//g;
63 353 50       1135 if(length($code) == 0) {
64 353         565 last;
65             }
66 0         0 $start = $pos + 1;
67             }
68              
69 353 50       675 if ($pos >= 0) {
70 353         855 my $code = substr $input, 0, $pos, '';
71 353 50       2171 $input =~ s/\A\Q$close_tag//
72             or die "Oops!";
73              
74             # XXX this is ugly, but i don't know how to get the parsing
75             # right otherwise if we also need to support ^foo
76 353 100       923 $code = 'else' if $code eq '^';
77              
78 353         446 my @extra;
79              
80 353   100     1688 my $autochomp = $code =~ m{^[!#^/=>]} || $code eq 'else';
81              
82 353 100       1341 if ($code =~ s/^=\s*([^\s]+)\s+([^\s]+)\s*=$//) {
    50          
83 2         75 ($tag_start, $tag_end) = ($1, $2);
84             }
85             elsif ($code =~ /^=/) {
86 0         0 die "Invalid delimiter tag: $code";
87             }
88              
89 353 100 100     4335 if ($autochomp && $standalone) {
90 120 100       568 if ($input =~ /\A\s*(?:\n|\z)/) {
91 92         1144 $input =~ s/\A$nl//;
92 92 100 100     788 if (@chunks > 0 && $chunks[-1][0] eq 'text' && $code !~ m{^>}) {
      66        
93 71         527 $chunks[-1][1] =~ s/^(?:(?!\n)\s)*\z//m;
94 71 100       205 if (@raw_text) {
95 48         275 $raw_text[-1] =~ s/^(?:(?!\n)\s)*\z//m;
96             }
97             }
98             }
99             }
100             else {
101 233         337 $standalone = 0;
102             }
103              
104 353 100 100     1870 if ($code =~ m{^/} || $code eq 'else') {
105 82         166 push @extra, pop @raw_text;
106 82         140 push @extra, pop @delimiters;
107 82 100       213 if (@raw_text) {
108 9         23 $raw_text[-1] .= $extra[0];
109             }
110             }
111 353 100       1078 if (@raw_text) {
112 95         242 $raw_text[-1] .= $tag_start . $code . $tag_end;
113             }
114 353 100 100     1907 if ($code =~ m{^[#^]} || $code eq 'else') {
115 82         172 push @raw_text, '';
116 82         266 push @delimiters, [$tag_start, $tag_end];
117             }
118              
119 353 100       876 if (length($code)) {
120 351 100       1671 push @chunks, [
121             ($close_tag eq '}}}' ? 'raw_code' : 'code'),
122             $code,
123             @extra,
124             ];
125             }
126              
127 353         1317 undef $close_tag;
128             }
129             else {
130 0         0 last; # the end tag is not found
131             }
132             }
133             elsif ($input =~ s/\A\Q$tag_start//) {
134 353 100 100     2303 if ($tag_start eq '{{' && $input =~ s/\A\{//) {
135 15         43 $close_tag = '}}}';
136             }
137             else {
138 338         1081 $close_tag = $tag_end;
139             }
140             }
141             elsif ($input =~ s/\A([^\n]*?(?:\n|(?=\Q$tag_start\E)|\z))//) {
142 420         1049 my $text = $1;
143 420 50       962 if (length($text)) {
144 420         1417 push @chunks, [ text => $text ];
145              
146 420 100       829 if ($standalone) {
147 243         1453 $standalone = $text =~ /(?:^|\n)\s*$/;
148             }
149             else {
150 177         994 $standalone = $text =~ /\n\s*$/;
151             }
152              
153 420 100       1753 if (@raw_text) {
154 147         530 $raw_text[-1] .= $text;
155             }
156             }
157             }
158             else {
159 0         0 confess "Oops: unreached code, near " . p($input);
160             }
161             }
162              
163 142 50       426 if ($close_tag) {
164             # calculate line number
165 0         0 my $orig_src = $_[0];
166 0         0 substr $orig_src, -length($input), length($input), '';
167 0         0 my $line = ($orig_src =~ tr/\n/\n/);
168 0         0 $self->_error("Malformed templates detected",
169             neat((split /\n/, $input)[0]), ++$line,
170             );
171             }
172              
173 142         1111 return @chunks;
174             }
175              
176             sub preprocess {
177 142     142 0 31405 my $self = shift;
178 142         273 my ($input) = @_;
179              
180 142         515 my @chunks = $self->split_tags($input);
181              
182 142         292 my $code = '';
183 142         317 for my $chunk (@chunks) {
184 771         1468 my ($type, $content, $raw_text, $delimiters) = @$chunk;
185 771 100       1908 if ($type eq 'text') {
    100          
    50          
186 420         1154 $content =~ s/(["\\])/\\$1/g;
187 420 100       1901 $code .= qq{print_raw "$content";\n}
188             if length($content);
189             }
190             elsif ($type eq 'code') {
191 336         552 my $extra = '';
192 336 100       4664 if ($content =~ s{^/}{}) {
    100          
193 72         266 $chunk->[2] =~ s/(["\\])/\\$1/g;
194 72         230 $chunk->[3][0] =~ s/(["\\])/\\$1/g;
195 72         170 $chunk->[3][1] =~ s/(["\\])/\\$1/g;
196              
197 72         289 $extra = '"'
198 72         359 . join('" "', $chunk->[2], @{ $chunk->[3] })
199             . '"';
200 72         472 $code .= qq{/$extra $content;\n};
201             }
202             elsif ($content eq 'else') {
203             # XXX fix duplication
204 10         50 $chunk->[2] =~ s/(["\\])/\\$1/g;
205 10         30 $chunk->[3][0] =~ s/(["\\])/\\$1/g;
206 10         28 $chunk->[3][1] =~ s/(["\\])/\\$1/g;
207              
208 10         42 $extra = '"'
209 10         21 . join('" "', $chunk->[2], @{ $chunk->[3] })
210             . '"';
211 10         46 $code .= qq{$content $extra;\n};
212             }
213             else {
214 254         762 $code .= qq{$content;\n};
215             }
216             }
217             elsif ($type eq 'raw_code') {
218 15         46 $code .= qq{&$content;\n};
219             }
220             else {
221 0         0 $self->_error("Oops: Unknown token: $content ($type)");
222             }
223             }
224              
225 142         244 print STDOUT $code, "\n" if _DUMP_PROTO;
226 142         1563 return $code;
227             }
228              
229             # XXX advance has some syntax special cases in it, probably need to override
230             # it too eventually
231              
232             sub init_symbols {
233 80     80 0 2102 my $self = shift;
234              
235 80         278 for my $type (qw(name key literal)) {
236 240         962 my $symbol = $self->symbol("($type)");
237 240         13001 $symbol->arity($type);
238 240         6106 $symbol->set_nud($self->can("nud_$type"));
239 240         1946 $symbol->lbp(10);
240             }
241              
242 80         249 for my $this (qw(. this)) {
243 160         1239 my $symbol = $self->symbol($this);
244 160         3020 $symbol->arity('key');
245 160         510 $symbol->id('.');
246 160         423 $symbol->lbp(10);
247 160         1502 $symbol->set_nud($self->can('nud_key'));
248             }
249              
250 80         2097 for my $field_access (qw(. /)) {
251 160         2498 $self->infix($field_access, 256, $self->can('led_dot'));
252             }
253              
254 80         1717 for my $block ('#', '^') {
255 160         2090 $self->symbol($block)->set_std($self->can('std_block'));
256             }
257              
258 80         1641 for my $else (qw(/ else)) {
259 160         1289 $self->symbol($else)->is_block_end(1);
260             }
261              
262 80         2154 $self->symbol('>')->set_std($self->can('std_partial'));
263              
264 80         1689 $self->symbol('&')->set_nud($self->can('nud_mark_raw'));
265 80         2879 $self->symbol('..')->set_nud($self->can('nud_uplevel'));
266 80         1943 $self->symbol('..')->lbp(10);
267              
268 80         1034 $self->infix('=', 20, $self->can('led_equals'));
269             }
270              
271             # copied from Text::Xslate::Parser, but using different definitions of
272             # $STRING and $OPERATOR_TOKEN
273             sub tokenize {
274 2565     2565 0 3646 my($parser) = @_;
275              
276 2565         26521 local *_ = \$parser->{input};
277              
278 2565         5949 my $comment_rx = $parser->comment_pattern;
279 2565         5481 my $id_rx = $parser->identity_pattern;
280 2565         2946 my $count = 0;
281 2567         15250 TRY: {
282 2565         4641 /\G (\s*) /xmsgc;
283 2567         6635 $count += ( $1 =~ tr/\n/\n/);
284 2567         6548 $parser->following_newline( $count );
285              
286 2567 100       51485 if(/\G $comment_rx /xmsgc) {
    100          
    100          
    100          
    50          
287 2         7 redo TRY; # retry
288             }
289             elsif(/\G ($id_rx)/xmsgc){
290 795         5716 return [ name => $1 ];
291             }
292             elsif(/\G ($NUMBER | $STRING)/xmsogc){
293 643         5348 return [ literal => $1 ];
294             }
295             elsif(/\G ($OPERATOR_TOKEN)/xmsogc){
296 985         7941 return [ operator => $1 ];
297             }
298             elsif(/\G (\S+)/xmsgc) {
299 0         0 Carp::confess("Oops: Unexpected token '$1'");
300             }
301             else { # empty
302 142         977 return [ special => '(end)' ];
303             }
304             }
305             }
306              
307             sub nud_name {
308 100     100 0 1129 my $self = shift;
309 100         148 my ($symbol) = @_;
310              
311 100         438 my $name = $self->SUPER::nud_name($symbol);
312              
313 100         2992 my $call = $self->call($name);
314              
315 100 100       444 if ($self->token->is_defined) {
316 3         5 push @{ $call->second }, $self->expression(0);
  3         20  
317             }
318              
319 100         238 return $call;
320             }
321              
322             sub nud_key {
323 282     282 0 1961 my $self = shift;
324 282         381 my ($symbol) = @_;
325              
326 282         1350 return $symbol->clone(arity => 'key');
327             }
328              
329             sub led_dot {
330 48     48 0 452 my $self = shift;
331 48         78 my ($symbol, $left) = @_;
332              
333             # XXX hack to make {{{.}}} work, but in general this syntax is ambiguous
334             # and i'm not going to deal with it
335 48 100 66     262 if ($left->arity eq 'call' && $left->first->id eq 'mark_raw') {
336 1         3 push @{ $left->second }, $symbol->nud($self);
  1         6  
337 1         33 return $left;
338             }
339              
340 47         219 my $dot = $self->make_field_lookup($left, $self->token, $symbol);
341              
342 47         3836 $self->advance;
343              
344 47         407 return $dot;
345             }
346              
347             sub std_block {
348 72     72 0 541 my $self = shift;
349 72         120 my ($symbol) = @_;
350              
351 72         257 my $inverted = $symbol->id eq '^';
352              
353 72         227 my $name = $self->expression(0);
354              
355 72 50 66     652 if ($name->arity ne 'key' && $name->arity ne 'key_field' && $name->arity ne 'call') {
      66        
356 0         0 $self->_unexpected("opening block name", $self->token);
357             }
358 72         249 my $name_string = $self->_field_to_string($name);
359              
360 72         263 $self->advance(';');
361              
362 72         108 my %block;
363 72         128 my $context = 'if';
364 72         282 $block{$context}{body} = $self->statements;
365              
366 72 100       433 if ($self->token->id eq 'else') {
367 10         38 $self->advance;
368              
369 10         44 $block{$context}{raw_text} = $self->token;
370 10         35 $self->advance;
371 10         48 $block{$context}{open_tag} = $self->token;
372 10         34 $self->advance;
373 10         42 $block{$context}{close_tag} = $self->token;
374 10         31 $self->advance;
375              
376 10         17 $context = 'else';
377 10         37 $block{$context}{body} = $self->statements;
378             }
379              
380 72         245 $self->advance('/');
381              
382 72         306 $block{$context}{raw_text} = $self->token;
383 72         260 $self->advance;
384 72         336 $block{$context}{open_tag} = $self->token;
385 72         232 $self->advance;
386 72         336 $block{$context}{close_tag} = $self->token;
387 72         347 $self->advance;
388              
389 72 100       247 if ($inverted) {
390 1         5 ($block{if}, $block{else}) = ($block{else}, $block{if});
391 1 50       7 if (!$block{if}) {
392 1         5 $block{if}{body} = $self->literal('');
393 1         39 $block{if}{raw_text} = $self->literal('');
394 1         37 $block{if}{open_tag} = $block{else}{open_tag};
395 1         4 $block{if}{close_tag} = $block{else}{close_tag};
396             }
397             }
398              
399 72         425 my $closing_name = $self->expression(0);
400              
401 72 50 66     611 if ($closing_name->arity ne 'key' && $closing_name->arity ne 'key_field' && $closing_name->arity ne 'call') {
      66        
402 0         0 $self->_unexpected("closing block name", $self->token);
403             }
404 72         203 my $closing_name_string = $self->_field_to_string($closing_name);
405              
406 72 50       225 if ($name_string ne $closing_name_string) {
407 0         0 $self->_unexpected('/' . $name_string, $self->token);
408             }
409              
410 72         305 $self->advance(';');
411              
412 72         415 return $self->print_raw(
413             $name->clone(
414             arity => 'block',
415             first => $name,
416             second => \%block,
417             ),
418             );
419             }
420              
421             sub nud_mark_raw {
422 16     16 0 104 my $self = shift;
423 16         28 my ($symbol) = @_;
424              
425 16         71 return $self->symbol('mark_raw')->clone(
426             line => $symbol->line,
427             )->nud($self);
428             }
429              
430             sub nud_uplevel {
431 17     17 0 114 my $self = shift;
432 17         31 my ($symbol) = @_;
433              
434 17         84 return $symbol->clone(arity => 'variable');
435             }
436              
437             sub std_partial {
438 3     3 0 20 my $self = shift;
439 3         5 my ($symbol) = @_;
440              
441 3         16 my $partial = $self->token->clone(arity => 'literal');
442 3         73 $self->advance;
443 3         5 my $args;
444 3 100       25 if ($self->token->id ne ';') {
445 1         3 $args = $self->expression(0);
446             }
447 3         12 $self->advance(';');
448              
449 3 50       23 return $symbol->clone(
450             arity => 'partial',
451             first => ($partial->id =~ /\./ ? $partial : [ $partial ]),
452             second => $args,
453             );
454             }
455              
456             sub led_equals {
457 4     4 0 29 my $self = shift;
458 4         8 my ($symbol, $left) = @_;
459              
460 4         26 my $right = $self->expression($symbol->lbp);
461              
462 4         15 return $symbol->clone(
463             arity => 'pair',
464             first => $left->clone(arity => 'literal'),
465             second => $right,
466             );
467             }
468              
469             sub undefined_name {
470 278     278 0 842 my $self = shift;
471 278         438 my ($name) = @_;
472              
473 278         1439 return $self->symbol('(key)')->clone(id => $name);
474             }
475              
476             sub define_function {
477 240     240 0 4964 my $self = shift;
478 240         991 my (@names) = @_;
479              
480 240         1292 $self->SUPER::define_function(@_);
481 240         9454 for my $name (@names) {
482 1631         4673 my $symbol = $self->symbol($name);
483 1631         31019 $symbol->set_nud($self->can('nud_name'));
484 1631         20911 $symbol->lbp(10);
485             }
486              
487 240         772 return;
488             }
489              
490             sub define_helper {
491 160     160 0 329 my $self = shift;
492 160         360 my (@names) = @_;
493              
494 160         408 $self->define_function(@names);
495 160         280 for my $name (@names) {
496 339         1499 my $symbol = $self->symbol($name);
497 339         3870 $symbol->is_helper(1);
498             }
499              
500 160         650 return;
501             }
502              
503             sub parse_literal {
504 643     643 0 892 my $self = shift;
505 643         2117 my ($literal) = @_;
506              
507 643 100       1812 if ($literal =~ /\A\[(.*)\]\z/ms) {
508 4         15 $literal = $1;
509 4         126 $literal =~ s/(["\\])/\\$1/g;
510 4         59 $literal = '"' . $literal . '"';
511             }
512              
513 643         2461 return $self->SUPER::parse_literal($literal);
514             }
515              
516             sub is_valid_field {
517 47     47 0 72 my $self = shift;
518 47         69 my ($field) = @_;
519              
520             # allow foo.[10]
521 47 100       178 return 1 if $field->arity eq 'literal';
522             # undefined symbols are all treated as keys - see undefined_name
523 44 100       727 return 1 if $field->arity eq 'key';
524             # allow ../../foo
525 11 50       63 return 1 if $field->id eq '..';
526              
527 0         0 return;
528             }
529              
530             sub expression {
531 791     791 0 8380 my $self = shift;
532 791         1128 my ($rbp) = @_;
533              
534 791         1737 my $token = $self->token;
535 791         1981 $self->advance;
536 791         3023 my $left = $token->nud($self);
537              
538 791         32487 while ($rbp < $self->token->lbp) {
539 111         838 $token = $self->token;
540 111 100       523 if ($token->has_led) {
541 52         390 $self->advance;
542 52         1069 $left = $token->led($self, $left);
543             }
544             else {
545 59 50       257 if ($left->arity ne 'call') {
546 0         0 $self->_error("Unexpected " . $token->arity, $left);
547             }
548 59         100 push @{ $left->second }, $self->expression($token->lbp);
  59         509  
549             }
550             }
551              
552 791         3443 return $left;
553             }
554              
555             sub call {
556 915     915 0 2769 my $self = shift;
557              
558 915         4025 my $call = $self->SUPER::call(@_);
559 915         42530 $call->is_helper($call->first->is_helper);
560 915         3269 return $call;
561             }
562              
563             sub make_field_lookup {
564 47     47 0 70 my $self = shift;
565 47         70 my ($var, $field, $dot) = @_;
566              
567 47 50       136 if (!$self->is_valid_field($field)) {
568 0         0 $self->_unexpected("a field name", $field);
569             }
570              
571 47   33     1419 $dot ||= $self->symbol('.');
572              
573 47         671 return $dot->clone(
574             arity => 'key_field',
575             first => $var,
576             second => $field->clone(arity => 'literal'),
577             );
578             }
579              
580             sub print_raw {
581 72     72 0 2189 my $self = shift;
582 72         359 return $self->print(@_)->clone(id => 'print_raw');
583             }
584              
585             sub literal {
586 383     383 0 567 my $self = shift;
587 383         615 my ($value) = @_;
588 383         1258 return $self->symbol('(literal)')->clone(id => $value);
589             }
590              
591             sub _field_to_string {
592 144     144   450 my $self = shift;
593 144         213 my ($symbol) = @_;
594              
595             # name and key can just be returned
596 144 50       997 return $symbol->id
597             unless $symbol->arity eq 'field';
598              
599             # field accesses should recurse on the first and append the second
600 0           return $self->_field_to_string($symbol->first) . '.' . $symbol->second->id;
601             }
602              
603             __PACKAGE__->meta->make_immutable;
604 12     12   169 no Mouse;
  12         25  
  12         141  
605              
606             =for Pod::Coverage
607             call
608             define_function
609             define_helper
610             expression
611             init_symbols
612             is_valid_field
613             led_dot
614             led_equals
615             literal
616             make_field_lookup
617             nud_key
618             nud_mark_raw
619             nud_name
620             nud_uplevel
621             parse_literal
622             preprocess
623             print_raw
624             split_tags
625             std_block
626             std_partial
627             symbol_class
628             tokenize
629             undefined_name
630              
631             =cut
632              
633             1;