File Coverage

blib/lib/Positron/DataTemplate.pm
Criterion Covered Total %
statement 279 299 93.3
branch 142 162 87.6
condition 40 69 57.9
subroutine 14 14 100.0
pod 0 3 0.0
total 475 547 86.8


line stmt bran cond sub pod time code
1             package Positron::DataTemplate;
2             our $VERSION = 'v0.1.3'; # VERSION
3              
4             =head1 NAME
5              
6             Positron::DataTemplate - templating plain data to plain data
7              
8             =head1 VERSION
9              
10             version v0.1.3
11              
12             =head1 SYNOPSIS
13              
14             my $engine = Positron::DataTemplate->new();
15             my $template = { contents => ['@list', '$title'] };
16             my $data = { list => [
17             { title => 'first title', url => '/first-title.html' },
18             { title => 'second title', url => '/second-title.html' },
19             ] };
20             my $result = $engine->process($template, $data);
21             # { contents => [ 'first title', 'second title' ] }
22              
23             =head1 DESCRIPTION
24              
25             C is a templating engine. Unlike most templating engines,
26             though, it does not work on text, but on raw data: the template is (typically)
27             a hash or array reference, and the result is one, too.
28              
29             This module rose from a script that regularly produced HTML snippets on disk,
30             using regular, text-based templates. Each use case used the same data, but a different
31             template. For one use case, however, the output was needed in
32             JSON format, not HTML. One solution would have been to use the text-based
33             templating system to produce a valid JSON document (quite risky). The other solution,
34             which was taken at that time, was to transform the input data into the desired
35             output structure in code, and use a JSON serializer on that, bypassing the template
36             output.
37              
38             The third solution would have been to provide a template that did not directly
39             produce the serialised JSON text, but described the data structure transformation
40             in an on-disc format. By working only with structured data, and never with text,
41             the serialized output must always be valid JSON.
42              
43             This (minus the serialization) is the domain of C.
44              
45             =head1 EXAMPLES
46              
47             This code is still being worked on. This includes the documentation. In the meanwhile,
48             please use the following examples (and some trial & error) to gain a first look.
49             Alternatively, if you have access to the tests of this distribution, these also
50             give some examples.
51              
52             =head2 Text replacement
53              
54             [ '$one', '{$two}', 'and {$three}' ] + { one => 1, two => 2, three => 3 }
55             -> [ '1', '2', 'and 3' ]
56              
57             =head2 Direct inclusion
58              
59             [ '&this', '&that' ] + { this => [1, 2], that => { 3 => 4 } }
60             -> [ [1, 2], { 3 => 4} ]
61              
62             =head2 Loops
63              
64             { titles => ['@list', '{$id}: {$title}'] }
65             + { list => [ { id => 1, title => 'one' }, { id => 2, title => 'two' } ] }
66             -> { titles => [ '1: one', '2: two' ] }
67              
68             =head2 Conditions
69              
70             { checked => ['?active', 'yes', 'no] } + { active => 1 }
71             -> { checked => 'yes' }
72              
73             =head2 Interpolation (works with a lot of constructs)
74              
75             [1, '&list', 4] + { list => [2, 3] }
76             -> [1, [2, 3], 4]
77             [1, '&-list', 4] + { list => [2, 3] }
78             -> [1, 2, 3, 4]
79             [1, '<', '&list', 4] + { list => [2, 3] }
80             -> [1, 2, 3, 4]
81              
82             { '< 1' => { a => 'b' }, '< 2' => { c => 'd', e => 'f' }
83             -> { a => 'b', c => 'd', e => 'f' }
84             { '< 1' => '&hash', two => 2 } + { hash => { one => 1 } }
85             -> { one => 1, two => 2 }
86              
87             =head2 Comments
88              
89             'this is {#not} a comment' -> 'this is a comment'
90             [1, '#comment', 2, 3] -> [1, 2, 3]
91             [1, '/comment', 2, 3] -> [1, 3]
92             [1, '//comment', 2, 3] -> [1]
93             { 1 => 2, '#3' => 4 } -> { 1 => 2, '' => 4 }
94             { 1 => 2, '/3' => 4 } -> { 1 => 2 }
95              
96             =head2 File inclusion (requires L and L)
97              
98             [1, '. "/tmp/data.json"', 3] + '{ key: "value"}'
99             -> [1, { key => 'value' }, 3]
100              
101             =head2 File wrapping (also requires L and L)
102              
103             [1, ': "/tmp/wrap.json"', { animal => 'dog' }, 3]
104             + '{ key: "value", contents: ":"}'
105             -> [1, { key => 'value', contents => { animal => 'dog' }, 3]
106              
107             =head2 Funtions on data
108              
109             [1, '^len', "abcde", 2] + { len => \&CORE::length }
110             -> [1, 5, 2]
111              
112             =head2 Assignment
113              
114             [1, '= title object.name', 'My {$title} and {$count}' ]
115             + { object => { name => 'Name', count => 10 } }
116             -> [1, 'My Name and']
117              
118             =head2 Escaping other constructs
119              
120             [ '~?cond', 'Talking about {{~}$templates}', '~.htaccess' ]
121             -> [ '?cond', 'Talking about {$templates}', '.htaccess' ]
122             =cut
123              
124 14     14   518557 use v5.10;
  14         62  
  14         669  
125 14     14   79 use strict;
  14         28  
  14         468  
126 14     14   104 use warnings;
  14         29  
  14         409  
127              
128 14     14   75 use Carp qw( croak );
  14         20  
  14         1142  
129 14     14   15375 use Data::Dump qw(dump);
  14         167183  
  14         1575  
130 14     14   10773 use Positron::Environment;
  14         36  
  14         582  
131 14     14   11450 use Positron::Expression;
  14         56  
  14         68720  
132              
133             sub new {
134             # Note: no Moose; we have no inheritance or attributes to speak of.
135 13     13 0 150 my ($class) = @_;
136 13         64 my $self = {
137             include_paths => ['.'],
138             };
139 13         58 return bless($self, $class);
140             }
141              
142             sub process {
143 130     130 0 537 my ($self, $template, $env) = @_;
144             # Returns (undef) in list context - is this correct?
145 130 100       356 return undef unless defined $template;
146 128         625 $env = Positron::Environment->new($env);
147 128         356 my ($return, $interpolate) = $self->_process($template, $env);
148             # $return may be an interpolating construct,
149             # which depends on the context here.
150 127 50 66     694 if (wantarray and $interpolate and ref($return) eq 'ARRAY') {
      33        
151 0         0 return @$return;
152             } else {
153 127         1227 return $return;
154             }
155             }
156              
157             sub _process {
158 677     677   1090 my ($self, $template, $env) = @_;
159 677 100       1727 if (not ref($template)) {
    100          
    50          
160 459         1007 return $self->_process_text($template, $env);
161             } elsif (ref($template) eq 'ARRAY') {
162 141         559 return $self->_process_array($template, $env);
163             } elsif (ref($template) eq 'HASH') {
164 77         348 return $self->_process_hash($template, $env);
165             }
166 0         0 return $template; # TODO: deep copy?
167             }
168              
169             sub _process_text {
170 459     459   662 my ($self, $template, $env) = @_;
171 459 100       914 return ($template, 0) unless $template; # undef, '', 0, or '0'
172 449         721 my $interpolate = 0;
173 449 100       3088 if ($template =~ m{ \A [&,] (-?) (.*) \z}xms) {
    100          
    100          
    100          
    100          
    100          
    100          
174 49 100       196 if ($1) { $interpolate = 1; }
  6         12  
175 49         83 my $expr = $2;
176 49 100       106 if ($expr eq ':') {
177             # Special case: internal wrap evaluation
178 3         14 my ($return, $i) = $self->_process($env->get(':'), $env);
179 3   33     16 $interpolate ||= $i;
180 3         8 return ($return, $interpolate);
181             } else {
182 46         147 return (Positron::Expression::evaluate($expr, $env), $interpolate);
183             }
184             } elsif ($template =~ m{ \A \$ (.*) \z}xms) {
185 70   100     217 my $value = Positron::Expression::evaluate($1, $env) // '';
186 70         261 return ("$value", 0);
187             } elsif ($template =~ m{ \A \x23 (\+?) }xms) {
188 4 100       17 return ('', ($1 ? 0 : 1));
189             } elsif ($template =~ m{ \A = \s* (\w+) \s+ (.*) }xms) {
190             # Always interpolates, the new identifier means nothing
191 1         5 Positron::Expression::evaluate($2, $env); # still perform it, means nothing
192 1         9 return ('', 1);
193             } elsif ($template =~ m{ \A ([.:]) (-?) \s* ([^\s-].*) }xms) {
194 9         25 my $filename_expr = $3;
195 9 100       31 if ($2) { $interpolate = 1; }
  1         2  
196 9         12 my $new_env = $env;
197 9 100       34 if ($1 eq ':') {
198             # A wrap in text context, explicitly unset ':'.
199 2         13 $new_env = Positron::Environment->new({ ':' => undef }, { parent => $env });
200             }
201 9         34 my $filename = Positron::Expression::evaluate($filename_expr, $new_env);
202 9         67 require JSON;
203 9         1077 require File::Slurp;
204 9         18596 my $json = JSON->new();
205 9         16 my $file = undef;
206 9         15 foreach my $path (@{$self->{include_paths}}) {
  9         24  
207 18 100       338 if (-f $path . $filename) {
208 9         27 $file = $path . $filename; # TODO: platform-independent chaining
209             }
210             }
211 9 50       26 if ($file) {
212 9         35 my $result = $json->decode(scalar(File::Slurp::read_file($file)));
213 9         878 my ($return, $i) = $self->_process($result, $new_env);
214 9   66     40 $interpolate ||= $i;
215 9         72 return ($return, $interpolate);
216             } else {
217 0         0 croak "Can't find template '$filename' in " . join(':', @{$self->{include_paths}});
  0         0  
218             }
219             } elsif ($template =~ m{ \A \: (-?) \s* \z }xms) {
220             # wrap evaluation
221 12 100       50 if ($1) { $interpolate = 1; }
  3         6  
222 12         46 my ($return, $i) = $self->_process($env->get(':'), $env);
223 12   100     46 $interpolate ||= $i;
224 12         34 return ($return, $interpolate);
225             } elsif ($template =~ m{ \A \^ (-?) \s* (.*) }xms) {
226             # Special non-list case, e.g. hash value (not key)
227             # cannot interpolate
228 3         13 my $function = Positron::Expression::evaluate($2, $env);
229 3         10 return (scalar($function->()), 0);
230             } else {
231 301         429 $template =~ s{
232             \{ \$ ([^\}]*) \}
233             }{
234 11   50     31 my $replacement = Positron::Expression::evaluate($1, $env) // '';
235 11         40 "$replacement";
236             }xmseg;
237 301         414 $template =~ s{
238             (\s*) \{ \x23 (-?) ([^\}]*) \} (\s*)
239             }{
240 3 100       15 $2 ? '' : $1 . $4;
241             }xmseg;
242             # At the very end: get rid of escaping tildes (one layer)
243 301         407 $template =~ s{ \A ~ }{}xms;
244 301         373 $template =~ s{ \{ ~ \} }{}xmsg;
245 301         978 return ($template, 0);
246             }
247             }
248              
249             sub _process_array {
250 167     167   391 my ($self, $template, $env) = @_;
251 167         208 my $interpolate = 0;
252 167 50       452 return ([], 0) unless @$template;
253 167         397 my @elements = @$template;
254 167 100       666 if ($elements[0] =~ m{ \A \@ (-?) (.*) \z}xms) {
    100          
255             # list iteration
256 13 100       49 if ($1) { $interpolate = 1; }
  5         12  
257 13         62 my $clause = $2;
258 13         23 shift @elements;
259 13         51 my $result = [];
260 13         53 my $list = Positron::Expression::evaluate($clause, $env);
261 13 50       58 if (not ref($list) eq 'ARRAY') {
262             # If it's not a list, make it a one-element list.
263             # Useful for forcing interpolation via '[@- ""]' or aliasing (to be introduced)
264 0         0 $list = [$list];
265             }
266 13         34 foreach my $el (@$list) {
267 26         169 my $new_env = Positron::Environment->new( $el, { parent => $env } );
268             # evaluate rest of list as array,
269 26         192 my ($return, undef) = $self->_process_array(\@elements, $new_env);
270             # and flatten
271 26         110 push @$result, @$return;
272             }
273 13         46 return ($result, $interpolate);
274             } elsif ($elements[0] =~ m{ \A \? (-?) (.*) \z}xms) {
275             # conditional
276 21 100       60 if ($1) { $interpolate = 1; }
  2         3  
277 21         30 my $clause = $2;
278 21         22 shift @elements;
279 21 100       48 my $has_else = (@elements > 1) ? 1 : 0;
280 21         58 my $cond = Positron::Expression::evaluate($clause, $env); # can be anything!
281             # for Positron, empty lists and hashes are false!
282 21         55 $cond = Positron::Expression::true($cond);
283 21 100 66     52 if (not $cond and not $has_else) {
284             # no else clause, return empty on false
285             # (please interpolate!)
286 1         4 return ('', 1);
287             }
288 20         25 my $then = shift @elements;
289 20         147 my $else = shift @elements;
290 20 100       40 my $result = $cond ? $then : $else;
291 20         44 my ($return, $i) = $self->_process($result, $env);
292 20   66     64 $interpolate ||= $i;
293 20         61 return ($return, $interpolate);
294             } else {
295 133         227 my $return = [];
296             # potential structural comments
297 133         231 my $skip_next = 0;
298 133         149 my $capturing_function = 0;
299 133         162 my $capturing_wrap = 0;
300 133         145 my $capturing_wrap_interpolates = 0;
301 133         143 my $interpolate_next = 0; # actual count
302 133         145 my $is_first_element = 1;
303 133         235 foreach my $element (@elements) {
304 342 100       2681 if ($element =~ m{ \A // (-?) }xms) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
305 4 100 66     20 if ($is_first_element and $1) { $interpolate = 1; }
  1         1  
306 4         5 last; # nothing more
307             } elsif ($element =~ m{ \A / (-?) }xms) {
308 3 100 66     15 if ($is_first_element and $1) { $interpolate = 1; }
  1         1  
309 3         4 $skip_next = 1;
310             } elsif ($skip_next) {
311 3         4 $skip_next = 0;
312             } elsif ($element =~ m{ \A \^ (-?) \s* ([^\s-].*) }xms) {
313 6 100 66     31 if ($is_first_element and $1) { $interpolate = 1; }
  1         1  
314 6         32 $capturing_function = Positron::Expression::evaluate($2, $env);
315             # do not push!
316             } elsif ($element =~ m{ \A \: (-?) \s* ([^\s-].*) }xms) {
317 11 100       40 $capturing_wrap_interpolates = $1 ? 1 : 0;
318 11         48 my $filename = Positron::Expression::evaluate($2, $env);
319 11 50       34 if (!$filename) {
320 0         0 warn "# no filename in expression '$element'?";
321             }
322 11         95 require JSON;
323 11         1029 require File::Slurp;
324 11         15171 my $json = JSON->new();
325 11         24 my $file = undef;
326 11         22 foreach my $path (@{$self->{include_paths}}) {
  11         39  
327 22 100       482 if (-f $path . $filename) {
328 11         41 $file = $path . $filename; # TODO: platform-independent chaining
329             }
330             }
331 11 50       40 if ($file) {
332 11         54 my $contents = File::Slurp::read_file($file);
333 11         1486 $capturing_wrap = $json->decode($contents);
334             } else {
335 0         0 croak "Can't find template '$filename' in " . join(':', @{$self->{include_paths}});
  0         0  
336             }
337             # do not push!
338             } elsif ($element =~ m{ \A = (-?) \s* (\w+) \s+ (.*) }xms) {
339 9 100 66     39 if ($is_first_element and $1) { $interpolate = 1; }
  3         4  
340 9         17 my $new_key = $2;
341 9         36 my $new_value = Positron::Expression::evaluate($3, $env);
342             # We change env here!
343 9         44 $env = Positron::Environment->new({}, { parent => $env });
344 9         37 $env->set($new_key, $new_value); # Handles '_' on either side
345             } elsif ($capturing_function) {
346             # we have a capturing function waiting for input
347 5         18 my ($arg, $i) = $self->_process($element, $env);
348             # interpolate: could be ['@- ""', arg1, arg2]
349 5 50 33     28 if (ref($arg) eq 'ARRAY' and $i) {
    50 33        
350 0         0 push @$return, $capturing_function->(@$arg);
351             } elsif (ref($arg) eq 'HASH' and $i) {
352 0         0 push @$return, $capturing_function->(%$arg);
353             } else {
354 5         13 push @$return, $capturing_function->($arg);
355             }
356             # no more waiting function
357 5         32 $capturing_function = 0;
358             } elsif ($capturing_wrap) {
359             # we have a capturing wrap file waiting for input
360             # Note: neither the wrap nor the element have been evaluated yet!
361 11         119 my $new_env = Positron::Environment->new({ ':' => $element }, { parent => $env });
362 11         47 my ($result, $i) = $self->_process($capturing_wrap, $new_env);
363 11   66     46 $i ||= $capturing_wrap_interpolates;
364             # interpolate: could be ['@- ""', arg1, arg2]
365             # or [1, ':- file', 'contents', 2]
366 11 100 100     75 if (ref($result) eq 'ARRAY' and $i) {
    50 66        
367 6         32 push @$return, @$result;
368             } elsif (ref($result) eq 'HASH' and $i) {
369 0         0 push @$return, %$result;
370             } else {
371 5         9 push @$return, $result;
372             }
373             # no more waiting wrap
374 11         57 $capturing_wrap = 0;
375             } elsif ($element =~ m{ \A < }xms) {
376 13         42 $interpolate_next += 1; # actual count
377             } else {
378 277         655 my ($result, $interpolate_me) = $self->_process($element, $env);
379 277         554 my @results = ($result);
380 277         375 $interpolate_next += $interpolate_me;
381 277   66     851 while ($interpolate_next > 0 and @results) {
382 38 100 50     129 if (ref($results[0]) eq 'ARRAY') {
    100          
383 32         53 my $array = shift @results;
384 32         88 unshift @results, @$array;
385             } elsif (($results[0] // '') eq '') {
386             # Note: the empty string, if it wants to interpolate, becomes the empty list
387             # i.e. just drop it.
388 3         6 shift @results;
389             } else {
390 3         6 last; # conditions can't match any more
391             }
392 35         129 $interpolate_next--;
393             }
394 277         544 $interpolate_next = 0;
395 277         772 push @$return, @results;
396             }
397 338         736 $is_first_element = 0; # not anymore
398             }
399 133 100       319 if ($capturing_function) {
400             # Oh no, a function waiting for args?
401 1         4 push @$return, $capturing_function->();
402             }
403 133 50       267 if ($capturing_wrap) {
404             # Oh no, a wrap waiting for args?
405 0         0 my $new_env = Positron::Environment->new({ ':' => undef }, { parent => $env });
406 0         0 my ($result, $i) = $self->_process($capturing_wrap, $new_env);
407 0 0 0     0 if (ref($result) eq 'ARRAY' and $i) {
    0 0        
408 0         0 push @$return, @$result;
409             } elsif (ref($result) eq 'HASH' and $i) {
410 0         0 push @$return, %$result;
411             } else {
412 0         0 push @$return, $result;
413             }
414             }
415 133         469 return ($return, $interpolate);
416             }
417             }
418             sub _process_hash {
419 77     77   116 my ($self, $template, $env) = @_;
420 77 100       196 return ({}, 0) unless %$template;
421 76         137 my %result = ();
422 76         96 my $hash_construct = undef;
423 76         90 my $switch_construct = undef;
424 76         215 foreach my $key (keys %$template) {
425 119 100       560 if ($key =~ m{ \A \% (.*) \z }xms) {
    100          
426 2         7 $hash_construct = [$key, $1]; last;
  2         5  
427             } elsif ($key =~ m{ \A \| (.*) \z }xms) {
428             # basically auto-interpolates
429 8         26 $switch_construct = [$key, $1]; last;
  8         17  
430             }
431             }
432 76 100       247 if ($hash_construct) {
    100          
433 2         11 my $e_content = Positron::Expression::evaluate($hash_construct->[1], $env);
434 2 50       6 croak "Error: result of expression '".$hash_construct->[1]."' must be hash" unless ref($e_content) eq 'HASH';
435 2         14 while (my ($key, $value) = each %$e_content) {
436 4         21 my $new_env = Positron::Environment->new( { key => $key, value => $value }, { parent => $env } );
437 4         16 my ($t_content, undef) = $self->_process( $template->{$hash_construct->[0]}, $new_env);
438 4 50       11 croak "Error: content of % construct must be hash" unless ref($t_content) eq 'HASH';
439             # copy into result (automatically interpolates)
440 4         12 foreach my $k (keys %$t_content) {
441 6         38 $result{$k} = $t_content->{$k};
442             }
443             }
444             } elsif ($switch_construct) {
445 8         33 my $e_content = Positron::Expression::evaluate($switch_construct->[1], $env); # The switch key
446             # escape the '|' by adding another one!
447 8 50 66     37 my $qe_content = ( defined $e_content and $e_content =~m{ \A \|}xms ) ? "|$e_content" : $e_content;
448 8 100 66     42 if (defined $e_content and exists $template->{$switch_construct->[0]}->{$qe_content}) {
    100          
449             # We have no interpolation of our own, just pass the below up.
450 4         16 return $self->_process($template->{$switch_construct->[0]}->{$qe_content}, $env);
451             } elsif (exists $template->{$switch_construct->[0]}->{'|'}) {
452 3         38 return $self->_process($template->{$switch_construct->[0]}->{'|'}, $env);
453             } else {
454 1         5 return ('', 1);
455             }
456             } else {
457             # simple copy
458             # '<': find first, and interpolate
459             # do by sorting keys alphabetically
460             my @keys = sort {
461 66 100       264 if($a =~ m{ \A < }xms) {
  50         133  
462 3 50       19 if ($b =~ m{ \A < }xms) {
463 0         0 return $a cmp $b;
464             } else {
465 3         12 return -1;
466             }
467             } else {
468 47 100       114 if ($b =~ m{ \A < }xms) {
469 3         15 return 1;
470             } else {
471 44         148 return $a cmp $b;
472             }
473             }
474             } keys %$template;
475 66         196 foreach my $key (@keys) {
476 109         201 my $value = $template->{$key};
477 109 100       284 if ($key =~ m{ \A < }xms) {
478             # interpolate
479 6         25 my ($values, $interpolate) = $self->_process($value, $env);
480 6         75 %result = (%result, %$values);
481 6         20 next;
482             }
483 103 100       235 if ($key =~ m{ \A / }xms) {
484             # structural comment
485 1         2 next;
486             }
487 102 100       327 if ($value =~ m{ \A / }xms) {
488             # structural comment (forbidden on values)
489 1         36 croak "Cannot comment out a value";
490             }
491 101 100       234 if ($key =~ m{ \A \^ \s* (.*)}xms) {
492             # consuming function call (interpolates)
493 1         4 my $func = Positron::Expression::evaluate($1, $env);
494 1         4 my ($value_in, undef) = $self->_process($value, $env);
495 1         5 my $hash_out = $func->($value_in);
496             # interpolate
497 1         11 foreach my $k (keys %$hash_out) {
498 1         5 $result{$k} = $hash_out->{$k};
499             }
500 1         4 next;
501             }
502 100 100       272 if ($key =~ m{ \A : (-?) \s* (.+) }xms) {
503             # consuming wrap (interpolates in any case)
504 2         3 my $capturing_wrap;
505 2         7 my $filename = Positron::Expression::evaluate($2, $env);
506 2         22 require JSON;
507 2         9 require File::Slurp;
508 2         11 my $json = JSON->new();
509 2         4 my $file = undef;
510 2         3 foreach my $path (@{$self->{include_paths}}) {
  2         6  
511 4 100       60 if (-f $path . $filename) {
512 2         7 $file = $path . $filename; # TODO: platform-independent chaining
513             }
514             }
515 2 50       5 if ($file) {
516 2         7 my $contents = File::Slurp::read_file($file);
517 2         166 $capturing_wrap = $json->decode($contents);
518             } else {
519 0         0 croak "Can't find template '$filename' in " . join(':', @{$self->{include_paths}});
  0         0  
520             }
521 2         22 my $new_env = Positron::Environment->new({ ':' => $value }, { parent => $env });
522 2         8 my ($hash_out, undef) = $self->_process($capturing_wrap, $new_env);
523             # interpolate
524 2         7 foreach my $k (keys %$hash_out) {
525 6         15 $result{$k} = $hash_out->{$k};
526             }
527 2         18 next;
528             }
529 98 100       255 if ($key =~ m{ \A = (-?) \s* (\w+) \s+ (.*) }xms) {
530             # assignment (always interpolates)
531 1         3 my $new_key = $2;
532 1         4 my $new_value = Positron::Expression::evaluate($3, $env);
533             # We change env here!
534 1         7 my $new_env = Positron::Environment->new({}, { parent => $env });
535 1         7 $new_env->set($new_key, $new_value); # Handles '_' on either side
536 1         3 my ($hash_out, undef) = $self->_process($value, $new_env);
537             # interpolate
538 1         4 foreach my $k (keys %$hash_out) {
539 1         3 $result{$k} = $hash_out->{$k};
540             }
541 1         5 next;
542             }
543 97 100       251 if ($key =~ m{ \A \? \s* (.*)}xms) {
544             # "conditional key", syntactic sugar that interpolates the hash
545             # Short for { '< 1' => ['?cond', { ... }, {}], ... }
546 2         7 my $cond = Positron::Expression::evaluate($1, $env);
547 2 100       5 if ($cond) {
548 1         3 my ($hash_out, undef) = $self->_process($value, $env);
549             # interpolate
550 1         4 foreach my $k (keys %$hash_out) {
551 1         3 $result{$k} = $hash_out->{$k};
552             }
553             } else {
554             # nothing!
555             }
556 2         5 next;
557             }
558 95         343 ($key, undef) = $self->_process($key, $env);
559 95         251 ($value, undef) = $self->_process($value, $env);
560 95         343 $result{$key} = $value;
561             }
562             }
563 67         428 return (\%result, 0);
564             }
565              
566             sub add_include_paths {
567 3     3 0 22 my ($self, @paths) = @_;
568 3         8 push @{$self->{'include_paths'}}, @paths;
  3         27  
569             }
570              
571             1; # End of Positron::DataTemplate
572              
573             __END__