File Coverage

blib/lib/HTML/Template/Plugin/Dot.pm
Criterion Covered Total %
statement 119 124 95.9
branch 71 88 80.6
condition 31 39 79.4
subroutine 12 12 100.0
pod n/a
total 233 263 88.5


line stmt bran cond sub pod time code
1             package HTML::Template::Plugin::Dot;
2 9     9   14942 use vars qw/$VERSION/;
  9         21  
  9         500  
3             $VERSION = '1.00';
4 9     9   44 use strict;
  9         17  
  9         411  
5 9     9   55 use Scalar::Util qw/blessed/;
  9         70  
  9         811  
6              
7 9     9   46 use Carp;
  9         16  
  9         544  
8 9     9   9460 use Data::Dumper;
  9         73595  
  9         801  
9 9     9   10091 use Regexp::Common qw/balanced delimited number/;
  9         37617  
  9         55  
10 9     9   71362 use Scalar::Util qw/reftype/;
  9         22  
  9         646  
11 9     9   49 use base 'Exporter';
  9         19  
  9         17571  
12              
13             sub import {
14             # my $caller = scalar(caller);
15 9     9   179 HTML::Template::Pluggable->add_trigger('middle_param', \&_dot_notation);
16 9         5007 goto &Exporter::import;
17             }
18              
19             sub _dot_notation {
20 124     124   5074 my $self = shift;
21 124         204 my $options = $self->{options};
22 124         174 my $param_map = $self->{param_map};
23              
24             # carp("dot_notation called for $_[0]");
25             # carp("param map: ", Dumper($param_map));
26             # @_ has already been setup for us by the time we're called.
27              
28 124         422 for (my $x = 0; $x <= $#_; $x += 2) {
29 127 50       435 my $param = $options->{case_sensitive} ? $_[$x] : lc $_[$x];
30 127         185 my $value = $_[($x + 1)];
31              
32             # necessary to cooperate with plugin system
33 127 50 66     516 next if ($self->{param_map_done}{$param} and not $self->{num_vars_left_in_loop});
34              
35 127         256 my ($exists,@dot_matches) = _exists_in_tmpl($param_map, $param);
36             # We don't have to worry about "die on bad params", because that will be handled
37             # by HTML::Template's param().
38 127 100       303 next unless $exists;
39              
40 126         218 my $value_type = ref($value);
41 126 100 66     308 if (@dot_matches) {
    100 33        
      66        
42 113         193 for (@dot_matches) {
43             # carp("calling _param_to_tmpl for $_, $param, $value");
44 164         346 my $value_for_tmpl = _param_to_tmpl($self,$_,$param,$value);
45 159         280 my $dot_value_type = ref($value_for_tmpl);
46             # carp("_param_to_tmpl returned '$value_for_tmpl' for '$_', '$param', '$value'");
47 159 100 66     885 unless (defined($dot_value_type) and length($dot_value_type) and ($dot_value_type eq 'ARRAY'
48             or (ref($value_for_tmpl) and (ref($value_for_tmpl) !~ /^(CODE)|(HASH)|(SCALAR)$/) and $value_for_tmpl->isa('ARRAY')))) {
49 136 50       444 (ref($param_map->{$_}) eq 'HTML::Template::VAR') or
50             croak("HTML::Template::param() : attempt to set parameter '$param' with a scalar - parameter is not a TMPL_VAR!");
51 136         151 ${$param_map->{$_}} = $value_for_tmpl;
  136         310  
52             }
53             else {
54 23 50       75 (ref($param_map->{$_}) eq 'HTML::Template::LOOP') or
55             croak("HTML::Template::param() : attempt to set parameter '$param' with an array ref - parameter is not a TMPL_LOOP!");
56 23         46 $param_map->{$_}[HTML::Template::LOOP::PARAM_SET] = $value_for_tmpl;
57             }
58              
59             # Necessary for plugin system compatibility
60 159         290 $self->{num_vars_left_in_loop} -= 1;
61 159         1218 $self->{param_map_done}{$param} = $value; # store the object for future reference
62             }
63             }
64             # We still need to care about tmpl_loops that aren't dot matches so we can adjust their loops
65             elsif (defined($value_type) and length($value_type) and ($value_type eq 'ARRAY'
66             or ((ref($value) !~ /^(CODE)|(HASH)|(SCALAR)$/) and $value->isa('ARRAY')))) {
67 2 50       8 (ref($param_map->{$param}) eq 'HTML::Template::LOOP') or
68             croak("HTML::Template::param() : attempt to set parameter '$param' with an array ref - parameter is not a TMPL_LOOP!");
69              
70             # TODO: Use constant names instead of "0"
71 2 100       17 $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         10  
72              
73             }
74             else {
75 11 50       35 (ref($param_map->{$param}) eq 'HTML::Template::VAR') or
76             croak("HTML::Template::param() : attempt to set parameter '$param' with a scalar - parameter is not a TMPL_VAR!");
77             # intetionally /don't/ set the values for non-dot notation params,
78             # and don't mark them as done, just that they exist.
79 11         71 $self->{num_vars_left_in_loop} -= 1;
80             }
81             }
82             }
83            
84             # Check to see if a param exists in the template, with support for dot notation
85             # returns an an array
86             # - bool for any matches
87             # - array of keys with dot notation that matched.
88             sub _exists_in_tmpl {
89 127     127   181 my ($param_map,$param) = @_;
90 127 100       296 return 1 if exists $param_map->{$param};
91 114 100       293 if (my @matching_dot_tokes = grep { /^$param\./ } keys %$param_map) { # (?:\s*[fF][oO][rR]\s+[_a-z]\w*\s+[Ii][nN]\s+)? after the ^ can be used for supporting "for cd in artist.cds" style loops
  201         1770  
92 113         355 return (1, @matching_dot_tokes);
93             }
94             else {
95 1         2 return undef;
96             }
97             }
98              
99             # =head2 _param_to_tmpl()
100             #
101             # my $result = _param_to_tmpl($pluggable,$tmpl_token_name,$param_name,$param_value);
102             #
103             # Returns the right thing to put in the template given a token name, a param name
104             # and a param value. Returns undef if this template token name and param name
105             # don't match.
106             #
107             # The template token name supports the dot notation, which means that method
108             # calls and nested hashes are expanded.
109             #
110             # However, first we check for a literal match, for backwards compatibility with
111             # HTML::Template.
112             #
113             # =cut
114              
115             sub _param_to_tmpl {
116 168     168   296 my ($self,$toke_name,$param_name,$param_value) = @_;
117              
118             # carp("_param_to_tmpl called for '$toke_name', '$param_name', '$param_value'");
119             # This clause may not be needed because the non-dot-notation
120             # cases are handled elsewhere.
121 168 50       729 if ($toke_name eq $param_name) {
    50          
122             # carp("toke equals param: $toke_name == $param_name");
123 0         0 return $param_value;
124             }
125             elsif (my ($one, $the_rest) = split /\./, $toke_name, 2) {
126             # my $loopmap_name = 'this'; # default for mapping array elements for loop vars
127             # $loopmap_name = $1 if $one =~ s/^\s*[fF][oO][rR]\s+([_a-z]\w*)\s+[Ii][nN]\s+//; the "for x in y" style
128 168 50       375 if ($one eq $param_name) {
129 168         303 my $loopmap_name = 'this'; # default for mapping array elements for loop vars
130 168 100       624 $loopmap_name = $1 if $the_rest =~ s/\s*:\s*([_a-z]\w*)\s*$//;
131              
132             # NOTE: we do the can-can because UNIVSERAL::isa($something, 'UNIVERSAL')
133             # doesn't appear to work with CGI, returning true for the first call
134             # and false for all subsequent calls.
135             # This is exactly what TT does.
136              
137             # Rhesa (Thu Aug 4 18:33:30 CEST 2005)
138             # Patch for mixing method calls and attribute access mixing,
139             # and optional parameter lists!
140             #
141             # First we're setting $ref to $param_value
142             #
143             # We're going to loop over $the_rest by finding anything that matches
144             # - a valid identifier $id ( [_a-z]\w* )
145             # - optionally followed by something resembling an argument list $data
146             # - optionally followed by a dot or $
147             # then we're checking if
148             # - $ref is an object
149             # - if we can call $id on it
150             # - in this case we further parse the argument list for strings
151             # or numbers or references to other h-t params
152             # - or if it's an attribute
153             # - or a hashref and we have no $data
154             # We'll use the result of that operation for $ref as long as there are dots
155             # followed by an identifier
156              
157 168         285 my $ref = $param_value;
158 168   66     786 $self->{param_map_done}{$one} ||= $ref;
159 168         363 my $want_loop = ref($self->{param_map}{$toke_name}) eq 'HTML::Template::LOOP';
160 168         188 my(@results); # keeps return values from dot operations
161             THE_REST:
162 168         997 while( $the_rest =~ s/^
163             ([_a-z]\w*) # an identifier
164             ($RE{balanced})? # optional param list
165             (?:\.|$) # dot or end of string
166             //xi ) {
167 199         27834 my ($id, $data) = ($1, $2);
168 199 100 100     1549 if (ref($ref) and blessed($ref)) {
    100          
169             # carp("$ref is an object, and its ref=", ref($ref), Dumper($ref));
170 80 100       280 if($ref->can($id)) {
    50          
171 73         2314 my @args = ();
172             # carp "Calling $id on ", ref($ref), " with $data";
173 73 100       150 if($data) {
174 33 50       256 $data =~ s/^\(// and $data =~ s/\)$//;
175 33         78 while( $data ) {
176 49 100       222 if ($data =~ s/
    50          
177             ^\s*
178             (
179             $RE{delimited}{-delim=>q{'"`}} # a string
180             |
181             $RE{num}{real} # or a number
182             )
183             (?:,\s*)?
184             //xi
185             ) {
186 39         10576 my $m = $1;
187 39         99 $m =~ s/^["'`]//; $m =~ s/["'`]$//;
  39         97  
188             # carp "found string or numeric argument $m";
189 39         544 push @args, $m;
190             }
191             elsif( $data =~ s/
192             ^\s*
193             ( # ($1) a sub-expression of the form "object.method(args)"
194             ([_a-z]\w*) # ($2) the object in question
195             (?:
196             \.
197             [_a-z]\w* # method name
198             $RE{balanced}? # optional argument list
199             )*
200             )
201             (?:,\s*)?
202             //xi
203             ) {
204 10         3753 my ($m, $o) = ($1, $2);
205             # carp("found subexpression '$m' with '$o'");
206             # carp Dumper($self->{param_map}), Dumper($self->{param_map_done});
207 10 100       67 if( exists($self->{param_map}->{$m}) ) {
    100          
208 4         23 my $prev = $self->param($m);
209             # carp("found '$prev' for '$m' in param_map");
210 4         74 push @args, $prev;
211             }
212             elsif( exists($self->{param_map_done}{$o}) ) {
213 4         34 my $prev = _param_to_tmpl($self, $m, $o, $self->{param_map_done}{$o});
214             # carp("found '$prev' for '$o' in param_map_done");
215 4         74 push @args, $prev;
216             }
217             else {
218 2         313 croak("Attempt to reference nonexisting parameter '$m' in argument list to '$id' in dot expression '$toke_name': $m is not a TMPL_VAR!");
219             }
220             }
221             else {
222             # local $,= ', ';
223             # carp("Parsing is in some weird state. args so far are '@args'. data = '$data'. id='$id'");
224 0         0 last;
225             }
226             }
227 31 50       81 croak("Bare word '$data' not allowed in argument list to '$id' in dot expression '$toke_name'") if $data;
228             }
229             # carp("calling '$id' on '$ref' with '@args'");
230 71         95 eval {
231 71 100 100     269 if($the_rest or !$want_loop) {
232 64         115 $one .= ".$id";
233 64         379 $ref = $ref->$id(@args);
234 60   100     8879 $self->{param_map_done}{$one} ||= $ref;
235             } else {
236 7         57 @results = $ref->$id(@args);
237             }
238             };
239 71 100       1488 if($@) {
240 4 100       24 if( $self->{options}{die_on_bad_params} ) {
241 2         363 croak("Error invoking $ref->$id(@args): $@");
242             } else {
243 2         252 carp("Error invoking $ref->$id(@args): $@");
244 2         182 @results = ();
245 2         5 $ref = $self->{param_map_done}{$one} = '';
246 2         2 $the_rest = '';
247 2         6 last THE_REST;
248             }
249             }
250             }
251             elsif(reftype($ref) eq'HASH') {
252 7 50       231 croak("Can't access hash key '$id' with a parameter list! ($data)") if $data;
253            
254 7 100 66     43 if($the_rest or !$want_loop) {
255 6 100       68 $ref = exists( $ref->{$id} ) ? $ref->{$id} : undef;
256             } else {
257 1 50       13 @results = exists( $ref->{$id} ) ? $ref->{$id} : ();
258             }
259             }
260             else {
261 0         0 croak("Don't know what to do with reference '$ref', identifier '$id' and data '$data', giving up.");
262             }
263             }
264             elsif(ref($ref) eq 'HASH') {
265             # carp("accessing key $id on $ref");
266 118 100 100     663 if($the_rest or !$want_loop) {
267 103 100       778 $ref = exists( $ref->{$id} ) ? $ref->{$id} : undef;
268             } else {
269 15 50       114 @results = exists( $ref->{$id} ) ? $ref->{$id} : ();
270             }
271             }
272              
273             # carp("setting ref for id=$id, toke=$toke_name, param=$param_name, and param map wants a ", ref($self->{param_map}{$toke_name}), " What we got is ", ref($ref), ", results is ", scalar(@results));
274              
275             }
276            
277 164 100 100     18147 if(!$the_rest and $want_loop) {
278 23 100 100     118 $ref = ($#results==0 and ref($results[0]) eq 'ARRAY') ? $results[0] : \@results;
279             }
280            
281 164 100       424 croak("Trailing characters '$the_rest' in dot expression '$toke_name'") if $the_rest;
282             # carp("we got $ref. the rest = $the_rest");
283            
284 163 100       313 if($want_loop) { # fixup the array to a conformant data structure
285 23 50       114 my @arr = (reftype($ref) eq 'ARRAY') ? @$ref : ($ref);
286 23         49 return [ map { {$loopmap_name => $_} } @arr ];
  56         798  
287             } else {
288 140 100       285 $ref = scalar(@$ref) if ref($ref) eq 'ARRAY';
289 140         396 return $ref;
290             }
291             }
292             # no match. give up.
293             else {
294             # carp("No match: one=$one, param_name=$param_name, the rest=$the_rest");
295 0           return undef;
296             }
297             }
298             # no dots and no literal match: give up
299             else {
300             # carp("No dots, no literal match: toke=$toke_name, name=$param_name, value=$param_value");
301 0           return undef;
302             }
303              
304             }
305              
306             1;
307              
308             __END__