File Coverage

blib/lib/XML/CuteQueries.pm
Criterion Covered Total %
statement 18 18 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 24 24 100.0


line stmt bran cond sub pod time code
1              
2             package XML::CuteQueries;
3              
4 21     21   190104 use strict;
  21         51  
  21         1246  
5 21     21   126 use warnings;
  21         40  
  21         1020  
6              
7             our $VERSION = '0.6614';
8              
9 21     21   560 use Carp;
  21         37  
  21         2420  
10 21     21   1551 use Scalar::Util qw(reftype blessed);
  21         48  
  21         4363  
11 21     21   13329 use XML::CuteQueries::Error;
  21         58  
  21         89  
12 21     21   132 use base 'XML::Twig';
  21         39  
  21         52773  
13              
14             use constant LIST => 1;
15             use constant KLIST => 2;
16              
17             use parent 'Exporter';
18             our @EXPORT_OK = qw(CQ slurp);
19              
20             # CQ {{{
21             sub CQ {
22             our $CQ ||= __PACKAGE__->new;
23              
24             no warnings 'misc'; ## no critic: yeah, they might do it wrong and pass an odd number, deal with it
25             if( my %o = @_ ) {
26             my $arg;
27             if( $arg = $o{file} ) {
28             eval { $CQ->parsefile($arg); 1 } or do {
29             $CQ = __PACKAGE__->new; # build new CQ so we can do the next twig
30             my $e = $@; $e =~ s/\s+(eval \d+)//;
31             croak $@;
32             }
33              
34             } elsif( $arg = $o{xml} ) {
35             eval { $CQ->parse($arg); 1 } or do {
36             $CQ = __PACKAGE__->new; # build new CQ so we can do the next twig
37             my $e = $@; $e =~ s/\s+(eval \d+)//;
38             croak $@;
39             }
40             }
41             }
42              
43             return $CQ
44             }
45             # }}}
46              
47             our %VALID_OPTS = (map {$_=>1} qw(nostrict nostrict_match nostrict_single nofilter_nontags notrim klist));
48              
49             # _data_error {{{
50             sub _data_error {
51             my $this = shift;
52             my $desc = shift || "single-value";
53             $desc = shift() . " [$desc result request]";
54              
55             XML::CuteQueries::Error->new(
56             type => XML::CuteQueries::Error::DATA_ERROR(),
57             text => $desc,
58             )->throw;
59              
60             return; # technically unreachable, but critic won't notice
61             }
62             # }}}
63             # _query_error {{{
64             sub _query_error {
65             my $this = shift;
66             my $err = shift;
67              
68             my $f = __FILE__;
69             $err =~ s/\s+at\s+\Q$f\E\s+line\s+\d+//;
70              
71             XML::CuteQueries::Error->new(
72             type => XML::CuteQueries::Error::QUERY_ERROR(),
73             text => $err,
74             )->throw;
75              
76             return; # technically unreachable, but critic won't notice
77             }
78             # }}}
79              
80             # _pre_parse_queries {{{
81             sub _pre_parse_queries {
82             my $this = shift;
83             my $opts = shift;
84              
85             if( @_ % 2 ) {
86             $this->_query_error("odd number of arguments, queries are hashes and therefore should be a series of key/value pairs.");
87             }
88              
89             return 1;
90             }
91             # }}}
92             # _execute_query {{{
93             sub _execute_query {
94             my ($this, $root, $opts, $query, $res_type, $context) = @_;
95              
96             XML::CuteQueries::Error->new(text=>"\$context specification error")->throw
97             if not defined $context or $context<1 or $context>2;
98              
99             my $mt = 0; # magic restype (restype scalar sub-type)
100             my $rt = 0; # processed reftype (false for scalars)
101              
102             if( $res_type ||= 0 ) {
103             unless( $rt = reftype $res_type ) {
104             if( $res_type =~ m/^(?:x|xml|xml\(\))\z/ ) { # xml()
105             $mt = "x";
106              
107             } elsif( $res_type =~ m/^(?:t|twig|twig\(\))\z/ ) { # twig()
108             $mt = "t";
109              
110             } elsif( $res_type =~ m/^(?:r|a|recurse|all)(?:_text(?:\(\))?)?/ ) { # recurse_text() all_text()
111             $mt = "r";
112              
113             } else {
114             $this->_query_error("unknown scalar query sub-type: $res_type");
115             }
116              
117             $res_type = undef;
118             }
119             }
120              
121             my $kar = 0; # klist keys are expected more than once
122             if( $query =~ s/^\[\]// ) {
123             # NOTE: I don't think this is ever valid XPath
124             $kar = 1;
125              
126             $this->_query_error("[] queries (\"[]$query\") do not make sense outside of klist contexts") unless $context == KLIST;
127             }
128              
129             my ($re, $nre) = (0,0);
130             if( my ($type, $code) = $query =~ m/^<([!Nn]?[Rr][Ee])>(.+?)(?:<\/\1>)?\z/ ) {
131             if( lc($type) eq "re" ) {
132             $re = 1;
133              
134             } else {
135             $re = $nre = 1;
136             }
137              
138             $query = qr($code);
139             }
140              
141             my @c;
142             my $attr_query;
143             my $oquery = $query;
144             if( not $rt ) {
145             if( $query =~ m/^\S/ and $query =~ s/\@([\w\d:]+|\*)\z// ) {
146             $attr_query = $1;
147             $query =~ s,(?<=\w)\/$,,;
148             @c = $root unless $query;
149             }
150             }
151              
152             # @c is only true when it's a root-attr query
153             unless(@c) {
154             @c = eval {
155             if( $re ) {
156             return grep {$_->gi !~ $query } $root->children if $nre;
157             return grep {$_->gi =~ $query } $root->children;
158             }
159              
160             return $root->get_xpath($query)
161             };
162              
163             for(@c) { $_ = $root if $_ == $this }
164              
165             $this->_query_error("while executing \"$query\": $@") if $@;
166             @c = grep {$_->gi !~ m/^#/} @c unless $opts->{nofilter_nontags};
167             }
168              
169             $this->_data_error($rt, "match failed for \"$query\"") unless @c or $opts->{nostrict_match};
170             return unless @c;
171              
172              
173             if( not $rt ) {
174             my $_trimlist;
175             my $_trimhash;
176              
177             if( $opts->{notrim} ) {
178             $_trimlist = $_trimhash = sub {@_};
179              
180             } else {
181             $_trimlist = sub { for(@_) { unless( m/\n/ ) { s/^\s+//; s/\s+$// }}; @_ };
182             $_trimhash = sub { my %h=@_; for(grep {defined $_} values %h) { unless( m/\n/ ) { s/^\s+//; s/\s+$// }}; %h };
183             }
184              
185             if( $attr_query ) {
186             if( $kar ) {
187             my %h;
188              
189             # NOTE: it's safe to assume we're in KLIST
190              
191             my @attr = $attr_query eq "*"
192             ? do { my %ua; grep { !$ua{$_}++ } map { keys %{$_->{att}} } @c }
193             : $attr_query;
194              
195             for my $attr (@attr) {
196             push @{$h{$attr}}, $_trimlist->(
197             map { $_->{$attr} }
198             grep { exists $_->{$attr} }
199             map { $_->{att} }
200             @c
201             );
202             }
203              
204             return %h;
205             }
206              
207             if( $attr_query eq "*" ) {
208             if( $context == KLIST ) {
209             return $_trimhash->( map { %{$_->{att}} } @c );
210             }
211              
212             return $_trimlist->( map { values %{$_->{att}} } @c );
213             }
214              
215             if( $context == KLIST ) {
216             return $_trimhash->( map { $attr_query => $_->{att}{$attr_query} } @c );
217             }
218              
219             return $_trimlist->( map { $_->{att}{$attr_query} } @c );
220             }
221              
222             my $get_value = {
223             t => sub { $_[0] },
224             x => 'xml_string',
225             r => 'text',
226             0 => 'text_only'
227             }->{$mt};
228              
229             if ($mt eq 't') {
230             $_trimlist = $_trimhash = sub { @_ };
231             }
232              
233             return $_trimlist->( map { $_->$get_value } @c ) unless $context == KLIST;
234              
235             my %h;
236              
237             for (@c) {
238             my $arr = $h{$_->gi} ||= [];
239             # discard all but the last result
240             @$arr = () if $opts->{nostrict_single} and not $kar;
241             push @$arr, $_->$get_value;
242             unless ($kar || @$arr == 1) {
243             $this->_data_error($rt, "expected exactly one match-per-tagname for \"$query\", got more")
244             }
245             }
246              
247             if ($kar) {
248             $_trimlist->( @$_ ) for values %h;
249             return %h;
250             } else {
251             $_ = $_->[-1] for values %h;
252             return $_trimhash->(%h);
253             }
254              
255             } elsif( $rt eq "HASH" ) {
256             if( $context == KLIST ) {
257             if( $kar ) {
258             my %h;
259              
260             for my $c (@c) {
261             push @{$h{$c->gi}},
262             {map { $this->_execute_query($c, $opts, $_ => $res_type->{$_}, KLIST) } keys %$res_type}
263             }
264              
265             return %h;
266              
267             } elsif( $opts->{nostrict_single} ) {
268             return map {
269             my $c = $_;
270             $c->gi => {map { $this->_execute_query($c, $opts, $_ => $res_type->{$_}, KLIST) } keys %$res_type}
271              
272             } @c;
273              
274             } else {
275             my %check;
276             return map {
277             my $c = $_;
278             my $g = $_->gi;
279              
280             $this->_data_error($rt, "expected exactly one match-per-tagname for \"$query\", got more")
281             if $check{$g}++;
282              
283             $g => {map { $this->_execute_query($c, $opts, $_ => $res_type->{$_}, KLIST) } keys %$res_type}
284              
285             } @c;
286             }
287             }
288              
289             return map {
290             my $c = $_;
291             scalar # I don't think I should need this word here, but I clearly do, plus would also work
292             {map {$this->_execute_query($c, $opts, $_ => $res_type->{$_}, KLIST)} keys %$res_type};
293             } @c;
294              
295             } elsif( $rt eq "ARRAY" ) {
296             my @p;
297             while( my ($pat, $res) = splice @$res_type, 0, 2 ) {
298             push @p, [$pat, $res];
299             }
300              
301             if( $context == KLIST ) {
302             if( $kar ) {
303             my %h;
304              
305             for my $c (@c) {
306             push @{$h{$c->gi}},
307             [ map {$this->_execute_query($c, $opts, @$_, LIST)} @p ]
308             }
309              
310             return %h;
311              
312             } elsif( $opts->{nostrict_single} ) {
313             return map {
314             my $c = $_;
315             $c->gi => [ map {$this->_execute_query($c, $opts, @$_, LIST)} @p ] } @c;
316              
317             } else {
318             my %check;
319             return map {
320             my $c = $_;
321             my $g = $c->gi;
322              
323             $this->_data_error($rt, "expected exactly one match-per-tagname for \"$query\", got more")
324             if $check{$g}++;
325              
326             $g => [ map {$this->_execute_query($c, $opts, @$_, LIST)} @p ] } @c;
327             }
328             }
329              
330             return map { my $c = $_; [ map {$this->_execute_query($c, $opts, @$_, LIST)} @p ] } @c;
331             }
332              
333             XML::CuteQueries::Error->new(text=>"unexpected condition met")->throw;
334             return;
335             }
336             # }}}
337              
338             # cute_query {{{
339             sub cute_query {
340             my $this = shift;
341             my $opts = {};
342             $opts = shift if ref $_[0] eq "HASH";
343              
344             $opts->{nostrict_match} = $opts->{nostrict_single} = $opts->{nostrict} if exists $opts->{nostrict};
345              
346             for(keys %$opts) {
347             $this->_query_error("no such query option \"$_\"") unless $VALID_OPTS{$_};
348             }
349              
350             my $context = LIST;
351             $context = KLIST if delete $opts->{klist};
352              
353             $this->_pre_parse_queries($opts, @_);
354              
355             my @result;
356             my ($query, $res_type) = @_; # used in error below
357              
358             while( my @q = splice @_, 0, 2 ) {
359             push @result, $this->_execute_query($this->root, $opts, @q, $context);
360             }
361              
362             unless( wantarray ) {
363              
364             if( @result>1 ) {
365             unless( $opts->{nostrict_single} ) {
366             my $rt = (defined $res_type and reftype $res_type) || '';
367             $this->_data_error($rt, "expected exactly one match for \"$query\", got " . @result)
368             }
369              
370             } elsif( @result<1 ) {
371             unless( $opts->{nostrict_match} ) {
372             my $rt = (defined $res_type and reftype $res_type) || '';
373             $this->_data_error($rt, "expected exactly one match for \"$query\", got " . @result)
374             }
375             }
376              
377             return $result[0]; # we never want the size of the array, preferring the first match
378             }
379              
380             return @result;
381             }
382             # }}}
383              
384             # hash_query {{{
385             sub hash_query {
386             my $this = shift;
387             my $opts = {};
388             $opts = shift if ref($_[0]) eq "HASH";
389              
390             $opts->{klist} = 1;
391             return $this->cute_query($opts, @_);
392             }
393             *klist_query = \&hash_query;
394             # }}}
395              
396             1;