File Coverage

lib/OODoc/Template.pm
Criterion Covered Total %
statement 199 210 94.7
branch 120 154 77.9
condition 24 44 54.5
subroutine 23 24 95.8
pod 13 14 92.8
total 379 446 84.9


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