File Coverage

blib/lib/Text/Caml.pm
Criterion Covered Total %
statement 195 204 95.5
branch 105 122 86.0
condition 19 24 79.1
subroutine 22 22 100.0
pod 5 5 100.0
total 346 377 91.7


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