File Coverage

blib/lib/Class/XPath.pm
Criterion Covered Total %
statement 133 136 97.7
branch 62 82 75.6
condition 70 91 76.9
subroutine 17 17 100.0
pod 2 4 50.0
total 284 330 86.0


line stmt bran cond sub pod time code
1             package Class::XPath;
2              
3 2     2   52985 use 5.006;
  2         9  
  2         92  
4 2     2   11 use strict;
  2         4  
  2         245  
5 2     2   11 use warnings;
  2         9  
  2         98  
6              
7             our $VERSION = '1.4';
8 2     2   10 use Carp qw(croak);
  2         4  
  2         173  
9 2     2   12 use constant DEBUG => 0;
  2         3  
  2         1282  
10              
11             # regex fragment for names in XPath expressions
12             our $NAME = qr/[\w:]+/;
13              
14             # declare prototypes
15             sub foreach_node (&@);
16              
17             # handle request to build methods from 'use Class::XPath'.
18             sub import {
19 2     2   31 my $pkg = shift;
20 2 50       12 return unless @_;
21 2         7 my $target = (caller())[0];
22             # hand off to add_methods
23 2         8 $pkg->add_methods(@_, target => $target, from_import => 1);
24             }
25              
26             {
27             # setup lists of required params
28             my %required = map { ($_,1) }
29             qw(get_name get_parent get_children
30             get_attr_names get_attr_value get_content
31             get_root call_match call_xpath);
32            
33             # add the xpath and match methods to
34             sub add_methods {
35 2     2 0 6 my $pkg = shift;
36 2         16 my %args = (call_match => 'match',
37             call_xpath => 'xpath',
38             @_);
39 2         7 my $from_import = delete $args{from_import};
40 2         5 my $target = delete $args{target};
41 2 50       8 croak("Missing 'target' parameter to ${pkg}->add_methods()")
42             unless defined $target;
43              
44             # check args
45 2         4 local $_;
46 2         9 for (keys %args) {
47 18 0       45 croak("Unrecognized parameter '$_' " .
    50          
48             ($from_import ? " on 'use $pkg' line. " :
49             "passed to ${pkg}->add_methods()"))
50             unless $required{$_};
51             }
52 2         16 for (keys %required) {
53 18 0       40 croak("Missing required parameter '$_' " .
    50          
54             ($from_import ? " on 'use $pkg' line. " :
55             "in call to ${pkg}->add_methods()"))
56             unless exists $args{$_};
57             }
58              
59             # translate get_* method names to sub-refs
60 2         8 for (grep { /^get_/ } keys %args) {
  18         50  
61 14 50 33     41 next if ref $args{$_} and ref $args{$_} eq 'CODE';
62 14         1340 $args{$_} = eval "sub { shift->$args{$_}(\@_) };";
63 14 50       44 croak("Unable to compile sub for '$_' : $@") if $@;
64             }
65              
66             # install code into requested names to call real match/xpath with
67             # supplied %args
68             {
69 2     2   12 no strict 'refs';
  2         5  
  2         2849  
  2         6  
70 2         14 *{"${target}::$args{call_match}"} =
71 2     169   7 sub { $pkg->match($_[0], \%args, $_[1]) };
  169         26251  
72 2         319 *{"${target}::$args{call_xpath}"} =
73 169     169   47515 sub { $pkg->xpath($_[0], \%args) }
74 2         7 }
75             }}
76              
77             sub match {
78 169     169 1 365 my ($pkg, $self, $args, $xpath) = @_;
79 169         463 my ($get_root, $get_parent, $get_children, $get_name) =
80 169         245 @{$args}{qw(get_root get_parent get_children get_name)};
81              
82 169 50       423 croak("Bad call to $args->{call_match}: missing xpath argument.")
83             unless defined $xpath;
84            
85 169         178 print STDERR "match('$xpath') called.\n" if DEBUG;
86              
87             # / is the root. This should probably work as part of the
88             # algorithm, but it doesn't.
89 169 50       378 return $get_root->($self) if $xpath eq '/';
90            
91             # . is self. This should also work as part of the algorithm,
92             # but it doesn't.
93 169 50       934 return $self if $xpath eq '.';
94              
95             # break up an incoming xpath into a set of @patterns to match
96             # against a list of @target elements
97 169         192 my (@patterns, @targets);
98            
99             # target aquisition
100 169 100       940 if ($xpath =~ m!^//(.*)$!) {
    100          
    100          
    50          
101 1         4 $xpath = $1;
102             # this is a match-anywhere pattern, which should be tried on
103             # all nodes
104 1     52   1543 foreach_node { push(@targets, $_) } $get_root->($self), $get_children;
  52         86  
105             } elsif ($xpath =~ m!^/(.*)$!) {
106 92         193 $xpath = $1;
107             # this match starts at the root
108 92         2284 @targets = ($get_root->($self));
109             } elsif ($xpath =~ m!^\.\./(.*)$!) {
110 27         310 $xpath = $1;
111             # this match starts at the parent
112 27         839 @targets = ($get_parent->($self));
113             } elsif ($xpath =~ m!^\./(.*)$!) {
114 0         0 $xpath = $1;
115 0         0 @targets = ($self);
116             } else {
117             # this match starts here
118 49         130 @targets = ($self);
119             }
120            
121             # pattern breakdown
122 169         1202 my @parts = split('/', $xpath);
123 169         454 my $count = 0;
124 169         320 for (@parts) {
125 174         177 $count++;
126 174 100 100     15691 if (/^$NAME$/) {
    100 66        
    100          
    100          
    100          
    100          
    50          
127             # it's a straight name match
128 6         29 push(@patterns, { name => $_ });
129             } elsif (/^($NAME)\[(-?\d+)\]$/o) {
130             # it's an indexed name
131 117         922 push(@patterns, { name => $1, index => $2 });
132             } elsif (/^($NAME)\[\@($NAME)\s*=\s*"([^"]+)"\]$/o or
133             /^($NAME)\[\@($NAME)\s*=\s*'([^']+)'\]$/o) {
134             # it's a string attribute match
135 6         72 push(@patterns, { name => $1, attr => $2, value => $3 });
136             } elsif (/^($NAME)\[\@($NAME)\s*(=|>|<|<=|>=|!=)\s*(\d+)\]$/o) {
137             # it's a numeric attribute match
138 13         173 push(@patterns, { name => $1, attr => $2, op => $3, value => $4 });
139             } elsif (/^($NAME)\[($NAME|\.)\s*=\s*"([^"]+)"\]$/o or
140             /^($NAME)\[($NAME|\.)\s*=\s*'([^']+)'\]$/o) {
141             # it's a string child match
142 14         653 push(@patterns, { name => $1, child => $2, value => $3 });
143             } elsif (/^($NAME)\[($NAME|\.)\s*(=|>|<|<=|>=|!=)\s*(\d+)\]$/) {
144             # it's a numeric child match
145 13         150 push(@patterns, { name => $1, child => $2, op => $3, value => $4 });
146             } elsif (/^\@($NAME)$/) {
147             # it's an attribute name
148 5         27 push(@patterns, { attr => $1 });
149              
150             # it better be last
151 5 100       778 croak("Bad call to $args->{call_match}: '$xpath' contains an attribute selector in the middle of the expression.")
152             if $count != @parts;
153             } else {
154             # unrecognized token
155 0         0 croak("Bad call to $args->{call_match}: '$xpath' contains unknown token '$_'");
156             }
157             }
158              
159 168 50       448 croak("Bad call to $args->{call_match}: '$xpath' contains no search tokens.")
160             unless @patterns;
161            
162             # apply the patterns to all available targets and collect results
163 168         292 my @results = map { $pkg->_do_match($_, $args, @patterns) } @targets;
  219         2645  
164            
165 168         2060 return @results;
166             }
167            
168             # the underlying match engine. this takes a list of patterns and
169             # applies them to child elements
170             sub _do_match {
171 227     227   464 my ($pkg, $self, $args, @patterns) = @_;
172 227         826 my ($get_parent, $get_children, $get_name, $get_attr_value, $get_attr_names, $get_content) =
173 227         385 @{$args}{qw(get_parent get_children get_name get_attr_value get_attr_names get_content)};
174 227         282 local $_;
175              
176             print STDERR "_do_match(" . $get_name->($self) . " => " .
177 227         250 join(', ', map { '{' . join(',', %$_) . '}' } @patterns) .
178             ") called.\n"
179             if DEBUG;
180              
181             # get pattern to apply to direct descendants
182 227         438 my $pat = shift @patterns;
183              
184             # find matches and put in @results
185 227         4408 my @results;
186             my @kids;
187              
188 2     2   14 { no warnings 'uninitialized';
  2         4  
  2         2089  
  227         234  
189 227         9988 @kids = grep { $get_name->($_) eq $pat->{name} } $get_children->($self);
  1044         39297  
190             }
191              
192 227 100       2644 if (defined $pat->{index}) {
    100          
    100          
193             # get a child by index
194 116 50       547 push @results, $kids[$pat->{index}]
195             if (abs($pat->{index}) <= $#kids);
196             } elsif (defined $pat->{attr}) {
197 27 100       57 if (defined $pat->{name}) {
198             # default op is 'eq' for string matching
199 19   100     69 my $op = $pat->{op} || 'eq';
200              
201             # do attribute matching
202 19         35 foreach my $kid (@kids) {
203 57         2245 my $value = $get_attr_value->($kid, $pat->{attr});
204 57 100 100     1841 push(@results, $kid)
      100        
      66        
      100        
      66        
      100        
      66        
      66        
      66        
      66        
      33        
      100        
      66        
205             if ($op eq 'eq' and $value eq $pat->{value}) or
206             ($op eq '=' and $value == $pat->{value}) or
207             ($op eq '!=' and $value != $pat->{value}) or
208             ($op eq '>' and $value > $pat->{value}) or
209             ($op eq '<' and $value < $pat->{value}) or
210             ($op eq '>=' and $value >= $pat->{value}) or
211             ($op eq '<=' and $value <= $pat->{value});
212             }
213             }
214             else {
215 8         14 my $attr = $pat->{attr};
216 24         301 push(@results, $get_attr_value->($self, $attr))
217 8 50       218 if grep { $_ eq $attr } $get_attr_names->($self);
218             }
219             } elsif (defined $pat->{child}) {
220 27 50       1145 croak("Can't process child pattern without name")
221             unless defined $pat->{name};
222             # default op is 'eq' for string matching
223 27   100     124 my $op = $pat->{op} || 'eq';
224             # do attribute matching
225 27         65 foreach my $kid (@kids) {
226 81 100       2304 foreach (
227 1200         43971 $pat->{child} eq "." ? $kid
228             : grep {$get_name->($_) eq $pat->{child}} $get_children->($kid)
229             ) {
230 405         1251 my $value;
231             foreach_node {
232 501     501   18955 my $txt = $get_content->($_);
233 501 100       4289 $value .= $txt if defined $txt;
234 405         2305 } $_, $get_children;
235 405 50       5259 next unless defined $value;
236 405 100 100     7986 push(@results, $kid)
      100        
      66        
      100        
      66        
      100        
      66        
      66        
      66        
      66        
      33        
      100        
      66        
237             if ($op eq 'eq' and $value eq $pat->{value}) or
238             ($op eq '=' and $value == $pat->{value}) or
239             ($op eq '!=' and $value != $pat->{value}) or
240             ($op eq '>' and $value > $pat->{value}) or
241             ($op eq '<' and $value < $pat->{value}) or
242             ($op eq '>=' and $value >= $pat->{value}) or
243             ($op eq '<=' and $value <= $pat->{value});
244             }
245             }
246             } else {
247 57         99 push @results, @kids;
248             }
249              
250             # all done?
251 227 100       1797 return @results unless @patterns;
252              
253             # apply remaining patterns on matching kids
254 4         7 return map { $pkg->_do_match($_, $args, @patterns) } @results;
  8         32  
255             }
256              
257              
258             sub xpath {
259 376     376 1 737 my ($pkg, $self, $args) = @_;
260 376         780 my ($get_parent, $get_children, $get_name) =
261 376         426 @{$args}{qw(get_parent get_children get_name)};
262              
263 376         10050 my $parent = $get_parent->($self);
264 376 100       5692 return '/' unless defined $parent; # root's xpath is /
265            
266             # get order within same-named nodes in the parent
267 207         19648 my $name = $get_name->($self);
268 207         864 my $count = 0;
269 207         5339 for my $kid ($get_children->($parent)) {
270 648 100       3734 last if $kid == $self;
271 441 100       11248 $count++ if $get_name->($kid) eq $name;
272             }
273              
274             # construct xpath using parent's xpath and our name and count
275 207 100       641 return $pkg->xpath($parent, $args) .
276             ($get_parent->($parent) ? '/' : '') .
277             $name . '[' . $count . ']';
278             }
279              
280              
281             # does a depth first traversal in a stack
282             sub foreach_node (&@) {
283 406     406 0 780 my ($code, $node, $get_children) = @_;
284 406         1485 my @stack = ($node);
285 406         883 while (@stack) {
286 553         2127 local $_ = shift(@stack);
287 553         1373 $code->();
288 553         20598 push(@stack, $get_children->($_));
289             }
290             }
291              
292             1;
293             __END__