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   10145 use vars qw/$VERSION/;
  12         23  
  12         559  
3             $VERSION = '1.04';
4 12     12   62 use strict;
  12         28  
  12         262  
5              
6 12     12   70 use Carp;
  12         21  
  12         538  
7 12     12   5827 use Data::Dumper;
  12         65455  
  12         740  
8 12     12   5433 use Regexp::Common qw/ RE_balanced RE_delimited RE_num_real /;
  12         36985  
  12         43  
9 12     12   1598197 use Scalar::Util qw/blessed reftype/;
  12         24  
  12         764  
10 12     12   70 use base 'Exporter';
  12         23  
  12         1757  
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   73 use constant DEBUG => 0;
  12         23  
  12         22788  
18              
19             sub import {
20             # my $caller = scalar(caller);
21 12     12   181 HTML::Template::Pluggable->add_trigger('middle_param', \&_dot_notation);
22 12         6942 goto &Exporter::import;
23             }
24              
25             sub _dot_notation {
26 133     133   4564 my $self = shift;
27 133         234 my $options = $self->{options};
28 133         154 my $param_map = $self->{param_map};
29              
30             # @_ has already been setup for us by the time we're called.
31              
32 133         146 my %input;
33 133         292 for (my $x = 0; $x <= $#_; $x += 2) {
34 141 50       285 my $param = $options->{case_sensitive} ? $_[$x] : lc $_[$x];
35 141         187 my $value = $_[($x + 1)];
36 141         375 $input{$param} = $value;
37             }
38              
39 133         138 DEBUG and carp("dot_notation called for $_[0]");
40 133         126 DEBUG and carp("param map: ", Dumper($param_map));
41 133         125 DEBUG and carp("input: ", Dumper(\%input));
42              
43 133         407 while (my ($param, $value) = each %input) {
44             # necessary to cooperate with plugin system
45 140 50 66     374 next if ($self->{param_map_done}{$param} and not $self->{num_vars_left_in_loop});
46              
47 140         227 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         196 DEBUG and carp("exists: $exists, dot matches: @dot_matches, param: $param");
51 140 100       291 next unless $exists;
52              
53 136         200 my $value_type = ref($value);
54 136 100 66     285 if(@dot_matches) {
    100 33        
      66        
55 121         167 for my $dot_match (@dot_matches) {
56 172         274 my $value_for_tmpl = _param_to_tmpl($self,$dot_match,$param,$value, \%input);
57 166         225 my $dot_value_type = ref($value_for_tmpl);
58              
59 166 100 66     532 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       329 (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         166 ${$param_map->{$dot_match}} = $value_for_tmpl;
  143         234  
64             }
65             else {
66 23 50       45 (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         29 $param_map->{$dot_match}[HTML::Template::LOOP::PARAM_SET()] = $value_for_tmpl;
69             }
70              
71             # Necessary for plugin system compatibility
72 166         286 $self->{num_vars_left_in_loop} -= 1;
73 166         757 $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         9  
84              
85             }
86             else {
87 13 50       33 (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       70 $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   163 my $self = shift;
104 140         230 my ($param) = @_;
105 140         169 my $param_map = $self->{param_map};
106              
107 140 100       251 return 1 if exists $param_map->{$param};
108 43         85 return @{$self->{_seen_exists_in_tmpl}{$param}} if exists $self->{_seen_exists_in_tmpl}
109 125 100 100     263 and exists $self->{_seen_exists_in_tmpl}{$param};
110              
111 82 100       214 if (my @matching_dot_tokens = grep { /^$param\./ } keys %$param_map) {
  142         1773  
112 78         243 $self->{_seen_exists_in_tmpl}{$param} = [1, @matching_dot_tokens];
113 78         104 return @{$self->{_seen_exists_in_tmpl}{$param}};
  78         240  
114             }
115             else {
116 4         14 $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   281 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       277 if ($token_name eq $param_name) {
143 1         4 return $param_value;
144             }
145              
146 176         415 my ($one, $the_rest) = split /\./, $token_name, 2;
147              
148 176 50       328 length $the_rest or return undef;
149 176 50       256 $one eq $param_name or return undef;
150              
151 176         195 my $loopmap_name = 'this'; # default for mapping array elements for loop vars
152 176 100       360 $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         192 my $ref = $param_value;
175 176   66     540 $self->{param_map_done}{$one} ||= $ref;
176 176         295 my $want_loop = ref($self->{param_map}{$token_name}) eq 'HTML::Template::LOOP';
177 176         198 my(@results); # keeps return values from dot operations
178             THE_REST:
179 176         1818 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         567 my ($id, $data) = ($1, $2);
185 207 100       602 if (blessed($ref)) {
    100          
186 88 100 100     285 if ($ref->can($id) or ($ref->can('AUTOLOAD') && !$ref->isa("Test::MockObject"))) {
    50 100        
187 79         1157 my @args = ();
188 79 100       153 if ($data) {
189 36 50       180 $data =~ s/^\(// and $data =~ s/\)$//;
190 36         99 while ($data) {
191 52 100       1419 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         91 my $m = $1;
202 40         105 $m =~ s/^["'`]//; $m =~ s/["'`]$//;
  40         83  
203 40         128 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         47 my ($m, $o) = ($1, $2);
219 12         18 DEBUG and carp("found subexpression '$m' with '$o'");
220 12         20 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       46 my $prev = exists $input->{$m} ? $input->{$m} : $self->param($m);
223 5         19 DEBUG and carp("found '$prev' for '$m' in param_map");
224 5         15 push @args, $prev;
225             }
226             elsif (exists($self->{param_map_done}{$o})) {
227 4         38 my $prev = _param_to_tmpl($self, $m, $o, $self->{param_map_done}{$o}, $input);
228 4         7 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         5 $self->{param_map}{$o} = HTML::Template::VAR->new();
233 1         22 my $prev = _param_to_tmpl($self, $m, $o, $input->{$o}, $input);
234 1         1 DEBUG and carp("found '$prev' through recursion");
235 1         5 push @args, $prev;
236             }
237             else {
238 2         277 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         100 eval {
248 77 100 100     232 if($the_rest or !$want_loop) {
249 70         131 $one .= ".$id";
250 70         341 $ref = $ref->$id(@args);
251 66   100     9479 $self->{param_map_done}{$one} ||= $ref;
252             } else {
253 7         38 @results = $ref->$id(@args);
254             }
255             };
256 77 100       1384 if($@) {
257 4 100       10 if( $self->{options}{die_on_bad_params} ) {
258 2         225 croak("Error invoking $ref->$id(@args): $@");
259             } else {
260 2         160 carp("Error invoking $ref->$id(@args): $@");
261 2         123 @results = ();
262 2         4 $ref = $self->{param_map_done}{$one} = '';
263 2         5 $the_rest = '';
264 2         5 last THE_REST;
265             }
266             }
267             }
268             elsif(reftype($ref) eq 'HASH') {
269 9 100       584 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       63 $ref = exists( $ref->{$id} ) ? $ref->{$id} : undef;
273             } else {
274 1 50       8 @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     267 if($the_rest or !$want_loop) {
283 103 100       455 $ref = exists( $ref->{$id} ) ? $ref->{$id} : undef;
284             } else {
285 15 50       68 @results = exists( $ref->{$id} ) ? $ref->{$id} : ();
286             }
287             }
288             }
289              
290 171 100 100     490 if(!$the_rest and $want_loop) {
291 23 100 100     59 $ref = ($#results==0 and ref($results[0]) eq 'ARRAY') ? $results[0] : \@results;
292             }
293              
294 171 100       322 croak("Trailing characters '$the_rest' in dot expression '$token_name'") if $the_rest;
295              
296 170 100       263 if($want_loop) { # fixup the array to a conformant data structure
297 23 50       59 my @arr = (reftype($ref) eq 'ARRAY') ? @$ref : ($ref);
298 23         30 return [ map { {$loopmap_name => $_} } @arr ];
  56         129  
299             } else {
300 147 100       212 $ref = scalar(@$ref) if ref($ref) eq 'ARRAY';
301 147         290 return $ref;
302             }
303             }
304              
305             1;
306              
307             __END__