File Coverage

blib/lib/Pod/Abstract/Path.pm
Criterion Covered Total %
statement 401 438 91.5
branch 133 164 81.1
condition 10 12 83.3
subroutine 51 52 98.0
pod 2 26 7.6
total 597 692 86.2


line stmt bran cond sub pod time code
1             package Pod::Abstract::Path;
2 6     6   43 use strict;
  6         15  
  6         245  
3 6     6   43 use warnings;
  6         13  
  6         340  
4              
5 6     6   4073 use Data::Dumper;
  6         67249  
  6         617  
6              
7 6     6   3373 use Pod::Abstract::BuildNode qw(node);
  6         47  
  6         613  
8              
9             $Data::Dumper::Indent = 1;
10              
11             our $VERSION = '0.26';
12              
13 6     6   45 use constant CHILDREN => 1; # /
  6         12  
  6         454  
14 6     6   36 use constant ALL => 2; # //
  6         13  
  6         302  
15 6     6   32 use constant NAME => 3; # head1
  6         13  
  6         318  
16 6     6   34 use constant INDEX => 4; # (3)
  6         11  
  6         288  
17 6     6   33 use constant L_SELECT => 5; # [
  6         9  
  6         414  
18 6     6   38 use constant ATTR => 6; # @label
  6         10  
  6         413  
19 6     6   56 use constant N_CMP => 7; # == != < <= > >=
  6         17  
  6         350  
20 6     6   33 use constant STRING => 8; # 'foobar'
  6         11  
  6         346  
21 6     6   34 use constant R_SELECT => 9; # ]
  6         39  
  6         319  
22 6     6   34 use constant NUM_OF => 10; # #
  6         24  
  6         306  
23 6     6   59 use constant NOT => 15; # !
  6         13  
  6         363  
24 6     6   36 use constant PARENT => 16; # ..
  6         23  
  6         277  
25 6     6   32 use constant MATCHES => 17; # =~
  6         10  
  6         333  
26 6     6   33 use constant REGEXP => 18; # {}
  6         11  
  6         354  
27 6     6   35 use constant NOP => 19; # .
  6         11  
  6         320  
28 6     6   50 use constant PREV => 20; # <<
  6         12  
  6         333  
29 6     6   40 use constant NEXT => 21; # >>
  6         12  
  6         277  
30 6     6   42 use constant ROOT => 22; # ^
  6         12  
  6         309  
31 6     6   31 use constant UNION => 23; # |
  6         12  
  6         321  
32 6     6   32 use constant INTERSECT => 24; # &
  6         12  
  6         379  
33 6     6   34 use constant S_CMP => 25; # eq lt gt le ge ne
  6         12  
  6         308  
34 6     6   33 use constant ALL_PARENT => 26; # ...
  6         12  
  6         34073  
35              
36             =pod
37              
38             =head1 NAME
39              
40             Pod::Abstract::Path - Search for POD nodes matching a path within a
41             document tree.
42              
43             =head1 SYNOPSIS
44              
45             /head1(1)/head2 # All head2 elements under
46             # the 2nd head1 element
47             //item # All items anywhere
48             //item[@label =~ {^\*$}] # All items with '*' labels.
49             //head2[/hilight] # All head2 elements containing
50             # "hilight" elements
51              
52             # Top level head1s containing head2s that have headings matching
53             # "NAME", and also have at least one list somewhere in their
54             # contents.
55             /head1[/head2[@heading =~ {NAME}]][//over]
56              
57             # Top level headings having the same title as the following heading.
58             /head1[@heading = >>@heading]
59              
60             # Top level headings containing at least one subheading with the same
61             # name.
62             /head1[@heading = ./head2@heading]
63              
64             =head1 DESCRIPTION
65              
66             Pod::Abstract::Path is a path selection syntax that allows fast and
67             easy traversal of Pod::Abstract documents. While it has a simple
68             syntax, there is significant complexity in the queries that you can
69             create.
70              
71             Not all of the designed features have yet been implemented, but it is
72             currently quite useful, and all of the filters in C make use of
73             Pod Paths.
74              
75             =head2 SYMBOLS:
76              
77             =over
78              
79             =item /
80              
81             Selects children of the left hand side.
82              
83             =item //
84              
85             Selects all descendants of the left hand side.
86              
87             =item (I)
88              
89             Selects only the element at I.
90              
91             =item .
92              
93             Selects the current node - this is a NOP that can be used in
94             expressions.
95              
96             =item ..
97              
98             Selects the parent node. If there are multiple nodes selected, all of
99             their parents will be included.
100              
101             =item ...
102              
103             Selects ALL parent nodes, up to the document root. e.g, C<"..."> on a
104             paragraph in a head3 will yield the head3, head2, head1 and root nodes (in
105             that order).
106              
107             Practical application: Find the first enclosing heading node:
108              
109             ...[@heaading](0)
110              
111             =item ^
112              
113             Selects the root node of the tree for the current node. This allows
114             you to escape from a nested expression. Note that this is the ROOT
115             node, not the node that you started from.
116              
117             If you want to evaluate an expression from a node as though it were
118             the root node, the easiest ways are to detach or dup it - otherwise
119             the root operator will find the original root node.
120              
121             =item name, #cut, :text, :verbatim, :paragraph
122              
123             Any element name, or symbolic type name, will restrict the selection
124             to only elements matching that type. e.g, "C" will
125             select all descendants, anywhere, but then restrict that set to only
126             C<:paragraph> type nodes.
127              
128             Names together separated by spaces will match all of those names -
129             e.g: C will match all lists and all head1s.
130              
131             =item &, | (union and intersection)
132              
133             Union will take expressions on either side, and return all nodes that
134             are members of either set. Intersection returns nodes that are members
135             of BOTH sets. These can be used to extend expressions, and within [
136             expressions ] where a path is supported (left side of a match, left or
137             right side of an = sign). These are NOT logical and/or, though a
138             similar effect can be induced through these operators.
139              
140             =item @attrname
141              
142             The named attribute of the nodes on the left hand side. Current
143             attributes are C<@heading> for head1 through head4, and C<@label> for
144             list items.
145              
146             =item [ expression ]
147              
148             Select only the left hand elements that match the expression in the
149             brackets. The expression will be evaluated from the point of view of
150             each node in the current result set.
151              
152             Expressions can be:
153              
154             =over
155              
156             =item simple: C<[/head2]>
157              
158             Any regular path will be true if there are any nodes matched. The
159             above example will be true if there are any head2 nodes as direct
160             children of the selected node.
161              
162             =item regex match: C<[@heading =~ {FOO}]>
163              
164             A regex match will be true if the left hand expression has nodes that
165             match the regular expression between the braces on the right hand
166             side. The above example will match anything with a heading containing
167             "FOO".
168              
169             Optionally, the right hand closing brace may have the C modifier to
170             cause case-insensitive matching. i.e C<[@heading =~ {foo}i]> will
171             match C or C.
172              
173             =item complement: C<[! /head2 ]>
174              
175             Reverses the remainder of the expression. The above example will match
176             anything B a child head2 node.
177              
178             =item compare operators: eg. C<[ /node1 eq /node2 ]>
179              
180             Matches nodes where the operator is satistied for at least one pair of
181             nodes. The right hand expression can be a constant string (single
182             quoted: C<'string'>, or a second expression. If two expressions are
183             used, they are matched combinationally - i.e, all result nodes on the
184             left are matched against all result nodes on the right. Both sides may
185             contain nested expressions.
186              
187             The following Perl compatible operators are supported:
188              
189             String: C< eq gt lt le ge ne >
190              
191             Numeric: C<<< == < > <= >= != >>>
192              
193             =back
194              
195             =back
196              
197             =head1 PERFORMANCE
198              
199             Pod::Abstract::Path is not designed to be fast. It is designed to be
200             expressive and useful, but it involves sucessive
201             expand/de-duplicate/linear search operations and doing this with large
202             documents containing many nodes is not suitable for high performance
203             systems.
204              
205             Simple expressions can be fast enough, but there is nothing to stop
206             you from writing "//[]" and linear-searching all 10,000
207             nodes of your Pod document. Use with caution in interactive systems.
208              
209             =head1 INTERFACE
210              
211             It is recommended you use the C<< Pod::Abstract::Node->select >> method
212             to evaluate Path expressions.
213              
214             If you wish to generate paths for use in other modules, use
215             C to generate a parse tree, pass that as an argument to
216             C, then use C to evaluate the expression against a list
217             of nodes. You can re-use the same parse tree to process multiple lists
218             of nodes in this fashion.
219              
220             =cut
221              
222             sub new {
223 146     146 0 251 my $class = shift;
224 146         248 my $expression = shift;
225 146         235 my $parse_tree = shift;
226              
227 146 100       313 if($parse_tree) {
228 86         326 my $self = bless {
229             expression => $expression,
230             parse_tree => $parse_tree
231             }, $class;
232 86         215 return $self;
233             } else {
234 60         231 my $self = bless { expression => $expression }, $class;
235              
236 60         178 my @lexemes = $self->lex($expression);
237 60         346 my $parse_tree = $self->parse_path(\@lexemes);
238 60         219 $self->{parse_tree} = $parse_tree;
239              
240 60         282 return $self;
241             }
242             }
243              
244             sub lex {
245 60     60 0 116 my $self = shift;
246 60         113 my $expression = shift;
247 60         139 my @l = ( );
248              
249             # Digest expression into @l
250 60         222 while($expression) {
251 467 100       4044 if($expression =~ m/^\/\//) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
    50          
    50          
252 45         134 substr($expression,0,2) = '';
253 45         152 push @l, [ ALL, undef ];
254             } elsif($expression =~ m/^\//) {
255 51         117 substr($expression,0,1) = '';
256 51         192 push @l, [ CHILDREN, undef ];
257             } elsif($expression =~ m/^\|/) {
258 6         11 substr($expression,0,1) = '';
259 6         18 push @l, [ UNION, undef ];
260             } elsif($expression =~ m/^\&/) {
261 2         7 substr($expression,0,1) = '';
262 2         5 push @l, [ INTERSECT, undef ];
263             } elsif($expression =~ m/^\[/) {
264 52         98 substr($expression,0,1) = '';
265 52         146 push @l, [ L_SELECT, undef ];
266             } elsif($expression =~ m/^\]/) {
267 52         107 substr($expression,0,1) = '';
268 52         162 push @l, [ R_SELECT, undef ];
269             } elsif($expression =~ m/^(eq|lt|gt|le|ge|ne)/) {
270 11         40 push @l, [ S_CMP, $1 ];
271 11         28 substr($expression,0,2) = '';
272             } elsif($expression =~ m/^([#_\:a-zA-Z0-9]+)/) {
273 76         290 push @l, [ NAME, $1 ];
274 76         244 substr($expression, 0, length $1) = '';
275             } elsif($expression =~ m/^\@([a-zA-Z0-9]+)/) {
276 17         73 push @l, [ ATTR, $1 ];
277 17         81 substr($expression, 0, length( $1 ) + 1) = '';
278             } elsif($expression =~ m/^\((\-?[0-9]+)\)/) {
279 11         34 push @l, [ INDEX, $1 ];
280 11         54 substr($expression, 0, length( $1 ) + 2) = '';
281             } elsif($expression =~ m/^\{(([^\}]|\\\})+)\}([i]?)/) {
282 16 100       67 my $case = $3 eq 'i' ? 0 : 1;
283 16         57 push @l, [ REGEXP, $1, $case ];
284 16         61 substr($expression, 0, length( $1 ) + 2 + length($3)) = '';
285             } elsif($expression =~ m/^'(([^']|\\')+)'/) {
286 7         25 push @l, [ STRING, $1 ];
287 7         22 substr($expression, 0, length( $1 ) + 2) = '';
288             } elsif($expression =~ m/^\=\~/) {
289 16         47 push @l, [ MATCHES, undef ];
290 16         47 substr($expression, 0, 2) = '';
291             } elsif($expression =~ m/^\.\./) {
292 0         0 push @l, [ PARENT, undef ];
293 0         0 substr($expression, 0, 2) = '';
294             } elsif($expression =~ m/^\^/) {
295 2         6 push @l, [ ROOT, undef ];
296 2         6 substr($expression, 0, 1) = '';
297             } elsif($expression =~ m/^\./) {
298 15         45 push @l, [ NOP, undef ];
299 15         40 substr($expression, 0, 1) = '';
300             } elsif($expression =~ m/^\<\
301 2         6 push @l, [ PREV, undef ];
302 2         7 substr($expression, 0, 2) = '';
303             } elsif($expression =~ m/^\>\>/) {
304 1         4 push @l, [ NEXT, undef ];
305 1         3 substr($expression, 0, 2) = '';
306             } elsif($expression =~ m/^(==|!=|<=|>=)/) {
307 0         0 push @l, [ N_CMP, $1 ];
308 0         0 substr($expression,0,2) = '';
309             } elsif($expression =~ m/^(<|>)/) {
310 0         0 push @l, [ N_CMP, $1 ];
311 0         0 substr($expression,0,1) = '';
312             } elsif($expression =~ m/^\!/) {
313 0         0 push @l, [ NOT, undef ];
314 0         0 substr($expression, 0, 1) = '';
315             } elsif($expression =~ m/^\%/) {
316 0         0 push @l, [ NUM_OF, undef ];
317 0         0 substr($expression, 0, 1) = '';
318             } elsif($expression =~ m/^'([\^']*)'/) {
319 0         0 push @l, [ STRING, $1 ];
320 0         0 substr($expression, 0, length( $1 ) + 2) = '';
321             } elsif($expression =~ m/(\s+)/) {
322             # Discard uncaptured whitespace
323 85         904 substr($expression, 0, length($1)) = '';
324             } else {
325 0         0 die "Invalid token encountered - remaining string is $expression";
326             }
327             }
328 60         243 return @l;
329             }
330              
331             =head1 METHODS
332              
333             =head2 filter_unique
334              
335             It is possible during processing - especially using ^ or .. operators
336             - to generate many duplicate matches of the same nodes. Each pass
337             around the loop, we filter to unique nodes so that duplicates cannot
338             inflate more than one time.
339              
340             This effectively means that C (however awful that is) will match
341             one node only - just really inefficiently.
342              
343             =cut
344              
345             sub filter_unique {
346 1873     1873 1 2887 my $self = shift;
347 1873         2732 my $ilist = shift;
348 1873         2845 my $nlist = [ ];
349              
350 1873         2983 my %seen = ( );
351 1873         3316 foreach my $node (@$ilist) {
352 3314 100       7003 push @$nlist, $node unless $seen{$node->serial};
353 3314         7146 $seen{$node->serial} = 1;
354             }
355              
356 1873         4628 return $nlist;
357             }
358              
359             # Rec descent process of expression.
360             sub process {
361 960     960 0 1555 my $self = shift;
362 960         1987 my @nodes = @_;
363              
364 960         1696 my $pt = $self->{parse_tree};
365 960         1785 my $ilist = [ @nodes ];
366              
367 960   100     3763 while($pt && $pt->{action} ne 'end_select') {
368 1873         3315 my $action = $pt->{action};
369 1873         3008 my @args = ( );
370 1873 100       4231 if($pt->{arguments}) {
371 1017         1539 @args = @{$pt->{arguments}};
  1017         2416  
372             }
373 1873 50       5844 if($self->can($action)) {
374 1873         4328 $ilist = $self->$action($ilist, @args);
375 1873         3953 $ilist = $self->filter_unique($ilist);
376             } else {
377 0         0 warn "discarding '$action', can't do that";
378             }
379 1873         8710 $pt = $pt->{'next'};
380             }
381 960         2780 return @$ilist;
382             }
383              
384             sub select_name {
385 796     796 0 1210 my $self = shift;
386 796         1224 my $ilist = shift;
387 796         1556 my @names = @_;
388 796         1213 my $nlist = [ ];
389              
390 796         1494 my %names = map { $_ => 1 } @names;
  798         2352  
391              
392 796         2084 for(my $i = 0; $i < @$ilist; $i ++) {
393 1558 100       3693 if($names{$ilist->[$i]->type}) {
394 483         1319 push @$nlist, $ilist->[$i];
395             };
396             }
397 796         2009 return $nlist;
398             }
399              
400             sub select_union {
401 10     10 0 19 my $self = shift;
402 10         23 my $class = ref $self;
403              
404 10         13 my $ilist = shift;
405 10         28 my $left = shift;
406 10         17 my $right = shift;
407              
408 10         56 my $l_path = $class->new('union left', $left);
409 10         111 my $r_path = $class->new('union right', $right);
410              
411 10         27 my @l_result = $l_path->process(@$ilist);
412 10         55 my @r_result = $r_path->process(@$ilist);
413              
414 10         45 return [ @l_result, @r_result ];
415             }
416              
417             sub select_intersect {
418 5     5 0 12 my $self = shift;
419 5         12 my $class = ref $self;
420              
421 5         10 my $ilist = shift;
422 5         8 my $left = shift;
423 5         9 my $right = shift;
424              
425 5         15 my $l_path = $class->new("intersect left", $left);
426 5         13 my $r_path = $class->new("intersect right", $right);
427              
428 5         13 my @l_result = $l_path->process(@$ilist);
429 5         16 my @r_result = $r_path->process(@$ilist);
430              
431 5         10 my %seen = ( );
432 5         10 my $nlist = [ ];
433 5         10 foreach my $a (@l_result) {
434 14         31 $seen{$a->serial} = 1;
435             }
436 5         10 foreach my $b (@r_result) {
437 4 100       12 push @$nlist, $b if $seen{$b->serial};
438             }
439              
440 5         27 return $nlist;
441             }
442              
443             sub select_attr {
444 133     133 0 227 my $self = shift;
445 133         310 my $ilist = shift;
446 133         237 my $name = shift;
447 133         271 my $nlist = [ ];
448              
449 133         249 foreach my $i (@$ilist) {
450 232         592 my $pv = $i->param($name);
451 232 100       529 if($pv) {
452 222         517 push @$nlist, $pv;
453             }
454             }
455 133         350 return $nlist;
456             }
457              
458             sub select_index {
459 11     11 0 19 my $self = shift;
460 11         19 my $ilist = shift;
461 11         48 my $index = shift;
462              
463 11 100       35 if($index < scalar @$ilist) {
464 10 100       24 if($index >= 0) {
465 9         47 return [ $ilist->[$index] ];
466             } else { # Index < 0
467 1         5 my $neg = abs $index;
468 1 50       5 if($neg <= scalar @$ilist) {
469 1         5 return [ $ilist->[-$neg] ];
470             } else {
471 0         0 return [ ]; # Out of bounds
472             }
473             }
474             } else {
475 1         5 return [ ]; # Out of bounds
476             }
477             }
478              
479             sub match_expression {
480 62     62 0 132 my $self = shift;
481 62         105 my $ilist = shift;
482 62         104 my $test_action = shift;
483 62         104 my $invert = shift;
484 62         101 my $exp = shift;
485 62         109 my $r_exp = shift;
486              
487 62         105 my $op = shift; # Only for some operators
488              
489 62         110 my $nlist = [ ];
490 62         125 foreach my $n(@$ilist) {
491 836         1875 my @t_list = $exp->process($n);
492 836         1321 my $t_result;
493             # Allow for r_exp to be another expression - generate both
494             # node lists if required.
495 836 100       1401 if( eval { $r_exp->can('process') } ) {
  836         5522  
496 34         75 my @r_list = $r_exp->process($n);
497 34         135 $t_result = $self->$test_action(\@t_list, \@r_list, $op);
498             } else {
499 802         2197 $t_result = $self->$test_action(\@t_list, $r_exp, $op);
500             }
501 836 50       2061 $t_result = !$t_result if $invert;
502 836 100       1943 if($t_result) {
503 172         454 push @$nlist, $n;
504             }
505             }
506 62         228 return $nlist;
507             }
508              
509             sub test_cmp_op {
510 53     53 0 139 my $self = shift;
511 53         131 my $l_list = shift;
512 53         85 my $r_exp = shift;
513 53         96 my $op = shift;
514              
515 53 100 66     174 if(scalar(@$r_exp) == 0 || eval { $r_exp->[0]->isa('Pod::Abstract::Node') }) {
  53 50       421  
516             # combination test
517 34         56 my $match = 0;
518 34         69 foreach my $l (@$l_list) {
519 32         86 my $lb = $l->body;
520 32 50       126 $lb = $l->pod unless $lb;
521 32         71 foreach my $r (@$r_exp) {
522 127         409 my $rb = $r->body;
523 127 50       580 $rb = $r->pod unless $rb;
524 127         10387 eval "\$match++ if \$lb $op \$rb";
525 127 50       756 die $@ if $@;
526             }
527             }
528 34         116 return $match;
529             } elsif($r_exp->[0] == STRING) {
530             # simple string test
531 19         41 my $str = $r_exp->[1];
532 19         30 my $match = 0;
533 19         40 foreach my $l (@$l_list) {
534 19         61 my $lb = $l->body;
535 19 100       72 $lb = $l->pod unless $lb;
536 19         1632 eval "\$match++ if \$lb $op \$str";
537 19 50       143 die $@ if $@;
538             }
539 19         185 return $match;
540             } else {
541 0         0 die "Don't know what to do with ", Dumper([$r_exp]);
542             }
543             }
544              
545             sub test_regexp {
546 66     66 0 101 my $self = shift;
547 66         133 my $t_list = shift;
548 66         111 my $regexp_set = shift;
549 66         135 my $regexp = $regexp_set->[0];
550 66         108 my $case = $regexp_set->[1];
551 66 100       133 if($case) {
552 18         177 $regexp = qr/$regexp/;
553             } else {
554 48         282 $regexp = qr/$regexp/i;
555             }
556              
557 66         175 my $match = 0;
558 66         160 foreach my $t_n (@$t_list) {
559 56         175 my $body = $t_n->body;
560 56 100       199 $body = $t_n->pod unless defined $body;
561 56 100       412 if($body =~ $regexp) {
562 21         54 $match ++;
563             }
564             }
565 66         197 return $match;
566             }
567              
568             sub test_simple {
569 717     717 0 1256 my $self = shift;
570 717         1089 my $t_list = shift;
571              
572 717         1604 return (scalar @$t_list) > 0;
573             }
574              
575             sub select_children {
576 756     756 0 1132 my $self = shift;
577 756         1207 my $ilist = shift;
578 756         1179 my $nlist = [ ];
579              
580 756         1404 foreach my $n (@$ilist) {
581 757         1988 my @children = $n->children;
582 757         1868 push @$nlist, @children;
583             }
584              
585 756         1609 return $nlist;
586             }
587              
588             sub select_next {
589 5     5 0 9 my $self = shift;
590 5         10 my $ilist = shift;
591 5         10 my $nlist = [ ];
592              
593 5         11 foreach my $n (@$ilist) {
594 5         34 my $next = $n->next;
595 5 100       14 if($next) {
596 4         11 push @$nlist, $next;
597             }
598             }
599              
600 5         16 return $nlist;
601             }
602              
603             sub select_prev {
604 8     8 0 43 my $self = shift;
605 8         12 my $ilist = shift;
606 8         15 my $nlist = [ ];
607              
608 8         19 foreach my $n (@$ilist) {
609 8         25 my $prev = $n->previous;
610 8 100       38 if($prev) {
611 7         22 push @$nlist, $prev;
612             }
613             }
614              
615 8         38 return $nlist;
616             }
617              
618             sub select_parents {
619 0     0 0 0 my $self = shift;
620 0         0 my $ilist = shift;
621 0         0 my $nlist = [ ];
622 0         0 foreach my $n (@$ilist) {
623 0 0       0 if($n->parent) {
624 0         0 push @$nlist, $n->parent;
625             }
626             }
627              
628 0         0 return $nlist;
629             }
630              
631             sub select_root {
632 21     21 0 54 my $self = shift;
633 21         42 my $ilist = shift;
634 21         36 my $nlist = [ ];
635 21         42 foreach my $n (@$ilist) {
636 55         162 push @$nlist, $n->root; # almost certainly all the same - not
637             # efficient but consistent.
638             }
639              
640 21         56 return $nlist;
641             }
642              
643             sub select_current {
644 21     21 0 38 my $self = shift;
645 21         36 my $ilist = shift;
646 21         42 return $ilist;
647             }
648              
649             sub select_all {
650 45     45 0 91 my $self = shift;
651 45         74 my $ilist = shift;
652 45         80 my $nlist = [ ];
653              
654 45         102 foreach my $n (@$ilist) {
655 45         113 push @$nlist, $self->expand_all($n);
656             }
657              
658 45         185 return $nlist;
659             }
660              
661             sub expand_all {
662 1454     1454 0 2418 my $self = shift;
663 1454         2068 my $n = shift;
664              
665 1454         3086 my @children = $n->children;
666 1454         2232 my @r = ( );
667 1454         2376 foreach my $c (@children) {
668 1409         2223 push @r, $c;
669 1409         2788 push @r, $self->expand_all($c);
670             };
671              
672 1454         3802 return @r;
673             }
674              
675             =head2 parse_path
676              
677             Parse a list of lexemes and generate a driver tree for the process
678             method. This is a simple recursive descent parser with one element of
679             lookahead.
680              
681             =cut
682              
683             sub parse_path {
684 124     124 1 235 my $self = shift;
685 124         207 my $l = shift;
686              
687 124         340 my $left = $self->parse_l_path($l);
688              
689             # Handle UNION or INTERSECT operators
690 124         253 my $next = shift @$l;
691 124 100       261 if($next) {
692 64         109 my $tok = $next->[0];
693 64 100       175 if($tok == UNION) {
    100          
694             return {
695 6         19 action => "select_union",
696             arguments => [ $left, $self->parse_path($l) ],
697             };
698             } elsif($tok == INTERSECT) {
699             return {
700 2         7 action => "select_intersect",
701             arguments => [ $left, $self->parse_path($l) ],
702             }
703             } else {
704 56         101 unshift @$l, $next;
705 56         141 return $left;
706             }
707             } else {
708 60         161 return $left;
709             }
710             }
711              
712              
713             sub parse_l_path {
714 394     394 0 677 my $self = shift;
715 394         589 my $l = shift;
716              
717 394         706 my $next = shift @$l;
718 394 100       943 my $tok = $next->[0] if $next;
719 394 100       974 my $val = $next->[1] if $next;
720              
721             # Accept: / (children), // (all), name,
722 394 100       974 if(not defined $next) {
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    50          
723             return {
724 60         267 'action' => 'end_select',
725             };
726 2004         4678 } elsif(grep { $tok == $_ }
727             (MATCHES, R_SELECT, S_CMP, N_CMP, UNION, INTERSECT)) {
728 64         125 unshift @$l, $next;
729             return {
730 64         507 'action' => 'end_select',
731             };
732             } elsif($tok == CHILDREN) {
733             return {
734 51         178 'action' => 'select_children',
735             'next' => $self->parse_l_path($l),
736             };
737             } elsif($tok == ALL) {
738             return {
739 45         167 'action' => 'select_all',
740             'next' => $self->parse_l_path($l),
741             };
742             } elsif($tok == NEXT) {
743             return {
744 1         5 'action' => 'select_next',
745             'next' => $self->parse_l_path($l),
746             };
747             } elsif($tok == PREV) {
748             return {
749 2         8 'action' => 'select_prev',
750             'next' => $self->parse_l_path($l),
751             };
752             } elsif($tok == PARENT) {
753             return {
754 0         0 'action' => 'select_parents',
755             'next' => $self->parse_l_path($l),
756             };
757             } elsif($tok == ROOT) {
758             return {
759 2         8 'action' => 'select_root',
760             'next' => $self->parse_l_path($l),
761             };
762             } elsif($tok == NOP) {
763             return {
764 15         44 'action' => 'select_current',
765             'next' => $self->parse_l_path($l),
766             };
767             } elsif($tok == NAME) {
768 74         289 $self->check_name($val); # Dies on fail.
769 74         200 my @extra_names = $self->parse_names($l);
770             return {
771 74         334 'action' => 'select_name',
772             'arguments' => [ $val, @extra_names ],
773             'next' => $self->parse_l_path($l),
774             };
775             } elsif($tok == ATTR) {
776             return {
777 17         85 'action' => 'select_attr',
778             'arguments' => [ $val ],
779             'next' => $self->parse_l_path($l),
780             };
781             } elsif($tok == INDEX) {
782             return {
783 11         40 'action' => 'select_index',
784             'arguments' => [ $val ],
785             'next' => $self->parse_l_path($l),
786             };
787             } elsif($tok == L_SELECT) {
788 52         117 unshift @$l, $next;
789 52         165 my $exp = $self->parse_expression($l);
790 52         148 $exp->{'next'} = $self->parse_l_path($l);
791 52         356 return $exp;
792             } else {
793 0         0 die "Unexpected token, ", Dumper([$next]);
794             }
795             }
796              
797             sub parse_names {
798 74     74 0 135 my $self = shift;
799 74         114 my $l = shift;
800 74         133 my @r = ( );
801              
802             # Collect a list of names until there are no more.
803 74   100     414 while(@$l && $l->[0][0] == NAME) {
804 2         6 my $next = shift @$l;
805 2         7 my $val = $next->[1];
806              
807 2 50       6 return unless $self->check_name($val); # This is going to produce a die, unless told not to.
808              
809 2         12 push @r, $val;
810             }
811              
812 74         213 return @r;
813             }
814              
815             my %allow = (
816             head1 => 1,
817             head2 => 1,
818             head3 => 1,
819             head4 => 1,
820             head5 => 1,
821             head6 => 1,
822             pod => 1,
823             over => 1,
824             item => 1,
825             back => 1,
826             begin => 1,
827             for => 1,
828             end => 1,
829             '#cut' => 1,
830             ':verbatim' => 1,
831             ':text' => 1,
832             ':paragraph' => 1,
833              
834             # Formatting commands
835             ':L' => 1, # Link
836             ':X' => 1, # Index
837             ':B' => 1, # Bold
838             ':C' => 1, # Code
839             ':E' => 1, # Escape
840             ':I' => 1, # Italic
841             ':F' => 1, # Filename
842             ':Z' => 1, # Zero
843             ':S' => 1, # Non-breaking spaces
844             );
845              
846             sub check_name {
847 76     76 0 172 my $self = shift;
848 76         164 my $val = shift;
849              
850 76 50       249 if( $allow{$val} ) {
851 76         182 return 1;
852             }
853              
854 0 0       0 if( $val =~ m/^[A-Z]$/ ) {
855 0         0 die "Expression name $val looks like a formatting code, did you mean :$val?\n";
856             }
857              
858 0 0       0 if( $allow{":$val"} ) {
859 0         0 die "Expression $val invalid, did you mean :$val?\n";
860             }
861              
862 0         0 die "Invalid node expression $val\n";
863             }
864              
865             sub parse_expression {
866 52     52 0 119 my $self = shift;
867 52         130 my $class = ref $self;
868 52         117 my $l = shift;
869              
870 52         89 my $l_select = shift @$l;
871 52 50       158 die "Expected L_SELECT, got ", Dumper([$l_select])
872             unless $l_select->[0] == L_SELECT;
873              
874             # See if we lead with a NOT
875 52 50       139 if($l->[0][0] == NOT) {
876 0         0 shift @$l;
877 0         0 unshift @$l, $l_select;
878              
879 0         0 my $exp = $self->parse_expression($l);
880 0         0 $exp->{arguments}[1] = !$exp->{arguments}[1];
881 0         0 return $exp;
882             }
883              
884 52         140 my $l_exp = $self->parse_path($l);
885 52         188 $l_exp = $class->new("select expression",$l_exp);
886 52         135 my $op = shift @$l;
887 52         84 my $op_tok = $op->[0];
888 52         134 my $op_val = $op->[1];
889 52         115 my $exp = undef;
890              
891 52 100 66     255 if($op_tok == MATCHES) {
    100          
    50          
892 16         32 my $re = shift @$l;
893 16         31 my $re_tok = $re->[0];
894 16         63 my $re_str = $re->[1];
895 16         33 my $case_sensitive = $re->[2];
896              
897 16 50       44 if($re_tok == REGEXP) {
898 16         132 $exp = {
899             'action' => 'match_expression',
900             'arguments' => [ 'test_regexp', 0,
901             $l_exp,
902             [ $re_str, $case_sensitive ] ],
903             }
904             } else {
905 0         0 die "Expected REGEXP, got ", Dumper([$re_tok]);
906             }
907             } elsif($op_tok == S_CMP || $op_tok == N_CMP) {
908 11         23 my $rh = shift @$l;
909 11         21 my $rh_tok = $rh->[0];
910 11         20 my $r_exp = undef;
911              
912 11 100       28 if($rh_tok == STRING) { # simple string equality
913 7         12 $r_exp = $rh;
914             } else {
915 4         10 unshift @$l, $rh;
916 4         11 $r_exp = $self->parse_path($l);
917 4         13 $r_exp = $class->new("select expression",$r_exp);
918             }
919 11         53 $exp = {
920             action => 'match_expression',
921             arguments => [ 'test_cmp_op', 0,
922             $l_exp, $r_exp, $op_val ],
923             };
924             } elsif($op_tok == R_SELECT) {
925             # simple expression
926 25         64 unshift @$l, $op;
927 25         117 $exp = {
928             'action' => 'match_expression',
929             'arguments' => [ 'test_simple', 0, $l_exp ],
930             }
931             } else {
932 0         0 die "Expected MATCHES, got ", Dumper([$op_tok]);
933             }
934              
935             # Must match close of select;
936 52         105 my $r_select = shift @$l;
937 52 50       157 die "Expected R_SELECT, got, ", Dumper([$r_select])
938             unless $r_select->[0] == R_SELECT;
939 52 50       127 die "Failed to generate expression"
940             unless $exp;
941              
942             # All OK!
943 52         158 return $exp;
944             }
945              
946             =head1 AUTHOR
947              
948             Ben Lilburne
949              
950             =head1 COPYRIGHT AND LICENSE
951              
952             Copyright (C) 2009-2025 Ben Lilburne
953              
954             This program is free software; you can redistribute it and/or modify
955             it under the same terms as Perl itself.
956              
957             =cut
958              
959             1;
960