File Coverage

lib/OODoc/Template.pm
Criterion Covered Total %
statement 205 216 94.9
branch 122 156 78.2
condition 24 41 58.5
subroutine 25 26 96.1
pod 13 14 92.8
total 389 453 85.8


line stmt bran cond sub pod time code
1             # Copyrights 2003,2007-2021 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of perl distribution OODoc-Template. It is licensed under
6             # the same terms as Perl itself: https://spdx.org/licenses/Artistic-2.0.html
7              
8 10     10   743612 use strict;
  10         107  
  10         302  
9 10     10   63 use warnings;
  10         16  
  10         366  
10              
11             package OODoc::Template;
12 10     10   48 use vars '$VERSION';
  10         15  
  10         725  
13             $VERSION = '0.17';
14              
15              
16 10     10   5370 use Log::Report 'oodoc-template';
  10         1224053  
  10         64  
17              
18 10     10   2703 use IO::File ();
  10         22  
  10         133  
19 10     10   44 use File::Spec ();
  10         31  
  10         240  
20 10     10   6938 use Data::Dumper qw(Dumper);
  10         63619  
  10         733  
21 10     10   84 use Scalar::Util qw(weaken);
  10         18  
  10         30652  
22              
23             my @default_markers = ('', '');
24              
25              
26             sub new(@)
27 11     11 1 3936 { my ($class, %args) = @_;
28 11         66 (bless {}, $class)->init(\%args);
29             }
30              
31             sub init($)
32 11     11 0 30 { my ($self, $args) = @_;
33              
34 11         61 $self->{cached} = {};
35 11         32 $self->{macros} = {};
36              
37 11         22 my $s = $self; weaken $s; # avoid circular ref
  11         86  
38 11   50 7   113 $args->{template} ||= sub { $s->includeTemplate(@_) };
  7         24  
39 11   50 1   172 $args->{macro} ||= sub { $s->defineMacro(@_) };
  1         4  
40              
41 11   100     59 $args->{search} ||= '.';
42 11   100     95 $args->{markers} ||= \@default_markers;
43 11   50 2   81 $args->{define} ||= sub { shift; (1, @_) };
  2         3  
  2         10  
44              
45 11         42 $self->pushValues($args);
46 11         43 $self;
47             }
48              
49              
50             sub process($)
51 111     111 1 104104 { my ($self, $templ) = (shift, shift);
52              
53 111 100       388 my $values = @_==1 ? shift : @_ ? {@_} : {};
    100          
54              
55 111 100       486 my $tree # parse with real copy
    100          
56             = ref $templ eq 'SCALAR' ? $self->parseTemplate($$templ)
57             : ref $templ eq 'ARRAY' ? $templ
58             : $self->parseTemplate("$templ");
59              
60 111 50       223 defined $tree
61             or return ();
62              
63 111 100       375 $self->pushValues($values)
64             if keys %$values;
65              
66 111         138 my @output;
67 111         185 foreach my $node (@$tree)
68 337 100       569 { unless(ref $node)
69 224         321 { push @output, $node;
70 224         294 next;
71             }
72            
73 113         234 my ($tag, $attr, $then, $else) = @$node;
74              
75 113         130 my %attrs;
76 113         330 while(my($k, $v) = each %$attr)
77             { $attrs{$k} = ref $v ne 'ARRAY' ? $v
78 5         14 : @$v==1 ? scalar $self->valueFor(@{$v->[0]})
79             : join '',
80 53 100       239 map {ref $_ eq 'ARRAY' ? scalar $self->valueFor(@$_) : $_}
  17 100       91  
    100          
81             @$v;
82             }
83              
84 113         275 (my $value, my $attrs, $then, $else)
85             = $self->valueFor($tag, \%attrs, $then, $else);
86              
87 113 100 100     664 unless(defined $then || defined $else)
88 74 100       141 { defined $value
89             or next;
90              
91 60 50 33     213 ref $value ne 'ARRAY' && ref $value ne 'HASH'
92             or error __x"value for {tag} is {value}, must be single"
93             , tag => $tag, value => $value;
94              
95 60         87 push @output, $value;
96 60         127 next;
97             }
98              
99 39   100     170 my $take_else
100             = !defined $value || (ref $value eq 'ARRAY' && @$value==0);
101              
102 39 100       70 my $container = $take_else ? $else : $then;
103              
104 39 100       85 defined $container
105             or next;
106              
107 27 100       77 $self->pushValues($attrs) if keys %$attrs;
108              
109 27 100       90 if($take_else)
    100          
    100          
110 6         11 { my ($nest_out, $nest_tree) = $self->process($container);
111 6         12 push @output, $nest_out;
112 6         8 $node->[3] = $nest_tree;
113             }
114             elsif(ref $value eq 'HASH')
115 3         9 { my ($nest_out, $nest_tree) = $self->process($container, $value);
116 3         5 push @output, $nest_out;
117 3         8 $node->[2] = $nest_tree;
118             }
119             elsif(ref $value eq 'ARRAY')
120 11         22 { foreach my $data (@$value)
121 16         78 { my ($nest_out, $nest_tree) = $self->process($container, $data);
122 16         25 push @output, $nest_out;
123 16         42 $node->[2] = $nest_tree;
124             }
125             }
126             else
127 7         25 { my ($nest_out, $nest_tree) = $self->process($container);
128 7         14 push @output, $nest_out;
129 7         39 $node->[2] = $nest_tree;
130             }
131              
132 27 100       86 $self->popValues if keys %$attrs;
133             }
134            
135 111 100       275 $self->popValues if keys %$values;
136              
137 111 100       559 wantarray ? (join('', @output), $tree) # LIST context
    100          
138             : defined wantarray ? join('', @output) # SCALAR context
139             : print @output; # VOID context
140             }
141              
142              
143             sub processFile($;@)
144 6     6 1 12 { my ($self, $filename) = (shift, shift);
145              
146 6 50       11 my $values = @_==1 ? shift : {@_};
147 6   66     24 $values->{source} ||= $filename;
148              
149 6         8 my $cache = $self->{cached};
150              
151 6         7 my ($output, $tree, $template);
152 6 100       14 if(exists $cache->{$filename})
    100          
153 4         24 { $tree = $cache->{$filename};
154 4 50       16 $output = $self->process($tree, $values)
155             if defined $tree;
156             }
157             elsif($template = $self->loadFile($filename))
158 1         128 { ($output, $tree) = $self->process($template, $values);
159 1         5 $cache->{$filename} = $tree;
160             }
161             else
162 1         5 { $tree = $cache->{$filename} = undef;
163             }
164              
165 6 50 66     19 defined $tree || defined wantarray
166             or error __x"cannot find template file {fn}", fn => $filename;
167              
168 6 50       19 wantarray ? ($output, $tree) # LIST context
    50          
169             : defined wantarray ? $output # SCALAR context
170             : print $output; # VOID context
171             }
172              
173              
174             sub defineMacro($$$$)
175 1     1 1 3 { my ($self, $tag, $attrs, $then, $else) = @_;
176             my $name = delete $attrs->{name}
177 1 50       6 or error __x"macro requires a name";
178              
179 1 50       5 defined $else
180             and error __x"macros cannot have an else part ({macro})",macro => $name;
181              
182 1         4 my %attrs = %$attrs; # for closure
183 1         4 $attrs{markers} = $self->valueFor('markers');
184              
185             $self->{macros}{$name} =
186 2     2   6 sub { my ($tag, $at) = @_;
187 2         26 $self->process($then, +{%attrs, %$at});
188 1         8 };
189              
190 1         4 ();
191            
192             }
193              
194              
195             sub valueFor($;$$$)
196 235     235 1 435 { my ($self, $tag, $attrs, $then, $else) = @_;
197              
198             #warn "Looking for $tag";
199             #warn Dumper $self->{values};
200 235         530 for(my $set = $self->{values}; defined $set; $set = $set->{NEXT})
201 283         789 { my $v = $set->{$tag};
202              
203 283 100       433 if(defined $v)
204             { # HASH defines container
205             # ARRAY defines container loop
206             # object or other things can be stored as well, but may get
207             # stringified.
208 211 100       755 return wantarray ? ($v, $attrs, $then, $else) : $v
    100          
209             if ref $v ne 'CODE';
210              
211             return wantarray
212 33 100       119 ? $v->($tag, $attrs, $then, $else)
213             : ($v->($tag, $attrs, $then, $else))[0]
214             }
215              
216             return wantarray ? (undef, $attrs, $then, $else) : undef
217 72 50       156 if exists $set->{$tag};
    100          
218              
219 68         81 my $code = $set->{DYNAMIC};
220 68 50       168 if(defined $code)
221 0         0 { my ($value, @other) = $code->($tag, $attrs, $then, $else);
222 0 0       0 return wantarray ? ($value, @other) : $value
    0          
223             if defined $value;
224             # and continue the search otherwise
225             }
226             }
227              
228 20 50       61 wantarray ? (undef, $attrs, $then, $else) : undef;
229             }
230              
231              
232             sub allValuesFor($;$$$)
233 2     2 1 4 { my ($self, $tag, $attrs, $then, $else) = @_;
234 2         3 my @values;
235              
236 2         6 for(my $set = $self->{values}; defined $set; $set = $set->{NEXT})
237             {
238 4 100       9 if(defined(my $v = $set->{$tag}))
239 2 50       6 { my $t = ref $v eq 'CODE' ? $v->($tag, $attrs, $then, $else) : $v;
240 2 50       6 push @values, $t if defined $t;
241             }
242              
243 4 50       10 if(defined(my $code = $set->{DYNAMIC}))
244 0         0 { my $t = $code->($tag, $attrs, $then, $else);
245 0 0       0 push @values, $t if defined $t;
246             }
247             }
248              
249 2         5 @values;
250             }
251              
252              
253             sub pushValues($)
254 68     68 1 125 { my ($self, $attrs) = @_;
255              
256 68 100       150 if(my $markers = $attrs->{markers})
257             { my @markers = ref $markers eq 'ARRAY' ? @$markers
258 14 100       83 : map {s/\\\,//g; $_} split /(?!<\\)\,\s*/, $markers;
  2         4  
  2         5  
259              
260 14 100       54 push @markers, $markers[0] . '/'
261             if @markers==2;
262              
263 14 100       43 push @markers, $markers[1]
264             if @markers==3;
265              
266             $attrs->{markers}
267 14 100       34 = [ map { ref $_ eq 'Regexp' ? $_ : qr/\Q$_/ } @markers ];
  56         511  
268             }
269              
270 68 100       144 if(my $search = $attrs->{search})
271 11 50       63 { $attrs->{search} = [ split /\:/, $search ]
272             if ref $search ne 'ARRAY';
273             }
274              
275 68         291 $self->{values} = { %$attrs, NEXT => $self->{values} };
276             }
277              
278              
279             sub popValues()
280 57     57 1 73 { my $self = shift;
281 57         154 $self->{values} = $self->{values}{NEXT};
282             }
283              
284              
285             sub includeTemplate($$$)
286 7     7 1 14 { my ($self, $tag, $attrs, $then, $else) = @_;
287              
288 7 50 33     26 defined $then || defined $else
289             and error __x"template is not a container";
290              
291 7 100       14 if(my $fn = $attrs->{file})
292 5         12 { my $output = $self->processFile($fn, $attrs);
293             $output = $self->processFile($attrs->{alt}, $attrs)
294 5 50 66     15 if !defined $output && $attrs->{alt};
295              
296 5 50       20 defined $output
297             or error __x"cannot find template file {fn}", fn => $fn;
298              
299 5         18 return ($output);
300             }
301              
302 2 50       5 if(my $name = $attrs->{macro})
303 2 50       6 { my $macro = $self->{macros}{$name}
304             or error __x"cannot find macro {name}", name => $name;
305              
306 2         5 return $macro->($tag, $attrs, $then, $else);
307             }
308              
309 0   0     0 error __x"file or macro attribute required for template in {source}"
310             , source => $self->valueFor('source') || '??';
311             }
312              
313              
314             sub loadFile($)
315 2     2 1 4 { my ($self, $relfn) = @_;
316 2         3 my $absfn;
317              
318 2 50       33 if(File::Spec->file_name_is_absolute($relfn))
319 0         0 { my $fn = File::Spec->canonpath($relfn);
320 0 0       0 $absfn = $fn if -f $fn;
321             }
322              
323 2 50       7 unless($absfn)
324 2         7 { my @srcs = map { @$_ } $self->allValuesFor('search');
  2         8  
325 2         3 foreach my $dir (@srcs)
326 3         125 { $absfn = File::Spec->rel2abs($relfn, $dir);
327 3 100       89 last if -f $absfn;
328 2         9 $absfn = undef;
329             }
330             }
331              
332 2 100       9 defined $absfn
333             or return undef;
334              
335 1         11 my $in = IO::File->new($absfn, 'r');
336 1 50       148 unless(defined $in)
337 0   0     0 { my $source = $self->valueFor('source') || '??';
338 0         0 fault __x"Cannot read from {fn} in {file}", fn => $absfn, file=>$source;
339             }
340              
341 1         35 \(join '', $in->getlines); # auto-close in
342             }
343              
344              
345             sub parse($@)
346 0     0 1 0 { my ($self, $template) = (shift, shift);
347 0         0 $self->process(\$template, @_);
348             }
349              
350              
351             sub parseTemplate($)
352 105     105 1 187 { my ($self, $template) = @_;
353              
354 105 50       245 defined $template
355             or return undef;
356              
357 105         234 my $markers = $self->valueFor('markers');
358              
359             # Remove white-space escapes
360 105         1301 $template =~ s! \\ (?: \s* (?: \\ \s*)? \n)+
361             (?: \s* (?= $markers->[0] | $markers->[3] ))?
362             !!mgx;
363              
364 105         171 my @frags;
365              
366             # NOT_$tag supported for backwards compat
367 105         2185 while( $template =~ s!^(.*?) # text before container
368             $markers->[0] \s*
369             (?: IF \s* )?
370             (NOT (?:_|\s+) )?
371             ([\w.-]+) \s* # tag
372             (.*?) \s* # attributes
373             $markers->[1]
374             !!xs
375             )
376 109         384 { push @frags, $1;
377 109         330 my ($not, $tag, $attr) = ($2, $3, $4);
378 109         145 my ($then, $else);
379              
380 109 100       1437 if($template =~ s! (.*?) # contained
381             ( $markers->[2]
382             \s* \Q$tag\E \s* # "our" tag
383             $markers->[3]
384             )
385             !!xs)
386 40         89 { $then = $1;
387 40         71 my $endline = $2;
388             }
389              
390 109 100       618 if($not) { ($then, $else) = (undef, $then) }
  5 100       11  
    100          
391             elsif(!defined $then) { }
392             elsif($then =~ s! $markers->[0]
393             \s* ELSE (?:_|\s+)
394             \Q$tag\E \s*
395             $markers->[1]
396             (.*)
397             !!xs)
398             { # ELSE_$tag for backwards compat
399 5         10 $else = $1;
400             }
401              
402 109         258 push @frags, [$tag, $self->parseAttrs($attr), $then, $else];
403             }
404              
405 105         204 push @frags, $template;
406 105         212 \@frags;
407             }
408              
409              
410             sub parseAttrs($)
411 113     113 1 198 { my ($self, $string) = @_;
412              
413 113         132 my %attrs;
414 113         359 while( $string =~
415             s!^\s* (?: '([^']+)' # attribute name (might be quoted)
416             | "([^"]+)"
417             | (\w+)
418             )
419             \s* (?: \= \>? \s* # an optional value
420             ( \"[^"]*\" # dquoted value
421             | \'[^']*\' # squoted value
422             | \$\{ [^}]+ \} # complex variable
423             | [^\s,]+ # unquoted value
424             )
425             )?
426             \s* \,? # optionally separated by commas
427             !!xs)
428 57   33     363 { my ($k, $v) = ($1||$2||$3, $4);
429 57 100       106 unless(defined $v)
430 11         25 { $attrs{$k} = 1;
431 11         33 next;
432             }
433              
434 46 100       132 if($v =~ m/^\'(.*)\'$/)
435             { # Single quoted parameter, no interpolation
436 11         36 $attrs{$k} = $1;
437 11         34 next;
438             }
439              
440 35         85 $v =~ s/^\"(.*)\"$/$1/;
441 35         147 my @v = split /( \$\{[^\}]+\} | \$\w+ )/x, $v;
442              
443 35 100 66     134 if(@v==1 && $v[0] !~ m/^\$/)
444 23         55 { $attrs{$k} = $v[0];
445 23         94 next;
446             }
447              
448 12         15 my @steps;
449 12         21 foreach (@v)
450 36 100       111 { if( m/^ (?: \$(\w+) | \$\{ (\w+) \s* \} ) $/x )
    100          
451 12         45 { push @steps, [ $+ ];
452             }
453             elsif( m/^ \$\{ (\w+) \s* ([^\}]+? \s* ) \} $/x )
454 4         14 { push @steps, [ $1, $self->parseAttrs($2) ];
455             }
456             else
457 20 100       51 { push @steps, $_ if length $_;
458             }
459             }
460              
461 12         60 $attrs{$k} = \@steps;
462             }
463              
464 113 50       208 error __x"attribute error in '{tag}'", tag => $_[1]
465             if length $string;
466              
467 113         826 \%attrs;
468             }
469              
470              
471             1;