File Coverage

blib/lib/Text/Xslate/Syntax/Handlebars.pm
Criterion Covered Total %
statement 291 310 93.8
branch 90 108 83.3
condition 31 39 79.4
subroutine 36 36 100.0
pod 0 23 0.0
total 448 516 86.8


line stmt bran cond sub pod time code
1             package Text::Xslate::Syntax::Handlebars;
2             our $AUTHORITY = 'cpan:DOY';
3             $Text::Xslate::Syntax::Handlebars::VERSION = '0.05';
4 12     12   11778 use Mouse;
  12         27  
  12         73  
5              
6 12     12   3650 use Carp 'confess';
  12         24  
  12         621  
7 12     12   66 use Text::Xslate::Util qw($DEBUG $NUMBER neat p);
  12         19  
  12         1490  
8              
9 12     12   6758 use Text::Handlebars::Symbol;
  12         28  
  12         732  
10              
11             extends 'Text::Xslate::Parser';
12              
13 12     12   69 use constant _DUMP_PROTO => scalar($DEBUG =~ /\b dump=proto \b/xmsi);
  12         22  
  12         50115  
14              
15             my $nl = qr/\x0d?\x0a/;
16              
17             my $bracket_string = qr/\[ [^\]]* \]/xms;
18             my $STRING = qr/(?: $Text::Xslate::Util::STRING | $bracket_string )/xms;
19              
20             my $single_char = '[.#^/>&;=]';
21             my $OPERATOR_TOKEN = sprintf(
22             "(?:%s|$single_char)",
23             join('|', map{ quotemeta } qw(..))
24             );
25              
26 81     81   12756 sub _build_identity_pattern { qr/\@?[A-Za-z_][A-Za-z0-9_?-]*/ }
27 81     81   588 sub _build_comment_pattern { qr/\![^;]*/ }
28              
29 81     81   1468 sub _build_line_start { undef }
30 81     81   513 sub _build_tag_start { '{{' }
31 81     81   564 sub _build_tag_end { '}}' }
32              
33 81     81   663 sub _build_shortcut_table { +{} }
34              
35 4623     4623 0 104204 sub symbol_class { 'Text::Handlebars::Symbol' }
36              
37             sub split_tags {
38 145     145 0 229 my $self = shift;
39 145         232 my ($input) = @_;
40              
41 145         462 my $tag_start = $self->tag_start;
42 145         369 my $tag_end = $self->tag_end;
43              
44 145         352 my $lex_comment = $self->comment_pattern;
45 145         1852 my $lex_code = qr/(?: $lex_comment | (?: $STRING | [^\['"] ) )/xms;
46              
47 145         257 my @chunks;
48              
49             my @raw_text;
50 0         0 my @delimiters;
51              
52 0         0 my $close_tag;
53 145         329 my $standalone = 1;
54 145         709 while ($input) {
55 1142 100       6629 if ($close_tag) {
    100          
    50          
56 359         480 my $start = 0;
57 359         416 my $pos;
58 359         1026 while(($pos = index $input, $close_tag, $start) >= 0) {
59 359         670 my $code = substr $input, 0, $pos;
60 359         2863 $code =~ s/$lex_code//g;
61 359 50       969 if(length($code) == 0) {
62 359         549 last;
63             }
64 0         0 $start = $pos + 1;
65             }
66              
67 359 50       696 if ($pos >= 0) {
68 359         743 my $code = substr $input, 0, $pos, '';
69 359 50       1888 $input =~ s/\A\Q$close_tag//
70             or die "Oops!";
71              
72             # XXX this is ugly, but i don't know how to get the parsing
73             # right otherwise if we also need to support ^foo
74 359 100       811 $code = 'else' if $code eq '^';
75              
76 359         434 my @extra;
77              
78 359   100     1454 my $autochomp = $code =~ m{^[!#^/=>]} || $code eq 'else';
79              
80 359 100       1157 if ($code =~ s/^=\s*([^\s]+)\s+([^\s]+)\s*=$//) {
    50          
81 2         6 ($tag_start, $tag_end) = ($1, $2);
82             }
83             elsif ($code =~ /^=/) {
84 0         0 die "Invalid delimiter tag: $code";
85             }
86              
87 359 100 100     1152 if ($autochomp && $standalone) {
88 122 100       480 if ($input =~ /\A\s*(?:\n|\z)/) {
89 94         511 $input =~ s/\A$nl//;
90 94 100 100     691 if (@chunks > 0 && $chunks[-1][0] eq 'text' && $code !~ m{^>}) {
      66        
91 73         291 $chunks[-1][1] =~ s/^(?:(?!\n)\s)*\z//m;
92 73 100       198 if (@raw_text) {
93 49         237 $raw_text[-1] =~ s/^(?:(?!\n)\s)*\z//m;
94             }
95             }
96             }
97             }
98             else {
99 237         334 $standalone = 0;
100             }
101              
102 359 100 100     1577 if ($code =~ m{^/} || $code eq 'else') {
103 83         159 push @extra, pop @raw_text;
104 83         141 push @extra, pop @delimiters;
105 83 100       218 if (@raw_text) {
106 9         22 $raw_text[-1] .= $extra[0];
107             }
108             }
109 359 100       869 if (@raw_text) {
110 96 100       209 if ($close_tag eq '}}}') {
111 8         20 $raw_text[-1] .= '{{{' . $code . '}}}';
112             }
113             else {
114 88         220 $raw_text[-1] .= $tag_start . $code . $tag_end;
115             }
116             }
117 359 100 100     1647 if ($code =~ m{^[#^]} || $code eq 'else') {
118 83         143 push @raw_text, '';
119 83         211 push @delimiters, [$tag_start, $tag_end];
120             }
121              
122 359 100       816 if (length($code)) {
123 357 100       1148 push @chunks, [
124             ($close_tag eq '}}}' ? 'raw_code' : 'code'),
125             $code,
126             @extra,
127             ];
128             }
129              
130 359         1241 undef $close_tag;
131             }
132             else {
133 0         0 last; # the end tag is not found
134             }
135             }
136             elsif ($input =~ s/\A\Q$tag_start//) {
137 359 100 100     1780 if ($tag_start eq '{{' && $input =~ s/\A\{//) {
138 25         76 $close_tag = '}}}';
139             }
140             else {
141 334         1106 $close_tag = $tag_end;
142             }
143             }
144             elsif ($input =~ s/\A([^\n]*?(?:\n|(?=\Q$tag_start\E)|\z))//) {
145 424         892 my $text = $1;
146 424 50       1466 if (length($text)) {
147 424         1053 push @chunks, [ text => $text ];
148              
149 424 100       887 if ($standalone) {
150 243         1130 $standalone = $text =~ /(?:^|\n)\s*$/;
151             }
152             else {
153 181         519 $standalone = $text =~ /\n\s*$/;
154             }
155              
156 424 100       1515 if (@raw_text) {
157 148         528 $raw_text[-1] .= $text;
158             }
159             }
160             }
161             else {
162 0         0 confess "Oops: unreached code, near " . p($input);
163             }
164             }
165              
166 145 50       319 if ($close_tag) {
167             # calculate line number
168 0         0 my $orig_src = $_[0];
169 0         0 substr $orig_src, -length($input), length($input), '';
170 0         0 my $line = ($orig_src =~ tr/\n/\n/);
171 0         0 $self->_error("Malformed templates detected",
172             neat((split /\n/, $input)[0]), ++$line,
173             );
174             }
175              
176 145         738 return @chunks;
177             }
178              
179             sub preprocess {
180 145     145 0 17988 my $self = shift;
181 145         255 my ($input) = @_;
182              
183 145         360 my @chunks = $self->split_tags($input);
184              
185 145         258 my $code = '';
186 145         283 for my $chunk (@chunks) {
187 781         1690 my ($type, $content, $raw_text, $delimiters) = @$chunk;
188 781 100       1890 if ($type eq 'text') {
    100          
    50          
189 424         1039 $content =~ s/(["\\])/\\$1/g;
190 424 100       1570 $code .= qq{print_raw "$content";\n}
191             if length($content);
192             }
193             elsif ($type eq 'code') {
194 332         432 my $extra = '';
195 332 100       998 if ($content =~ s{^/}{}) {
    100          
196 73         232 $chunk->[2] =~ s/(["\\])/\\$1/g;
197 73         142 $chunk->[3][0] =~ s/(["\\])/\\$1/g;
198 73         148 $chunk->[3][1] =~ s/(["\\])/\\$1/g;
199              
200             $extra = '"'
201 73         125 . join('" "', $chunk->[2], @{ $chunk->[3] })
  73         247  
202             . '"';
203 73         279 $code .= qq{/$extra $content;\n};
204             }
205             elsif ($content eq 'else') {
206             # XXX fix duplication
207 10         42 $chunk->[2] =~ s/(["\\])/\\$1/g;
208 10         24 $chunk->[3][0] =~ s/(["\\])/\\$1/g;
209 10         23 $chunk->[3][1] =~ s/(["\\])/\\$1/g;
210              
211             $extra = '"'
212 10         19 . join('" "', $chunk->[2], @{ $chunk->[3] })
  10         40  
213             . '"';
214 10         35 $code .= qq{$content $extra;\n};
215             }
216             else {
217 249         635 $code .= qq{$content;\n};
218             }
219             }
220             elsif ($type eq 'raw_code') {
221 25         67 $code .= qq{&$content;\n};
222             }
223             else {
224 0         0 $self->_error("Oops: Unknown token: $content ($type)");
225             }
226             }
227              
228 145         176 print STDOUT $code, "\n" if _DUMP_PROTO;
229 145         1086 return $code;
230             }
231              
232             # XXX advance has some syntax special cases in it, probably need to override
233             # it too eventually
234              
235             sub init_symbols {
236 81     81 0 1888 my $self = shift;
237              
238 81         189 for my $type (qw(name key literal)) {
239 243         855 my $symbol = $self->symbol("($type)");
240 243         2824 $symbol->arity($type);
241 243         1594 $symbol->set_nud($self->can("nud_$type"));
242 243         1695 $symbol->lbp(10);
243             }
244              
245 81         167 for my $this (qw(. this)) {
246 162         805 my $symbol = $self->symbol($this);
247 162         2482 $symbol->arity('key');
248 162         421 $symbol->id('.');
249 162         354 $symbol->lbp(10);
250 162         1050 $symbol->set_nud($self->can('nud_key'));
251             }
252              
253 81         573 for my $field_access (qw(. /)) {
254 162         1980 $self->infix($field_access, 256, $self->can('led_dot'));
255             }
256              
257 81         1500 for my $block ('#', '^') {
258 162         1624 $self->symbol($block)->set_std($self->can('std_block'));
259             }
260              
261 81         1462 for my $else (qw(/ else)) {
262 162         1062 $self->symbol($else)->is_block_end(1);
263             }
264              
265 81         1300 $self->symbol('>')->set_std($self->can('std_partial'));
266              
267 81         1332 $self->symbol('&')->set_nud($self->can('nud_mark_raw'));
268 81         1752 $self->symbol('..')->set_nud($self->can('nud_uplevel'));
269 81         1777 $self->symbol('..')->lbp(10);
270              
271 81         1067 $self->infix('=', 20, $self->can('led_equals'));
272             }
273              
274             # copied from Text::Xslate::Parser, but using different definitions of
275             # $STRING and $OPERATOR_TOKEN
276             sub tokenize {
277 2608     2608 0 3887 my($parser) = @_;
278              
279 2608         5263 local *_ = \$parser->{input};
280              
281 2608         6129 my $comment_rx = $parser->comment_pattern;
282 2608         5467 my $id_rx = $parser->identity_pattern;
283 2608         3408 my $count = 0;
284             TRY: {
285 2608         3168 /\G (\s*) /xmsgc;
  2610         6277  
286 2610         5222 $count += ( $1 =~ tr/\n/\n/);
287 2610         6234 $parser->following_newline( $count );
288              
289 2610 100       23576 if(/\G $comment_rx /xmsgc) {
    100          
    100          
    100          
    50          
290 2         8 redo TRY; # retry
291             }
292             elsif(/\G ($id_rx)/xmsgc){
293 806         5205 return [ name => $1 ];
294             }
295             elsif(/\G ($NUMBER | $STRING)/xmsogc){
296 650         4158 return [ literal => $1 ];
297             }
298             elsif(/\G ($OPERATOR_TOKEN)/xmsogc){
299 1007         6380 return [ operator => $1 ];
300             }
301             elsif(/\G (\S+)/xmsgc) {
302 0         0 Carp::confess("Oops: Unexpected token '$1'");
303             }
304             else { # empty
305 145         807 return [ special => '(end)' ];
306             }
307             }
308             }
309              
310             sub nud_name {
311 112     112 0 1396 my $self = shift;
312 112         163 my ($symbol) = @_;
313              
314 112         344 my $name = $self->SUPER::nud_name($symbol);
315              
316 112         2856 my $call = $self->call($name);
317              
318 112 100       497 if ($self->token->is_defined) {
319 3         3 push @{ $call->second }, $self->expression(0);
  3         23  
320             }
321              
322 112         319 return $call;
323             }
324              
325             sub nud_key {
326 287     287 0 1454 my $self = shift;
327 287         417 my ($symbol) = @_;
328              
329 287         758 return $symbol->clone(arity => 'key');
330             }
331              
332             sub led_dot {
333 48     48 0 283 my $self = shift;
334 48         87 my ($symbol, $left) = @_;
335              
336             # XXX hack to make {{{.}}} work, but in general this syntax is ambiguous
337             # and i'm not going to deal with it
338 48 100 66     203 if ($left->arity eq 'call' && $left->first->id eq 'mark_raw') {
339 1         2 push @{ $left->second }, $symbol->nud($self);
  1         6  
340 1         27 return $left;
341             }
342              
343 47         170 my $dot = $self->make_field_lookup($left, $self->token, $symbol);
344              
345 47         2041 $self->advance;
346              
347 47         305 return $dot;
348             }
349              
350             sub std_block {
351 73     73 0 450 my $self = shift;
352 73         114 my ($symbol) = @_;
353              
354 73         210 my $inverted = $symbol->id eq '^';
355              
356 73         204 my $name = $self->expression(0);
357              
358 73 50 66     534 if ($name->arity ne 'key' && $name->arity ne 'key_field' && $name->arity ne 'call') {
      66        
359 0         0 $self->_unexpected("opening block name", $self->token);
360             }
361 73         218 my $name_string = $self->_field_to_string($name);
362              
363 73         311 $self->advance(';');
364              
365 73         97 my %block;
366 73         117 my $context = 'if';
367 73         240 $block{$context}{body} = $self->statements;
368              
369 73 100       359 if ($self->token->id eq 'else') {
370 10         33 $self->advance;
371              
372 10         44 $block{$context}{raw_text} = $self->token;
373 10         31 $self->advance;
374 10         38 $block{$context}{open_tag} = $self->token;
375 10         31 $self->advance;
376 10         34 $block{$context}{close_tag} = $self->token;
377 10         33 $self->advance;
378              
379 10         19 $context = 'else';
380 10         32 $block{$context}{body} = $self->statements;
381             }
382              
383 73         209 $self->advance('/');
384              
385 73         255 $block{$context}{raw_text} = $self->token;
386 73         209 $self->advance;
387 73         242 $block{$context}{open_tag} = $self->token;
388 73         205 $self->advance;
389 73         261 $block{$context}{close_tag} = $self->token;
390 73         202 $self->advance;
391              
392 73 100       252 if ($inverted) {
393 1         4 ($block{if}, $block{else}) = ($block{else}, $block{if});
394 1 50       4 if (!$block{if}) {
395 1         3 $block{if}{body} = $self->literal('');
396 1         30 $block{if}{raw_text} = $self->literal('');
397 1         28 $block{if}{open_tag} = $block{else}{open_tag};
398 1         3 $block{if}{close_tag} = $block{else}{close_tag};
399             }
400             }
401              
402 73         167 my $closing_name = $self->expression(0);
403              
404 73 50 66     523 if ($closing_name->arity ne 'key' && $closing_name->arity ne 'key_field' && $closing_name->arity ne 'call') {
      66        
405 0         0 $self->_unexpected("closing block name", $self->token);
406             }
407 73         171 my $closing_name_string = $self->_field_to_string($closing_name);
408              
409 73 50       212 if ($name_string ne $closing_name_string) {
410 0         0 $self->_unexpected('/' . $name_string, $self->token);
411             }
412              
413 73         212 $self->advance(';');
414              
415 73         278 return $self->print_raw(
416             $name->clone(
417             arity => 'block',
418             first => $name,
419             second => \%block,
420             ),
421             );
422             }
423              
424             sub nud_mark_raw {
425 26     26 0 156 my $self = shift;
426 26         41 my ($symbol) = @_;
427              
428 26         87 return $self->symbol('mark_raw')->clone(
429             line => $symbol->line,
430             )->nud($self);
431             }
432              
433             sub nud_uplevel {
434 17     17 0 95 my $self = shift;
435 17         30 my ($symbol) = @_;
436              
437 17         61 return $symbol->clone(arity => 'variable');
438             }
439              
440             sub std_partial {
441 3     3 0 18 my $self = shift;
442 3         5 my ($symbol) = @_;
443              
444 3         15 my $partial = $self->token->clone(arity => 'literal');
445 3         72 $self->advance;
446 3         5 my $args;
447 3 100       20 if ($self->token->id ne ';') {
448 1         3 $args = $self->expression(0);
449             }
450 3         18 $self->advance(';');
451              
452 3 50       26 return $symbol->clone(
453             arity => 'partial',
454             first => ($partial->id =~ /\./ ? $partial : [ $partial ]),
455             second => $args,
456             );
457             }
458              
459             sub led_equals {
460 4     4 0 28 my $self = shift;
461 4         8 my ($symbol, $left) = @_;
462              
463 4         22 my $right = $self->expression($symbol->lbp);
464              
465 4         15 return $symbol->clone(
466             arity => 'pair',
467             first => $left->clone(arity => 'literal'),
468             second => $right,
469             );
470             }
471              
472             sub undefined_name {
473 283     283 0 422 my $self = shift;
474 283         435 my ($name) = @_;
475              
476 283         815 return $self->symbol('(key)')->clone(id => $name);
477             }
478              
479             sub define_function {
480 243     243 0 2865 my $self = shift;
481 243         633 my (@names) = @_;
482              
483 243         787 $self->SUPER::define_function(@_);
484 243         8217 for my $name (@names) {
485 1651         4107 my $symbol = $self->symbol($name);
486 1651         19517 $symbol->set_nud($self->can('nud_name'));
487 1651         10372 $symbol->lbp(10);
488             }
489              
490 243         626 return;
491             }
492              
493             sub define_helper {
494 162     162 0 295 my $self = shift;
495 162         348 my (@names) = @_;
496              
497 162         355 $self->define_function(@names);
498 162         316 for my $name (@names) {
499 343         877 my $symbol = $self->symbol($name);
500 343         3134 $symbol->is_helper(1);
501             }
502              
503 162         430 return;
504             }
505              
506             sub parse_literal {
507 650     650 0 852 my $self = shift;
508 650         1010 my ($literal) = @_;
509              
510 650 100       1628 if ($literal =~ /\A\[(.*)\]\z/ms) {
511 4         10 $literal = $1;
512 4         15 $literal =~ s/(["\\])/\\$1/g;
513 4         10 $literal = '"' . $literal . '"';
514             }
515              
516 650         1848 return $self->SUPER::parse_literal($literal);
517             }
518              
519             sub is_valid_field {
520 47     47 0 64 my $self = shift;
521 47         72 my ($field) = @_;
522              
523             # allow foo.[10]
524 47 100       159 return 1 if $field->arity eq 'literal';
525             # undefined symbols are all treated as keys - see undefined_name
526 44 100       213 return 1 if $field->arity eq 'key';
527             # allow ../../foo
528 11 50       54 return 1 if $field->id eq '..';
529              
530 0         0 return;
531             }
532              
533             sub expression {
534 812     812 0 6750 my $self = shift;
535 812         1167 my ($rbp) = @_;
536              
537 812         1770 my $token = $self->token;
538 812         2031 $self->advance;
539 812         2253 my $left = $token->nud($self);
540              
541 812         20061 while ($rbp < $self->token->lbp) {
542 122         298 $token = $self->token;
543 122 100       442 if ($token->has_led) {
544 52         155 $self->advance;
545 52         268 $left = $token->led($self, $left);
546             }
547             else {
548 70 50       245 if ($left->arity ne 'call') {
549 0         0 $self->_error("Unexpected " . $token->arity, $left);
550             }
551 70         103 push @{ $left->second }, $self->expression($token->lbp);
  70         345  
552             }
553             }
554              
555 812         2828 return $left;
556             }
557              
558             sub call {
559 932     932 0 1326 my $self = shift;
560              
561 932         2677 my $call = $self->SUPER::call(@_);
562 932         33389 $call->is_helper($call->first->is_helper);
563 932         2755 return $call;
564             }
565              
566             sub make_field_lookup {
567 47     47 0 78 my $self = shift;
568 47         72 my ($var, $field, $dot) = @_;
569              
570 47 50       118 if (!$self->is_valid_field($field)) {
571 0         0 $self->_unexpected("a field name", $field);
572             }
573              
574 47   33     586 $dot ||= $self->symbol('.');
575              
576 47         142 return $dot->clone(
577             arity => 'key_field',
578             first => $var,
579             second => $field->clone(arity => 'literal'),
580             );
581             }
582              
583             sub print_raw {
584 73     73 0 1693 my $self = shift;
585 73         251 return $self->print(@_)->clone(id => 'print_raw');
586             }
587              
588             sub literal {
589 384     384 0 511 my $self = shift;
590 384         539 my ($value) = @_;
591 384         978 return $self->symbol('(literal)')->clone(id => $value);
592             }
593              
594             sub _field_to_string {
595 146     146   200 my $self = shift;
596 146         236 my ($symbol) = @_;
597              
598             # name and key can just be returned
599 146 50       769 return $symbol->id
600             unless $symbol->arity eq 'field';
601              
602             # field accesses should recurse on the first and append the second
603 0           return $self->_field_to_string($symbol->first) . '.' . $symbol->second->id;
604             }
605              
606             __PACKAGE__->meta->make_immutable;
607 12     12   80 no Mouse;
  12         23  
  12         65  
608              
609             =for Pod::Coverage
610             call
611             define_function
612             define_helper
613             expression
614             init_symbols
615             is_valid_field
616             led_dot
617             led_equals
618             literal
619             make_field_lookup
620             nud_key
621             nud_mark_raw
622             nud_name
623             nud_uplevel
624             parse_literal
625             preprocess
626             print_raw
627             split_tags
628             std_block
629             std_partial
630             symbol_class
631             tokenize
632             undefined_name
633              
634             =cut
635              
636             1;