File Coverage

blib/lib/Text/Caml.pm
Criterion Covered Total %
statement 194 203 95.5
branch 105 122 86.0
condition 18 24 75.0
subroutine 22 22 100.0
pod 5 5 100.0
total 344 376 91.4


line stmt bran cond sub pod time code
1             package Text::Caml;
2              
3 14     14   176777 use strict;
  14         18  
  14         348  
4 14     14   43 use warnings;
  14         13  
  14         483  
5              
6             require Carp;
7             require Scalar::Util;
8 14     14   51 use File::Spec ();
  14         16  
  14         28315  
9              
10             our $VERSION = '0.15';
11              
12             our $LEADING_SPACE = qr/(?:\n [ ]*)?/x;
13             our $TRAILING_SPACE = qr/(?:[ ]* \n)?/x;
14             our $START_TAG = qr/\{\{/x;
15             our $END_TAG = qr/\}\}/x;
16              
17             our $START_OF_PARTIAL = quotemeta '>';
18             our $START_OF_SECTION = quotemeta '#';
19             our $START_OF_INVERTED_SECTION = quotemeta '^';
20             our $END_OF_SECTION = quotemeta '/';
21             our $START_OF_TEMPLATE_INHERITANCE = quotemeta '<';
22             our $END_OF_TEMPLATE_INHERITANCE = quotemeta '/';
23             our $START_OF_BLOCK = quotemeta '$';
24             our $END_OF_BLOCK = quotemeta '/';
25              
26             sub new {
27 17     17 1 2748 my $class = shift;
28 17         38 my (%params) = @_;
29              
30 17         28 my $self = {};
31 17         27 bless $self, $class;
32              
33 17         68 $self->{templates_path} = $params{templates_path};
34 17         27 $self->{default_partial_extension} = $params{default_partial_extension};
35              
36 17 100       56 $self->set_templates_path('.')
37             unless $self->templates_path;
38              
39 17         36 return $self;
40             }
41              
42 54     54 1 430 sub templates_path { $_[0]->{templates_path} }
43 11     11 1 19 sub set_templates_path { $_[0]->{templates_path} = $_[1] }
44              
45             sub render {
46 80     80 1 24501 my $self = shift;
47 80         82 my $template = shift;
48 80 100       200 my $context = ref $_[0] eq 'HASH' ? $_[0] : {@_};
49              
50 80         138 $self->_parse($template, $context);
51             }
52              
53             sub render_file {
54 5     5 1 435 my $self = shift;
55 5         4 my $template = shift;
56 5 50       15 my $context = ref $_[0] eq 'HASH' ? $_[0] : {@_};
57              
58 5         10 $template = $self->_slurp_template($template);
59 4         12 return $self->_parse($template, $context);
60             }
61              
62             sub _parse {
63 177     177   149 my $self = shift;
64 177         138 my $template = shift;
65 177         124 my $context = shift;
66 177         122 my $override = shift;
67              
68 177         181 my $output = '';
69              
70 177         324 pos $template = 0;
71 177         395 while (pos $template < length $template) {
72 240 100       2060 if ($template =~ m/($LEADING_SPACE)?\G $START_TAG /gcxms) {
    100          
73 163         146 my $chunk = '';
74              
75 163         239 my $leading_newline = !!$1;
76              
77             # Tripple
78 163 100       3040 if ($template =~ m/\G { (.*?) } $END_TAG/gcxms) {
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    50          
79 2         3 $chunk .= $self->_parse_tag($1, $context);
80             }
81              
82             # Replace
83             elsif ($template =~ m/\G - (.*?) $END_TAG/gcxms) {
84 1         2 $chunk .= '{{' . $1 . '}}';
85             }
86              
87             # Comment
88             elsif ($template =~ m/\G ! .*? $END_TAG/gcxms) {
89             }
90              
91             # Section
92             elsif ($template
93             =~ m/\G $START_OF_SECTION \s* (.*?) \s* $END_TAG ($TRAILING_SPACE)?/gcxms
94             )
95             {
96 47         61 my $name = $1;
97 47         39 my $end_of_section = $name;
98              
99 47 50       934 if ($template
100             =~ m/\G (.*?) ($LEADING_SPACE)? $START_TAG $END_OF_SECTION $end_of_section $END_TAG ($TRAILING_SPACE)?/gcxms
101             )
102             {
103 47         96 $chunk .= $self->_parse_section($name, $1, $context);
104             }
105             else {
106 0         0 Carp::croak("Section's '$name' end not found");
107             }
108             }
109              
110             # Inverted section
111             elsif ($template
112             =~ m/\G $START_OF_INVERTED_SECTION (.*?) $END_TAG ($TRAILING_SPACE)?/gcxms
113             )
114             {
115 12         14 my $name = $1;
116              
117 12 50       200 if ($template
118             =~ m/ \G (.*?) ($LEADING_SPACE)? $START_TAG $END_OF_SECTION $name $END_TAG ($TRAILING_SPACE)?/gcxms
119             )
120             {
121 12         23 $chunk
122             .= $self->_parse_inverted_section($name, $1, $context);
123             }
124             else {
125 0         0 Carp::croak("Section's '$name' end not found");
126             }
127             }
128              
129             # End of section
130             elsif ($template =~ m/\G $END_OF_SECTION (.*?) $END_TAG/gcxms) {
131 0         0 Carp::croak("Unexpected end of section '$1'");
132             }
133              
134             # Partial
135             elsif ($template =~ m/\G $START_OF_PARTIAL \s* (.*?) \s* $END_TAG/gcxms) {
136 8         19 $chunk .= $self->_parse_partial($1, $context);
137             }
138              
139             # Inherited template
140             elsif ($template =~ m/\G $START_OF_TEMPLATE_INHERITANCE \s* (.*?) \s* $END_TAG/gcxms)
141             {
142 6         8 my $name = $1;
143 6         6 my $end_of_inherited_template = $name;
144              
145 6 50       150 if ($template
146             =~ m/\G (.*?) ($LEADING_SPACE)? $START_TAG $END_OF_TEMPLATE_INHERITANCE $end_of_inherited_template $END_TAG ($TRAILING_SPACE)?/gcxms
147             )
148             {
149 6         15 $chunk .= $self->_parse_inherited_template($name, $1, $context);
150             }
151             else {
152 0         0 Carp::croak("Nested template's '$name' end not found");
153             }
154             }
155              
156             # block
157             elsif ($template =~ m/\G $START_OF_BLOCK \s* (.*?) \s* $END_TAG/gcxms) {
158 5         9 my $name = $1;
159 5         5 my $end_of_block = $name;
160              
161 5 50       64 if ($template
162             =~ m/\G (.*?) ($LEADING_SPACE)? $START_TAG $END_OF_BLOCK $end_of_block $END_TAG/gcxms
163             )
164             {
165 5         8 $chunk .= $self->_parse_block($name, $1, $context, $override);
166             }
167             else {
168 0         0 Carp::croak("Block's '$name' end not found");
169             }
170             }
171              
172             # Tag
173             elsif ($template =~ m/\G (.*?) $END_TAG/gcxms) {
174 77         137 $chunk .= $self->_parse_tag_escaped($1, $context);
175             }
176             else {
177 0         0 Carp::croak("Can't find where tag is closed");
178             }
179              
180 163 100 100     331 if ($chunk ne '') {
    100          
181 135         321 $output .= $chunk;
182             }
183             elsif ($output eq '' || $leading_newline) {
184 23 50       168 if ($template =~ m/\G $TRAILING_SPACE/gcxms) {
185 23         112 $output =~ s/[ ]*\z//xms;
186             }
187             }
188             }
189              
190             # Text before tag
191             elsif ($template =~ m/\G (.*?) (?=$START_TAG\{?)/gcxms) {
192 28         91 $output .= $1;
193             }
194              
195             # Other text
196             else {
197 49         96 $output .= substr($template, pos($template));
198 49         64 last;
199             }
200             }
201              
202 177         365 return $output;
203             }
204              
205             sub _parse_tag {
206 79     79   70 my $self = shift;
207 79         83 my ($name, $context) = @_;
208              
209 79         77 my $value;
210             my %args;
211              
212             # Current element
213 79 100       131 if ($name eq '.') {
214 20 50       31 return '' if $self->_is_empty($context, $name);
215              
216 20         19 $value = $context->{$name};
217             }
218              
219             else {
220 59         93 $value = $self->_get_value($context, $name);
221             }
222              
223 79 100       139 if (ref $value eq 'CODE') {
224 5         11 my $content = $value->($self, '', $context);
225 5 100       16 $content = '' unless defined $content;
226 5         17 return $self->_parse($content, $context);
227             }
228              
229 74         99 return $value;
230             }
231              
232             sub _find_value {
233 115     115   94 my $self = shift;
234 115         96 my ($context, $name) = @_;
235              
236 115         239 my @parts = split /\./ => $name;
237              
238 115         94 my $value = $context;
239              
240 115         124 foreach my $part (@parts) {
241 129 50 66     477 if ( ref $value eq "HASH"
      66        
      66        
242             && exists $value->{'_with'}
243             && Scalar::Util::blessed($value->{'_with'})
244             && $value->{'_with'}->can($part))
245             {
246 3         7 $value = $value->{'_with'}->$part;
247 3         10 next;
248             }
249              
250 126 100       189 if( ref $value eq "ARRAY" ) {
251 2         3 $value = $value->[$part];
252 2         3 next;
253             }
254              
255 124 100 100     299 if ( exists $value->{'.'}
      66        
256             && Scalar::Util::blessed($value->{'.'})
257             && $value->{'.'}->can($part))
258             {
259 2         6 $value = $value->{'.'}->$part;
260 2         8 next;
261             }
262              
263 122 100       188 return undef if $self->_is_empty($value, $part);
264             $value =
265 104 100       242 Scalar::Util::blessed($value) ? $value->$part : $value->{$part};
266             }
267              
268 97         166 return \$value;
269             }
270              
271             sub _get_value {
272 106     106   72 my $self = shift;
273 106         130 my ($context, $name) = @_;
274              
275 106 100       172 if ($name eq '.') {
276 3 50       5 return '' if $self->_is_empty($context, $name);
277 3         6 return $context->{$name};
278             }
279              
280 103         126 my $value = $self->_find_value($context, $name);
281              
282 103 100       200 return $value ? $$value : '';
283             }
284              
285             sub _parse_tag_escaped {
286 77     77   63 my $self = shift;
287 77         113 my ($tag, $context) = @_;
288              
289 77         58 my $do_not_escape;
290 77 100       178 if ($tag =~ s/\A \&//xms) {
291 1         1 $do_not_escape = 1;
292             }
293              
294 77         105 my $output = $self->_parse_tag($tag, $context);
295              
296 77 100       181 $output = $self->_escape($output) unless $do_not_escape;
297              
298 77         112 return $output;
299             }
300              
301             sub _parse_section {
302 47     47   32 my $self = shift;
303 47         97 my ($name, $template, $context) = @_;
304              
305 47         72 my $value = $self->_get_value($context, $name);
306              
307 47         46 my $output = '';
308              
309 47 100       139 if (ref $value eq 'HASH') {
    100          
    100          
    100          
    100          
310 2         8 $output .= $self->_parse($template, {%$context, %$value});
311             }
312             elsif (ref $value eq 'ARRAY') {
313 19         19 my $idx = 0;
314 19         26 foreach my $el (@$value) {
315 38 100       98 my %subcontext = ref $el eq 'HASH' ? %$el : ('.' => $el);
316 38         40 $subcontext{'_idx'} = $idx;
317              
318 38         53 $subcontext{'_even'} = $idx % 2 == 0;
319 38         49 $subcontext{'_odd'} = $idx % 2 != 0;
320              
321 38         40 $subcontext{'_first'} = $idx == 0;
322 38         46 $subcontext{'_last'} = $idx == $#$value;
323              
324 38         174 $output .= $self->_parse($template, {%$context, %subcontext});
325              
326 38         107 $idx++;
327             }
328             }
329             elsif (ref $value eq 'CODE') {
330 4         8 $template = $self->_parse($template, $context);
331 4         10 $output
332             .= $self->_parse($value->($self, $template, $context), $context);
333             }
334             elsif (ref $value) {
335 3         14 $output .= $self->_parse($template, {%$context, _with => $value});
336             }
337             elsif ($value) {
338 12         41 $output .= $self->_parse($template, $context);
339             }
340              
341 47         78 return $output;
342             }
343              
344             sub _parse_inverted_section {
345 12     12   11 my $self = shift;
346 12         25 my ($name, $template, $context) = @_;
347              
348 12         16 my $value = $self->_find_value($context, $name);
349 12 100       26 return $self->_parse($template, $context)
350             unless defined $value;
351              
352 6         7 $value = $$value;
353 6         9 my $output = '';
354              
355 6 50       18 if (ref $value eq 'HASH') {
    100          
    50          
356             }
357             elsif (ref $value eq 'ARRAY') {
358 2 100       5 return '' if @$value;
359              
360 1         9 $output .= $self->_parse($template, $context);
361             }
362             elsif (!$value) {
363 0         0 $output .= $self->_parse($template, $context);
364             }
365              
366 5         9 return $output;
367             }
368              
369             sub _parse_partial {
370 8     8   9 my $self = shift;
371 8         11 my ($template, $context) = @_;
372              
373 8 100       33 if (my $ext = $self->{default_partial_extension}) {
374 1         3 $template = "$template.$ext";
375             }
376              
377 8         8 my $parse = 1;
378 8 100       22 if ($template =~ s{^\&}{}) {
379 1         1 $parse = 0;
380             }
381              
382 8         16 my $content = $self->_slurp_template($template);
383              
384 8 100       30 return $parse ? $self->_parse($content, $context) : $content;
385             }
386              
387             sub _parse_inherited_template {
388 6     6   7 my $self = shift;
389 6         11 my ($name, $override, $context) = @_;
390              
391 6 50       17 if (my $ext = $self->{default_partial_extension}) {
392 0         0 $name = "$name.$ext";
393             }
394              
395 6         9 my $content = $self->_slurp_template($name);
396              
397 6         19 return $self->_parse($content, $context, $override);
398             }
399              
400             sub _parse_block {
401 5     5   5 my $self = shift;
402 5         11 my ($name, $template, $context, $override) = @_;
403              
404             # get block content from override
405 5         4 my $content;
406            
407             # first, see if we can find any starting block with this name in the override
408 5 100       32 if ($override =~ m/ $START_OF_BLOCK \s* $name \s* $END_TAG/gcxms) {
409             # get the content of the override block and make sure there's a corresponding end-block tag for it!
410 3 50       25 if ($override =~ m/ (.*) $START_TAG $END_OF_BLOCK \s* $name \s* $END_TAG/gcxms){
411 3         4 my $content = $1;
412 3         10 return $self->_parse($content, $context);
413             } else {
414 0         0 Carp::croak("Block's '$name' end not found");
415             }
416             }
417            
418 2         5 return $self->_parse($template, $context);
419             }
420              
421             sub _slurp_template {
422 19     19   16 my $self = shift;
423 19         33 my ($template) = @_;
424              
425 19 100 66     38 my $path =
426             defined $self->templates_path
427             && !(File::Spec->file_name_is_absolute($template))
428             ? File::Spec->catfile($self->templates_path, $template)
429             : $template;
430              
431 19 100 66     464 Carp::croak("Can't find '$path'") unless defined $path && -f $path;
432              
433 18         15 my $content = do {
434 18         46 local $/;
435 18 50   5   417 open my $file, '<:encoding(UTF-8)', $path or return;
  5         25  
  5         6  
  5         25  
436 18         41103 <$file>;
437             };
438              
439 18 50       342 Carp::croak("Can't open '$template'") unless defined $content;
440              
441 18         32 chomp $content;
442              
443 18         35 return $content;
444             }
445              
446             sub _is_empty {
447 145     145   99 my $self = shift;
448 145         173 my ($vars, $name) = @_;
449              
450 145         97 my $var;
451              
452 145 100       332 if (Scalar::Util::blessed($vars)) {
453 3         11 $var = $vars->$name;
454             }
455             else {
456 142 100       218 return 1 unless exists $vars->{$name};
457 138         141 $var = $vars->{$name};
458             }
459              
460 141 100       195 return 1 unless defined $var;
461 139 100       257 return 1 if $var eq '';
462              
463 127         573 return 0;
464             }
465              
466             sub _escape {
467 76     76   60 my $self = shift;
468 76         57 my $value = shift;
469              
470 76         82 $value =~ s/&/&/g;
471 76         58 $value =~ s/
472 76         67 $value =~ s/>/>/g;
473 76         57 $value =~ s/"/"/g;
474              
475 76         87 return $value;
476             }
477              
478             1;
479             __END__