File Coverage

blib/lib/HTML/Template/Plugin/Dot.pm
Criterion Covered Total %
statement 144 146 98.6
branch 81 96 84.3
condition 42 51 82.3
subroutine 12 12 100.0
pod n/a
total 279 305 91.4


line stmt bran cond sub pod time code
1             package HTML::Template::Plugin::Dot;
2 12     12   11188 use vars qw/$VERSION/;
  12         23  
  12         546  
3             $VERSION = '1.04';
4 12     12   59 use strict;
  12         26  
  12         271  
5              
6 12     12   57 use Carp;
  12         41  
  12         597  
7 12     12   5938 use Data::Dumper;
  12         65794  
  12         866  
8 12     12   5413 use Regexp::Common qw/ RE_balanced RE_delimited RE_num_real /;
  12         36580  
  12         51  
9 12     12   1611376 use Scalar::Util qw/blessed reftype/;
  12         25  
  12         813  
10 12     12   69 use base 'Exporter';
  12         25  
  12         1963  
11              
12             # prefetch regexps for speed
13             our $RE_balanced = RE_balanced();
14             our $RE_delimited = RE_delimited(-delim=>q{'"`});
15             our $RE_num_real = RE_num_real();
16              
17 12     12   76 use constant DEBUG => 0;
  12         29  
  12         23702  
18              
19             sub import {
20             # my $caller = scalar(caller);
21 12     12   201 HTML::Template::Pluggable->add_trigger('middle_param', \&_dot_notation);
22 12         7888 goto &Exporter::import;
23             }
24              
25             sub _dot_notation {
26 133     133   4693 my $self = shift;
27 133         174 my $options = $self->{options};
28 133         166 my $param_map = $self->{param_map};
29              
30             # @_ has already been setup for us by the time we're called.
31              
32 133         153 my %input;
33 133         284 for (my $x = 0; $x <= $#_; $x += 2) {
34 140 50       296 my $param = $options->{case_sensitive} ? $_[$x] : lc $_[$x];
35 140         201 my $value = $_[($x + 1)];
36 140         336 $input{$param} = $value;
37             }
38              
39 133         149 DEBUG and carp("dot_notation called for $_[0]");
40 133         134 DEBUG and carp("param map: ", Dumper($param_map));
41 133         140 DEBUG and carp("input: ", Dumper(\%input));
42              
43 133         396 while (my ($param, $value) = each %input) {
44             # necessary to cooperate with plugin system
45 140 50 66     379 next if ($self->{param_map_done}{$param} and not $self->{num_vars_left_in_loop});
46              
47 140         226 my ($exists,@dot_matches) = _exists_in_tmpl($self, $param);
48             # We don't have to worry about "die on bad params", because that will be handled
49             # by HTML::Template's param().
50 140         173 DEBUG and carp("exists: $exists, dot matches: @dot_matches, param: $param");
51 140 100       269 next unless $exists;
52              
53 136         227 my $value_type = ref($value);
54 136 100 66     318 if(@dot_matches) {
    100 33        
      66        
55 121         174 for my $dot_match (@dot_matches) {
56 172         325 my $value_for_tmpl = _param_to_tmpl($self,$dot_match,$param,$value, \%input);
57 166         219 my $dot_value_type = ref($value_for_tmpl);
58              
59 166 100 66     543 unless (defined($dot_value_type) and length($dot_value_type) and ($dot_value_type eq 'ARRAY'
      66        
      100        
60             or (ref($value_for_tmpl) and (ref($value_for_tmpl) !~ /^(CODE)|(HASH)|(SCALAR)$/) and $value_for_tmpl->isa('ARRAY')))) {
61 143 50       350 (ref($param_map->{$dot_match}) eq 'HTML::Template::VAR') or
62             croak("HTML::Template::param() : attempt to set parameter '$param' with a scalar - parameter is not a TMPL_VAR!");
63 143         158 ${$param_map->{$dot_match}} = $value_for_tmpl;
  143         251  
64             }
65             else {
66 23 50       46 (ref($param_map->{$dot_match}) eq 'HTML::Template::LOOP') or
67             croak("HTML::Template::param() : attempt to set parameter '$param' with an array ref - parameter is not a TMPL_LOOP!");
68 23         33 $param_map->{$dot_match}[HTML::Template::LOOP::PARAM_SET()] = $value_for_tmpl;
69             }
70              
71             # Necessary for plugin system compatibility
72 166         299 $self->{num_vars_left_in_loop} -= 1;
73 166         721 $self->{param_map_done}{$param} = $value; # store the object for future reference
74             }
75             }
76             # We still need to care about tmpl_loops that aren't dot matches so we can adjust their loops
77             elsif (defined($value_type) and length($value_type) and ($value_type eq 'ARRAY'
78             or ((ref($value) !~ /^(CODE)|(HASH)|(SCALAR)$/) and $value->isa('ARRAY')))) {
79 2 50       7 (ref($param_map->{$param}) eq 'HTML::Template::LOOP') or
80             croak("HTML::Template::param() : attempt to set parameter '$param' with an array ref - parameter is not a TMPL_LOOP!");
81              
82             # TODO: Use constant names instead of "0"
83 2 100       9 $self->{num_vars_left_in_loop} += keys %{ $param_map->{$param}[HTML::Template::LOOP::TEMPLATE_HASH()]{'0'}{'param_map'} } if exists $param_map->{$param}[HTML::Template::LOOP::TEMPLATE_HASH()]{'0'};
  1         8  
84              
85             }
86             else {
87 13 50       40 (ref($param_map->{$param}) eq 'HTML::Template::VAR') or
88             croak("HTML::Template::param() : attempt to set parameter '$param' with a scalar - parameter is not a TMPL_VAR!");
89             # intetionally /don't/ set the values for non-dot notation params,
90             # and don't mark them as done, just that they exist.
91             # but only if there actually are dot params in the template. if there aren't,
92             # this property needs to stay undefined (to fix test 6 in t/RT40714-1.t).
93 13 100       84 $self->{num_vars_left_in_loop} -= 1 if exists $self->{num_vars_left_in_loop};
94             }
95             }
96             }
97              
98             # Check to see if a param exists in the template, with support for dot notation
99             # returns an an array
100             # - bool for any matches
101             # - array of keys with dot notation that matched.
102             sub _exists_in_tmpl {
103 140     140   177 my $self = shift;
104 140         189 my ($param) = @_;
105 140         175 my $param_map = $self->{param_map};
106              
107 140 100       269 return 1 if exists $param_map->{$param};
108 43         84 return @{$self->{_seen_exists_in_tmpl}{$param}} if exists $self->{_seen_exists_in_tmpl}
109 125 100 100     283 and exists $self->{_seen_exists_in_tmpl}{$param};
110              
111 82 100       196 if (my @matching_dot_tokens = grep { /^$param\./ } keys %$param_map) {
  142         1891  
112 78         224 $self->{_seen_exists_in_tmpl}{$param} = [1, @matching_dot_tokens];
113 78         112 return @{$self->{_seen_exists_in_tmpl}{$param}};
  78         245  
114             }
115             else {
116 4         11 $self->{_seen_exists_in_tmpl}{$param} = [];
117 4         10 return;
118             }
119             }
120              
121             # =head2 _param_to_tmpl()
122             #
123             # my $result = _param_to_tmpl($pluggable,$tmpl_token_name,$param_name,$param_value);
124             #
125             # Returns the right thing to put in the template given a token name, a param name
126             # and a param value. Returns undef if this template token name and param name
127             # don't match.
128             #
129             # The template token name supports the dot notation, which means that method
130             # calls and nested hashes are expanded.
131             #
132             # However, first we check for a literal match, for backwards compatibility with
133             # HTML::Template.
134             #
135             # =cut
136              
137             sub _param_to_tmpl {
138 177     177   320 my ($self, $token_name, $param_name, $param_value, $input) = @_;
139              
140             # This clause may not be needed because the non-dot-notation
141             # cases are handled elsewhere.
142 177 100       284 if ($token_name eq $param_name) {
143 1         4 return $param_value;
144             }
145              
146 176         407 my ($one, $the_rest) = split /\./, $token_name, 2;
147              
148 176 50       308 length $the_rest or return undef;
149 176 50       278 $one eq $param_name or return undef;
150              
151 176         206 my $loopmap_name = 'this'; # default for mapping array elements for loop vars
152 176 100       373 $loopmap_name = $1 if $the_rest =~ s/\s*:\s*([_a-z]\w*)\s*$//;
153              
154             # Rhesa (Thu Aug 4 18:33:30 CEST 2005)
155             # Patch for mixing method calls and attribute access mixing,
156             # and optional parameter lists!
157             #
158             # First we're setting $ref to $param_value
159             #
160             # We're going to loop over $the_rest by finding anything that matches
161             # - a valid identifier $id ( [_a-z]\w* )
162             # - optionally followed by something resembling an argument list $data
163             # - optionally followed by a dot or $
164             # then we're checking if
165             # - $ref is an object
166             # - if we can call $id on it
167             # - in this case we further parse the argument list for strings
168             # or numbers or references to other h-t params
169             # - or if it's an attribute
170             # - or a hashref and we have no $data
171             # We'll use the result of that operation for $ref as long as there are dots
172             # followed by an identifier
173              
174 176         200 my $ref = $param_value;
175 176   66     546 $self->{param_map_done}{$one} ||= $ref;
176 176         286 my $want_loop = ref($self->{param_map}{$token_name}) eq 'HTML::Template::LOOP';
177 176         204 my(@results); # keeps return values from dot operations
178             THE_REST:
179 176         1825 while ($the_rest =~ s/^
180             ([_a-z]\w*) # an identifier
181             ($RE_balanced)? # optional param list
182             (?:\.|$) # dot or end of string
183             //xi ) {
184 207         592 my ($id, $data) = ($1, $2);
185 207 100       608 if (blessed($ref)) {
    100          
186 88 100 100     313 if ($ref->can($id) or ($ref->can('AUTOLOAD') && !$ref->isa("Test::MockObject"))) {
    50 100        
187 79         1223 my @args = ();
188 79 100       171 if ($data) {
189 36 50       192 $data =~ s/^\(// and $data =~ s/\)$//;
190 36         81 while ($data) {
191 52 100       1428 if ($data =~ s/
    50          
192             ^\s*
193             (
194             $RE_delimited # a string
195             |
196             $RE_num_real # or a number
197             )
198             (?:,\s*)?
199             //xi
200             ) {
201 40         92 my $m = $1;
202 40         89 $m =~ s/^["'`]//; $m =~ s/["'`]$//;
  40         99  
203 40         126 push @args, $m;
204             }
205             elsif ($data =~ s/
206             ^\s*
207             ( # ($1) a sub-expression of the form "object.method(args)"
208             ([_a-z]\w*) # ($2) the object in question
209             (?:
210             \.
211             [_a-z]\w* # method name
212             $RE_balanced? # optional argument list
213             )*
214             )
215             (?:,\s*)?
216             //xi
217             ) {
218 12         51 my ($m, $o) = ($1, $2);
219 12         16 DEBUG and carp("found subexpression '$m' with '$o'");
220 12         16 DEBUG and carp Dumper($self->{param_map}), Dumper($self->{param_map_done});
221 12 100       49 if (exists($self->{param_map}{$m})) {
    100          
    100          
222 5 100       51 my $prev = exists $input->{$m} ? $input->{$m} : $self->param($m);
223 5         12 DEBUG and carp("found '$prev' for '$m' in param_map");
224 5         16 push @args, $prev;
225             }
226             elsif (exists($self->{param_map_done}{$o})) {
227 4         30 my $prev = _param_to_tmpl($self, $m, $o, $self->{param_map_done}{$o}, $input);
228 4         8 DEBUG and carp("found '$prev' for '$o' in param_map_done");
229 4         16 push @args, $prev;
230             }
231             elsif (exists($input->{$o})) {
232 1         4 $self->{param_map}{$o} = HTML::Template::VAR->new();
233 1         12 my $prev = _param_to_tmpl($self, $m, $o, $input->{$o}, $input);
234 1         2 DEBUG and carp("found '$prev' through recursion");
235 1         4 push @args, $prev;
236             }
237             else {
238 2         225 croak("Attempt to reference nonexisting parameter '$m' in argument list to '$id' in dot expression '$token_name': $m is not a TMPL_VAR!");
239             }
240             }
241             else {
242 0         0 last;
243             }
244             }
245 34 50       69 croak("Bare word '$data' not allowed in argument list to '$id' in dot expression '$token_name'") if $data;
246             }
247 77         109 eval {
248 77 100 100     262 if($the_rest or !$want_loop) {
249 70         132 $one .= ".$id";
250 70         406 $ref = $ref->$id(@args);
251 66   100     10075 $self->{param_map_done}{$one} ||= $ref;
252             } else {
253 7         34 @results = $ref->$id(@args);
254             }
255             };
256 77 100       1474 if($@) {
257 4 100       17 if( $self->{options}{die_on_bad_params} ) {
258 2         260 croak("Error invoking $ref->$id(@args): $@");
259             } else {
260 2         184 carp("Error invoking $ref->$id(@args): $@");
261 2         127 @results = ();
262 2         6 $ref = $self->{param_map_done}{$one} = '';
263 2         3 $the_rest = '';
264 2         5 last THE_REST;
265             }
266             }
267             }
268             elsif(reftype($ref) eq 'HASH') {
269 9 100       622 croak("Can't access hash key '$id' with a parameter list! ($data)") if $data;
270              
271 8 100 66     39 if($the_rest or !$want_loop) {
272 7 100       64 $ref = exists( $ref->{$id} ) ? $ref->{$id} : undef;
273             } else {
274 1 50       7 @results = exists( $ref->{$id} ) ? $ref->{$id} : ();
275             }
276             }
277             else {
278 0         0 croak("Don't know what to do with reference '$ref', identifier '$id' and data '$data', giving up.");
279             }
280             }
281             elsif(ref($ref) eq 'HASH') {
282 118 100 100     268 if($the_rest or !$want_loop) {
283 103 100       447 $ref = exists( $ref->{$id} ) ? $ref->{$id} : undef;
284             } else {
285 15 50       64 @results = exists( $ref->{$id} ) ? $ref->{$id} : ();
286             }
287             }
288             }
289              
290 171 100 100     529 if(!$the_rest and $want_loop) {
291 23 100 100     66 $ref = ($#results==0 and ref($results[0]) eq 'ARRAY') ? $results[0] : \@results;
292             }
293              
294 171 100       343 croak("Trailing characters '$the_rest' in dot expression '$token_name'") if $the_rest;
295              
296 170 100       237 if($want_loop) { # fixup the array to a conformant data structure
297 23 50       55 my @arr = (reftype($ref) eq 'ARRAY') ? @$ref : ($ref);
298 23         35 return [ map { {$loopmap_name => $_} } @arr ];
  56         128  
299             } else {
300 147 100       241 $ref = scalar(@$ref) if ref($ref) eq 'ARRAY';
301 147         300 return $ref;
302             }
303             }
304              
305             1;
306              
307             __END__