File Coverage

lib/YAX/Query.pm
Criterion Covered Total %
statement 113 130 86.9
branch 56 86 65.1
condition 7 12 58.3
subroutine 13 14 92.8
pod 9 12 75.0
total 198 254 77.9


line stmt bran cond sub pod time code
1             package YAX::Query;
2              
3 3     3   12 use strict;
  3         6  
  3         94  
4              
5 3     3   1312 use YAX::Constants qw/:all/;
  3         8  
  3         6385  
6              
7             our $rx_iden = "[a-zA-Z0-9-\\:_]+|\\*";
8             our $rx_item = "\\[(?:(?:-?\\d+)|(?:\\d+\\s*\\.\\.\\s*-?\\d+))\\]";
9             our $rx_func = "\\b(?:parent|document|id)\\b\\(\\)";
10             our $rx_type = "#(?:text|processing-instruction|comment|cdata|node)";
11             our $rx_filt = "\\(.+?\\)(?:$rx_item)?(?=(?:\\.|\$))";
12             our $rx_attr = "@(?:$rx_iden)(?:$rx_item)?";
13             our $rx_elmt = "(?:$rx_iden)(?:$rx_item)?";
14             our $rx_term = "(?:$rx_type)(?:$rx_item)?|(?:$rx_func)(?:$rx_item)?";
15             our $rx_frag = "(?:$rx_attr)|(?:$rx_term)|(?:$rx_elmt)|(?:$rx_filt)";
16             our $rx_chld = "\\.(?:$rx_frag)";
17             our $rx_desc = "\\.\\.(?:$rx_frag)";
18             our $rx_expr = "$rx_desc|$rx_chld";
19              
20             our $RX_TEST = "^(?:$rx_expr)+\$";
21             our $RX_EXEC = $rx_expr;
22             our $RX_ITEM = '\[(-?\d+)\]$';
23             our $RX_SLCE = '\[(-?\d+)\s*\.\.\s*(-?\d+)\]$';
24              
25             our %CACHE;
26              
27             sub new {
28 14     14 1 33 my ( $class, $node ) = @_;
29 14         49 my $self = bless [ $node ], $class;
30 14         57 $self;
31             }
32              
33             sub tokenize {
34 14     14 0 26 my ( $self, $expr ) = @_;
35 14         390 $expr =~ /$RX_EXEC/g;
36             }
37              
38             sub compile {
39 14     14 0 25 my ( $self, $expr ) = @_;
40 14 50       52 $expr = ".$expr" unless substr( $expr, 0, 1 ) eq '.';
41 14 50       491 die "failed to parse `$expr'" unless $expr =~ /$RX_TEST/g;
42              
43 14 50       48 return @{ $CACHE{ $expr } } if exists $CACHE{ $expr };
  0         0  
44              
45 14         17 my @exec;
46 14         43 my @tokens = $self->tokenize( $expr );
47              
48 14         23 my ( $index, $start, $end, $seen_flat );
49 14         28 foreach my $token ( @tokens ) {
50 28         40 $token = substr( $token, 1 );
51              
52 28         35 undef( $index );
53 28         32 undef( $start );
54 28         29 undef( $end );
55              
56 28 100       156 if ( $token =~ /$RX_ITEM/ ) {
    100          
57 6         40 $token =~ s/$RX_ITEM//;
58 6         17 $index = $1;
59             } elsif ( $token =~ /$RX_SLCE/ ) {
60 1         21 $token =~ s/$RX_SLCE//;
61 1         4 $start = $1;
62 1         3 $end = $2;
63             }
64              
65 28 100       162 if ( substr( $token, 0, 1 ) eq '.' ) {
    100          
    50          
    100          
    50          
    50          
    50          
    100          
    50          
    100          
    100          
66 14 50       35 $seen_flat && die "cannot select `$token' following `$seen_flat'";
67 14         42 push @exec, [ 'descendants', substr( $token, 1 ) ];
68             }
69             elsif ( $token eq '#node' ) {
70 1 50       4 $seen_flat && die "cannot select `$token' following `$seen_flat'";
71 1         2 push @exec, [ 'children' ];
72             }
73             elsif ( $token eq '*' ) {
74 0 0       0 $seen_flat && die "cannot select `$token' following `$seen_flat'";
75 0         0 push @exec, [ 'children', ELEMENT_NODE ];
76             }
77             elsif ( $token eq '#text' ) {
78 1 50       4 $seen_flat && die "cannot select `$token' following `$seen_flat'";
79 1         5 push @exec, [ 'children', TEXT_NODE ];
80 1         4 $seen_flat = $token;
81             }
82             elsif ( $token eq '#cdata' ) {
83 0         0 $seen_flat = $token;
84 0         0 push @exec, [ 'children', CDATA_SECTION_NODE ];
85             }
86             elsif ( $token eq '#processing-instruction' ) {
87 0 0       0 $seen_flat && die "cannot select `$token' following `$seen_flat'";
88 0         0 push @exec, [ 'children', PROCESSING_INSTRUCTION_NODE ];
89             }
90             elsif ( $token eq '#comment' ) {
91 0 0       0 $seen_flat && die "cannot select `$token' following `$seen_flat'";
92 0         0 push @exec, [ 'children', COMMENT_NODE ];
93             }
94             elsif ( $token eq '@*' ) {
95 1 50       5 $seen_flat && die "cannot select `$token' following `$seen_flat'";
96 1         4 push @exec, [ 'attributes' ];
97 1         2 $seen_flat = $token;
98             }
99             elsif ( substr( $token, 0, 1 ) eq '@' ) {
100 0 0       0 $seen_flat && die "cannot select `$token' following `$seen_flat'";
101 0         0 push @exec, [ 'attribute', substr( $token, 1 ) ];
102 0         0 $seen_flat = $token;
103             }
104             elsif ( substr( $token, 0, 1 ) eq '(' ) {
105 3         9 push @exec, [ 'filter', substr( $token, 1, -1 ) ];
106             }
107             elsif ( $token eq 'parent()' ) {
108 1 50       6 $seen_flat && die "cannot select `$token' following `$seen_flat'";
109 1         4 push @exec, [ 'parent' ];
110             }
111             else {
112 7 50       17 $seen_flat && die "cannot select `$token' following `$seen_flat'";
113 7         20 push @exec, [ 'child', $token ]
114             }
115              
116 28 100 66     129 if ( defined $index ) {
    100          
117 6         25 push @exec, [ 'item', 0+$index ];
118             }
119             elsif ( defined $start and defined $end ) {
120 1         6 push @exec, [ 'slice', 0+$start, 0+$end ];
121             }
122             }
123              
124 14         51 $CACHE{ $expr } = [ @exec ];
125 14         58 return @exec;
126             }
127              
128             sub select {
129 14     14 1 23 my ( $self, $expr ) = @_;
130 14         50 my @exec = $self->compile( $expr );
131 14         21 my ( $meth, @list );
132 14         28 foreach my $exec ( @exec ) {
133 35         49 $meth = shift @$exec;
134 35 100       112 if ( $meth eq 'item' ) {
    100          
    100          
135 6         23 @$self = ( $self->[ $exec->[0] ] );
136             }
137             elsif ( $meth eq 'slice' ) {
138 1         10 @$self = @$self[ $exec->[0] .. $exec->[1] ];
139             }
140             elsif ( $meth eq 'filter' ) {
141 3         11 $self->filter( $exec->[0] );
142             }
143             else {
144 25         53 @list = @$self;
145 25         41 @$self = ( );
146 25         32 foreach my $node ( @list ) {
147 37         109 $self->$meth( $node, @$exec );
148             }
149             }
150             }
151 14         54 $self;
152             }
153              
154             sub parent {
155 3     3 1 5 my ( $self, $node ) = @_;
156 3         9 push @$self, $node->parent;
157 3         9 $self;
158             }
159              
160             sub children {
161 4     4 1 9 my ( $self, $node, $type ) = @_;
162 4 50       16 if ( UNIVERSAL::can( $node, 'children' ) ) {
163 4         5 foreach my $child ( @{ $node->children } ) {
  4         12  
164 14 100 100     41 next if defined $type and ( $child->type != $type );
165 13         34 push @$self, $child;
166             }
167             }
168 4         12 $self;
169             }
170              
171             sub child {
172 15     15 1 26 my ( $self, $node, $name ) = @_;
173 15 50       48 if ( UNIVERSAL::can( $node, 'children' ) ) {
174 15         16 foreach my $child ( @{ $node->children } ) {
  15         38  
175 48 100       108 next unless $child->name eq $name;
176 16         31 push @$self, $child;
177             }
178             }
179 15         35 $self;
180             }
181              
182             sub attributes {
183 1     1 1 3 my ( $self, $node ) = @_;
184 1         35 push @$self, $node->attributes;
185 1         7 $self;
186             }
187              
188             sub attribute {
189 0     0 1 0 my ( $self, $node, $name ) = @_;
190 0         0 push @$self, $node->attributes->{ $name };
191 0         0 $self;
192             }
193              
194             sub descendants {
195 14     14 1 28 my ( $self, $node, $name ) = @_;
196 14 50       33 $name = '*' unless $name;
197 14 50       57 if ( UNIVERSAL::can( $node, 'children' ) ) {
198 14         14 my @stack;
199 14         20 my $count = 0;
200 14         18 foreach my $child ( reverse @{ $node->children } ) {
  14         44  
201 30         67 $stack[ $count++ ] = $child;
202             }
203 14         39 while ( --$count >= 0 ) {
204 624         682 my $n = $stack[ $count ];
205 624 50 33     3008 if ( $name eq '*' and $n->type == ELEMENT_NODE ) {
    50 33        
    100          
206 0         0 push @$self, $n;
207             }
208             elsif ( $name eq '#processing-instruction' and
209             ( $n->type == PROCESSING_INSTRUCTION_NODE ) ) {
210 0         0 push @$self, $n;
211             }
212             elsif ( $n->name eq $name ) {
213 53         95 push @$self, $n;
214             }
215 624 100       2367 if ( UNIVERSAL::can( $n, 'children' ) ) {
216 214         192 foreach my $child ( reverse @{ $n->children } ) {
  214         501  
217 594         1293 $stack[ $count++ ] = $child;
218             }
219             }
220             }
221 14         33 undef( @stack );
222             }
223 14         52 $self;
224             }
225              
226             sub filter {
227 3     3 1 5 my ( $self, $test ) = @_;
228 3 50       9 unless ( ref $test eq 'CODE' ) {
229 3         6 my $orig = $test;
230 3         26 $test =~ s/@([a-zA-Z\-:._]+)\b/\$_->{$1}/g;
231 3         5 $orig = $test;
232 3         8 $test = mk_code( $test );
233 3 50       9 die "$@ while compiling filter: `$orig'" if $@;
234             }
235 3         8 @$self = grep { &$test } @$self;
  12         239  
236 3         25 $self;
237             }
238              
239 3     3 0 235 sub mk_code { eval 'sub { '.$_[-1].' }' }
240              
241             1;
242             __END__