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