File Coverage

blib/lib/Positron/Template.pm
Criterion Covered Total %
statement 267 281 95.0
branch 113 130 86.9
condition 47 66 71.2
subroutine 25 26 96.1
pod 0 4 0.0
total 452 507 89.1


line stmt bran cond sub pod time code
1             package Positron::Template;
2             our $VERSION = 'v0.1.3'; # VERSION
3              
4             =head1 NAME
5              
6             Positron::Template - a DOM based templating system
7              
8             =head1 VERSION
9              
10             version v0.1.3
11              
12             =head1 SYNOPSIS
13              
14             use Positron::Template;
15              
16             my $template = Positron::Template->new();
17              
18             my $dom = create_dom_tree();
19             my $data = { foo => 'bar', baz => [ 1, 2, 3 ] };
20             my $result = $template->process($dom, $data);
21              
22             =cut
23              
24 12     12   396226 use v5.10;
  12         47  
  12         576  
25 12     12   72 use strict;
  12         25  
  12         396  
26 12     12   66 use warnings;
  12         24  
  12         336  
27              
28 12     12   70 use Carp;
  12         24  
  12         1121  
29 12     12   7410 use Positron::Environment;
  12         30  
  12         341  
30 12     12   20527 use Positron::Expression;
  12         49  
  12         570  
31 12     12   81 use Scalar::Util qw(blessed);
  12         26  
  12         48025  
32              
33             sub new {
34 11     11 0 125 my ($class, %options) = @_;
35 11   50     273 my $self = {
      50        
      50        
      33        
      50        
36             opener => '{',
37             closer => '}',
38             dom => $options{dom} // undef,
39             environment => Positron::Environment->new($options{env} // undef, { immutable => 1 }) // undef,
40             handler => $options{handler} || _handler_for($options{dom}) // undef,
41             include_paths => ['.'],
42             };
43 11         60 return bless ($self, $class);
44             }
45              
46             # Stop writing these until need is shown
47             sub dom {
48 0     0 0 0 my ($self, $dom) = @_;
49 0 0       0 if (@_ == 1) {
50 0         0 return $self->{'dom'};
51             } else {
52 0         0 $self->{'dom'} = $dom;
53             }
54             }
55              
56             sub add_include_paths {
57 2     2 0 13 my ($self, @paths) = @_;
58 2         5 push @{$self->{'include_paths'}}, @paths;
  2         14  
59             }
60              
61              
62             sub process {
63 141     141 0 23757 my ($self, $dom, $data) = @_;
64             # TODO: what if only one is passed? -> $self attributes
65             # What if a HashRef is passed? -> new Environment object
66 141 50 33     973 if (ref($dom) eq 'HASH' or blessed($dom) and $dom->isa('Positron::Environment')) {
      33        
67 0         0 $data = $dom;
68 0         0 $dom = $self->{'dom'};
69             }
70 141 50       325 if (not $data) {
71 0         0 $data = $self->{'environment'};
72             }
73 141 50       381 if (ref($data) eq 'HASH') {
74 141         655 $data = Positron::Environment->new($data);
75             }
76              
77 141 100       366 if (not ref($dom)) {
78 23         53 return $self->_process_text($dom, $data);
79             }
80             # Real DOM -> List of nodes
81            
82 118         192 my @nodes = ();
83              
84 118   66     368 $self->{'handler'} //= _handler_for($dom);
85 118         323 @nodes = $self->_process_element($dom, $data);
86             # DESTROY Handler?
87              
88             # Many people know that they will get a single node here.
89             # May as well not force them to unpack a list.
90 114 100       1279 return (wantarray) ? @nodes : $nodes[0];
91             }
92              
93             sub _process_text {
94 146     146   251 my ($self, $string, $environment, $with_quants) = @_;
95 146         267 my $string_finder = $self->_make_finder('$');
96 146         428 my $last_changing_quant = undef;
97 146         166 my $did_change = undef;
98             # First $ sigils; the quantifier chomps whitespace around it.
99 146         1286 $string =~ s{
100             (\s*)
101             $string_finder
102             (\s*)
103             }{
104 81         354 my ($ws_before, $sigil, $quant, $expr, $ws_after) = ($1, $2, $3, $4, $5);
105 81   100     225 my $replacement = Positron::Expression::evaluate($expr, $environment) // '';
106 81 100       250 if ($quant eq '-') {
    100          
107 3         5 $ws_before = '';
108 3         4 $ws_after = '';
109             } elsif ($quant eq '*') {
110 3   100     15 $ws_before &&= ' ';
111 3   50     15 $ws_after &&= ' ';
112 3 100       16 if ($replacement eq '') {
113 1         2 $ws_before = ' '; $ws_after = '';
  1         2  
114             }
115             }
116 81         108 $last_changing_quant = $quant;
117 81         90 $did_change = 1;
118 81         263 "$ws_before$replacement$ws_after";
119             }xmseg;
120             # Next comments; the quantifier chomps whitespace around it.
121 146         361 my $comment_finder = $self->_make_finder('#');
122 146         1007 $string =~ s{
123             (\s*)
124             $comment_finder
125             (\s*)
126             }{
127 12         158 my ($ws_before, $sigil, $quant, $comment, $ws_after) = ($1, $2, $3, $4, $5);
128 12 100       47 if ($quant eq '-') {
    100          
129 1         3 $ws_before = '';
130 1         1 $ws_after = '';
131             } elsif ($quant eq '*') {
132 2         4 $ws_before = ' ';
133 2         4 $ws_after = '';
134             }
135 12         16 $last_changing_quant = $quant;
136 12         20 $did_change = 1;
137 12         36 "$ws_before$ws_after";
138             }xmseg;
139             # Next voider; the quantifier chomps whitespace around it.
140 146         325 my $voider_finder = $self->_make_finder('~');
141 146         937 $string =~ s{
142             (\s*)
143             $voider_finder
144             (\s*)
145             }{
146 10         42 my ($ws_before, $sigil, $quant, undef, $ws_after) = ($1, $2, $3, $4, $5);
147 10 100       46 if ($quant eq '-') {
    100          
148 1         3 $ws_before = '';
149 1         3 $ws_after = '';
150             } elsif ($quant eq '*') {
151 1         4 $ws_before = ' ';
152 1         2 $ws_after = ' ';
153 1 50       13 if ("$ws_before$ws_after" =~ m{\A \s+ \z}xms) {
154 1         3 $ws_before = ' '; $ws_after = '';
  1         3  
155             }
156             }
157 10         15 $last_changing_quant = $quant;
158 10         37 $did_change = 1;
159 10         33 "$ws_before$ws_after";
160             }xmseg;
161 146 100       1408 return $with_quants? ($string, $did_change, $last_changing_quant) : $string;
162             }
163              
164             sub _process_element {
165 525     525   718 my ($self, $node, $environment) = @_;
166 525         723 my $handler = $self->{'handler'};
167              
168 525 100       1103 if (not ref($node)) {
169 102         234 return $self->_process_text($node, $environment);
170             }
171              
172             # check for assignments
173             # create a modified environment if some are detected
174             # proceed as normal
175              
176             # Evaluate structure sigils
177 423         1548 my ($sigil, $quant, $tail) = $self->_get_structure_sigil($node);
178              
179             # Have sigil, evaluate
180 423   100     1309 $sigil //= ''; # for 'eq'
181 423 100       3102 if ($sigil eq '@') {
    100          
    100          
    100          
    100          
    100          
    100          
182 31         94 return $self->_process_loop($node, $environment, $sigil, $quant, $tail);
183             } elsif ($sigil ~~ ['?', '!']) {
184 23         66 return $self->_process_condition($node, $environment, $sigil, $quant, $tail);
185             } elsif ($sigil eq '|') {
186 75         180 return $self->_process_switch($node, $environment, $sigil, $quant, $tail);
187             } elsif ($sigil eq '/') {
188 4         15 return $self->_process_structure_comment($node, $environment, $sigil, $quant, $tail);
189             } elsif ($sigil ~~ ['.', ',']) {
190 12         42 return $self->_process_include($node, $environment, $sigil, $quant, $tail);
191             } elsif ($sigil ~~ [':', ';']) {
192 50         125 return $self->_process_wrap($node, $environment, $sigil, $quant, $tail);
193             } elsif ($sigil eq '^') {
194 9         29 return $self->_process_function($node, $environment, $sigil, $quant, $tail);
195             } else {
196 219         648 my $new_node = $handler->shallow_clone($node);
197 219         671 $handler->push_contents( $new_node, map { $self->_process_element($_, $environment) } $handler->list_contents($node));
  184         423  
198 215         471 $self->_remove_structure_sigils($new_node);
199             #$self->resolve_hash_attr($new_node, $environment);
200 215         464 $self->_resolve_text_attr($new_node, $environment);
201 215         894 return $new_node;
202             }
203             # String ones
204 0         0 return $node;
205             }
206              
207             sub _process_loop {
208 31     31   67 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
209 31         48 my $handler = $self->{'handler'};
210             # TODO: true() and coercion!
211 31   50     99 my $loop = Positron::Expression::evaluate($tail, $environment) || [];
212 31 100       86 if (not @$loop) {
213             # keep if we should, else nothing
214 8 100       53 return ($quant eq '+') ? ($self->_clone_and_resolve($node, $environment)) : ();
215             }
216             # else have loop
217 23         31 my @contents;
218 23         46 foreach my $row (@$loop) {
219 46         226 my $env = Positron::Environment->new($row, {parent => $environment});
220 46         172 my @row_contents = map { $self->_process_element( $_, $env) } $handler->list_contents($node);
  46         110  
221 46 100       213 push @contents, ($quant eq '*') ? ($self->_clone_and_resolve($node, $env, @row_contents)) : @row_contents;
222             }
223 23 100 100     131 if ($quant ne '-' and $quant ne '*') { # remove this in any case
224 21         58 return ($self->_clone_and_resolve($node, $environment, @contents));
225             }
226 2         9 return @contents;
227             }
228              
229             sub _process_condition {
230 23     23   45 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
231 23         38 my $handler = $self->{'handler'};
232 23         74 my $truth = Positron::Expression::true(Positron::Expression::evaluate($tail, $environment));
233 23 100       57 if ($sigil eq '!') {$truth = not $truth;}
  9         41  
234 23   100     118 my $keep = ($truth and $quant ne '-' or $quant eq '+');
235 23         35 my @contents = ();
236 23 100 100     78 if ($truth or $quant eq '*') {
237 12         34 @contents = map { $self->_process_element($_, $environment) } $handler->list_contents($node);
  12         30  
238             }
239 23 100       86 return ($keep) ? ($self->_clone_and_resolve($node, $environment, @contents)) : @contents;
240             }
241              
242             sub _process_switch {
243 75     75   135 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
244 75         110 my $handler = $self->{'handler'};
245 75         74 my $truth;
246 75 100       161 if ($tail =~ m{ \A : \s+ (.*) }xms) {
247             # "switch/given": setter
248 15         23 my $expr = $1;
249 15   100     47 my $goal = Positron::Expression::evaluate($expr, $environment) // ''; # always defined
250 15         81 $environment = Positron::Environment->new({'|' => $goal}, { parent => $environment, immutable => 0});
251 15         36 $truth = 1; # counts as a true condition for quantifiers
252             } else {
253             # "case/when": getter
254 60         147 my $goal = $environment->get('|');
255 60 100       113 if (defined ($goal)) {
256             # not consumed yet
257 52   100     120 my $test = Positron::Expression::evaluate($tail, $environment) // '';
258             # "truth" as in a condition
259             # To trigger the default, the $tail expression must be "empty", not the result!
260 52   100     334 $truth = ($goal eq $test or $tail =~ m{ \A \s* \z }xms);
261 52 100       111 if ($truth) {
262             # remember the match for defaults
263 15         42 $environment->set('|', undef); # Don't delete, because of parent
264             }
265             } else {
266             # already consumed, or never saw a '|:'
267 8         11 $truth = 0;
268             }
269             }
270             # Keep and contents (see condition)
271 75         104 my @contents = ();
272 75   66     333 my $keep = ($truth and $quant ne '-' or $quant eq '+');
273 75 100 66     249 if ($truth or $quant eq '*') {
274 30         76 @contents = map { $self->_process_element($_, $environment) } $handler->list_contents($node);
  75         158  
275             }
276 75 100       274 return ($keep) ? ($self->_clone_and_resolve($node, $environment, @contents)) : @contents;
277             }
278              
279             sub _process_structure_comment {
280 4     4   7 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
281 4         7 my $handler = $self->{'handler'};
282             # basically an always-false condition
283             # we could reuse our $keep and @contents code here, but this is probably
284             # more readable:
285 4 100       13 if ($quant eq '+') {
    100          
286             # keep node, not contents
287 1         5 return $self->_clone_and_resolve($node, $environment);
288             } elsif ($quant eq '*') {
289             # keep contents, not node
290 1         5 return map { $self->_process_element($_, $environment) } $handler->list_contents($node);
  2         11  
291             } else {
292 2         7 return; # nothing
293             }
294             }
295              
296             sub _process_include {
297 12     12   25 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
298 12         20 my $handler = $self->{'handler'};
299              
300 12         22 my @contents = ();
301              
302 12 100       23 if ($sigil eq '.') {
303             # from file
304 6         32 my $filename = Positron::Expression::evaluate($tail, $environment);
305 6         11 my $filepath = undef;
306 6         10 foreach my $include_path (@{$self->{'include_paths'}}) {
  6         18  
307 12 100       736 if (-r $include_path . $filename) {
308 5         18 $filepath = $include_path . $filename;
309             }
310             }
311 6 100       19 if (not defined $filepath) {
312 1         42 croak "Could not find $filename (from $tail) for inclusion";
313             }
314              
315              
316             # automatically die if we can't read this
317 5         22 @contents = $handler->parse_file($filepath);
318 4         10 @contents = map { $self->_process_element($_, $environment) } @contents;
  4         15  
319             } else {
320             # from env
321 6         25 my $env_contents = Positron::Expression::evaluate($tail, $environment);
322 6 50       16 if ($env_contents) {
323 6 50       16 if (ref($env_contents) eq 'ARRAY') {
324             # special case: can't allow ['a', 'text'], must be [['a', 'text']], sorry
325 6 100 66     48 if ($handler->isa('Positron::Handler::ArrayRef') and not ref($env_contents->[0])) {
326 4         10 @contents = ($env_contents);
327             } else {
328 2         6 @contents = @$env_contents;
329             }
330             } else {
331 0         0 @contents = ($env_contents);
332             }
333             } else {
334             # warn?
335 0         0 @contents = ();
336             }
337             }
338              
339 10         25 my $keep = ($quant eq '+');
340 10 100       62 return ($keep) ? ($self->_clone_and_resolve($node, $environment, @contents)) : @contents;
341             }
342              
343             # TODO: Refactor. Either extract the parts that are common between _include and
344             # _wrap, or just push them both together in one function.
345             sub _process_wrap {
346 50     50   94 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
347 50         66 my $handler = $self->{'handler'};
348              
349 50         67 my @contents = ();
350              
351 50 100       113 if ($tail =~ m{ \S }xms) {
352              
353 26         28 my @wrapping_contents;
354 26 100       50 if ($sigil eq ':') {
355             # filename, read that and include "us".
356 14         48 my $filename = Positron::Expression::evaluate($tail, $environment);
357 14         20 my $filepath = undef;
358 14         19 foreach my $include_path (@{$self->{'include_paths'}}) {
  14         32  
359 28 100       429 if (-r $include_path . $filename) {
360 13         38 $filepath = $include_path . $filename;
361             }
362             }
363 14 100       34 if (not defined $filepath) {
364 1         29 croak "Could not find $filename (from $tail) for wrapping";
365             }
366             # automatically die if we can't read this
367 13         38 @wrapping_contents = $handler->parse_file($filepath);
368             } else {
369             # from env
370 12         38 my $env_contents = Positron::Expression::evaluate($tail, $environment);
371 12 50       29 if ($env_contents) {
372 12 50       24 if (ref($env_contents) eq 'ARRAY') {
373             # special case: can't allow ['a', 'text'], must be [['a', 'text']], sorry
374 12 50 33     77 if ($handler->isa('Positron::Handler::ArrayRef') and not ref($env_contents->[0])) {
375 12         25 @wrapping_contents = ($env_contents);
376             } else {
377 0         0 @wrapping_contents = @$env_contents;
378             }
379             } else {
380 0         0 @wrapping_contents = ($env_contents);
381             }
382             } else {
383             # warn?
384 0         0 @wrapping_contents = ();
385             }
386             }
387              
388             # TODO: resolve later? We'd need to clone $node and remove structure sigils to defeat recursion
389             # Resolve now; also allows clone_and_resolve to clear sigils to defeat recursion
390 24         102 @contents = map { $self->_process_element($_, $environment) } $handler->list_contents($node);
  48         106  
391             # only quant-less versions pass the parent
392 24 100       72 my @passed_nodes = $quant ? @contents : $self->_clone_and_resolve($node, $environment, @contents);
393              
394 24         182 $environment = Positron::Environment->new({':' => [ @passed_nodes ]}, { parent => $environment, immutable => 0});
395              
396 24         70 @contents = @wrapping_contents;
397 24         33 @contents = map { $self->_process_element($_, $environment) } @contents;
  24         50  
398              
399             } else {
400             # inclusion marker
401 24   50     65 my $passed_nodes = $environment->get(':') || [];
402             # On error, just kill all (warn, maybe?)
403             # Remember: already resolved!
404 24         52 @contents = @$passed_nodes;
405             }
406              
407             # this works for both cases!
408 48         74 my $keep = ($quant eq '+');
409 48 100       254 return ($keep) ? ($self->_clone_and_resolve($node, $environment, @contents)) : @contents;
410             }
411              
412             sub _process_function {
413 9     9   17 my ($self, $node, $environment, $sigil, $quant, $tail) = @_;
414 9         18 my $handler = $self->{'handler'};
415 9         34 my $function = Positron::Expression::evaluate($tail, $environment);
416              
417             # The "self" node can be part of the arguments (quant ''), receive the results of the function
418             # (quant '+'), or be dropped altogether (quant '-').
419 9         19 my $keep = ($quant eq '+');
420 9   100     115 my $pass_self = (not $keep and $quant ne '-');
421              
422 9         14 my @contents = ();
423             # Need these in any case:
424 9         29 @contents = map { $self->_process_element($_, $environment) } $handler->list_contents($node);
  12         33  
425 9 100       22 if ($pass_self) {
426             # we could also need to pass "self".
427 3         11 @contents = $self->_clone_and_resolve($node, $environment, @contents);
428             }
429              
430 9         33 @contents = $function->(@contents);
431 9 100       126 return ($keep) ? ($self->_clone_and_resolve($node, $environment, @contents)) : @contents;
432             }
433              
434              
435             sub _make_finder {
436 1186     1186   57131 my ($self, $sigils) = @_;
437             # What to do on empty sigils? Need to find during development!
438 1186 100       2308 die "Empty sigil list!" unless $sigils;
439 1185         2154 my ($opener, $closer) = ($self->{opener}, $self->{closer});
440 1185         2123 my ($eopener, $ecloser) = ("\\$opener","\\$closer");
441 1185         6929 my ($esigils) = join('', map { "\\$_" } split(qr{}, $sigils));
  8228         14774  
442 1185         29175 return qr{
443             $eopener
444             ( [$esigils] )
445             ( [-+*]? )
446             ( [^$ecloser]* )
447             $ecloser
448             }xms;
449             }
450              
451             # Handlers for:
452             # scalar string, no handler
453             # HTML::Element
454             # XML::LibXML
455             # ArrayRef Handler
456             # TODO: an extensible mechanism
457             sub _handler_for {
458 20     20   48 my ($dom) = @_;
459 20 100       194 return unless ref($dom); # Text at most, needs no handler
460 9 50       34 if (ref($dom) eq 'ARRAY') {
    0          
461 9         11830 require Positron::Handler::ArrayRef;
462 9         64 return Positron::Handler::ArrayRef->new();
463             } elsif (my $package = blessed($dom)) {
464 0 0       0 eval "require Positron::Handler::$package; 1;" or croak "Could not load handler for $package";
465             }
466             }
467              
468             sub _get_structure_sigil {
469 423     423   535 my ($self, $node) = @_;
470 423         532 my $handler = $self->{'handler'};
471 423         778 my $structure_finder = $self->_make_finder('@?!/.:,;|^');
472 423         1685 foreach my $attr ($handler->list_attributes($node)) {
473 225         643 my $value = $handler->get_attribute($node, $attr);
474 225 100       2066 if ($value =~ m{ $structure_finder }xms) {
475 204         1190 return ($1, $2, $3);
476             }
477             }
478 219         597 return; # Has none
479             }
480              
481             sub _remove_structure_sigils {
482 322     322   441 my ($self, $node) = @_;
483 322         441 my $handler = $self->{'handler'};
484             # NOTE: we remove '=' here as well, even though it's not a structure sigil!
485 322         590 my $structure_finder = $self->_make_finder('@?!/.:,;=|^');
486 322         1073 foreach my $attr ($handler->list_attributes($node)) {
487 129         426 my $value = $handler->get_attribute($node, $attr);
488 129         1083 my $did_change = ($value =~ s{ $structure_finder }{}xmsg);
489 129 100       331 if ($did_change) {
490             # We removed something from this attribute -> delete it if empty
491 108 50       243 if ($value eq '') {
492 108         367 $handler->set_attribute($node, $attr, undef);
493             }
494             }
495             }
496 322         816 return; # void?
497             }
498              
499             sub _clone_and_resolve {
500 107     107   212 my ($self, $node, $environment, @contents) = @_;
501 107         168 my $handler = $self->{'handler'};
502 107         292 my $clone = $handler->shallow_clone($node);
503 107         248 $self->_remove_structure_sigils($clone);
504 107         240 $self->_resolve_text_attr($clone, $environment);
505 107         298 $handler->push_contents($clone, @contents);
506 107         557 return $clone;
507             }
508              
509             sub _resolve_text_attr {
510 322     322   427 my ($self, $node, $environment) = @_;
511 322         457 my $handler = $self->{'handler'};
512 322         836 foreach my $attr ($handler->list_attributes($node)) {
513 21         59 my ($value, $did_change, $last_changing_quant) = $self->_process_text($handler->get_attribute($node, $attr), $environment, 1);
514 21 100       61 if ($did_change) {
515 15 100 100     59 if ($value eq '' and not $last_changing_quant eq '+') {
516             # We removed somethin from this attribute -> delete it if empty, unless the last sigil says otherwise
517 4         7 $value = undef;
518             }
519 15         50 $handler->set_attribute($node, $attr, $value);
520             }
521             }
522 322         563 return;
523             }
524             1;
525              
526             __END__