File Coverage

blib/lib/Pod/Query.pm
Criterion Covered Total %
statement 321 365 87.9
branch 139 204 68.1
condition 56 81 69.1
subroutine 35 37 94.5
pod 7 7 100.0
total 558 694 80.4


line stmt bran cond sub pod time code
1             package Pod::Query;
2              
3 7     7   486357 use v5.16;
  7         54  
4 7     7   46 use strict;
  7         28  
  7         167  
5 7     7   32 use warnings;
  7         13  
  7         202  
6 7     7   4186 use Pod::Text();
  7         338447  
  7         211  
7 7     7   3208 use Pod::LOL();
  7         60548  
  7         221  
8 7     7   2917 use File::Spec::Functions qw( catfile );
  7         5268  
  7         564  
9 7     7   56 use List::Util qw( first );
  7         15  
  7         839  
10 7     7   3550 use Text::ParseWords qw( parse_line );
  7         9790  
  7         435  
11 7     7   3005 use Term::Size::Any qw( chars );
  7         1610  
  7         51  
12              
13             =head1 NAME
14              
15             Pod::Query - Query pod documents
16              
17             =head1 VERSION
18              
19             Version 0.37
20              
21             =cut
22              
23             our $VERSION = '0.37';
24             our $DEBUG_LOL_DUMP = 0;
25             our $DEBUG_STRUCT_OVER = 0;
26             our $DEBUG_TREE = 0;
27             our $DEBUG_TREE_DUMP = 0;
28             our $DEBUG_FIND_CONDITIONS = 0;
29             our $DEBUG_FIND_AFTER_DEFAULTS = 0;
30             our $DEBUG_PRE_FIND_DUMP = 0;
31             our $DEBUG_FIND = 0;
32             our $DEBUG_FIND_DUMP = 0;
33             our $DEBUG_INVERT = 0;
34             our $DEBUG_RENDER = 0;
35             our $MOCK_ROOT = 0;
36              
37             =head1 SYNOPSIS
38              
39             Query POD information from a file
40              
41             % perl -MPod::Query -E 'say for Pod::Query->new("ojo")->find("head1[0]")'
42              
43             NAME
44             ojo - Fun one-liners with Mojo
45              
46             % perl -MPod::Query -E 'say Pod::Query->new("ojo")->find("head1[0]/Para[0]")'
47              
48             ojo - Fun one-liners with Mojo
49              
50             % perl -MPod::Query -E 'say Pod::Query->new(shift)->find("head1[0]/Para[0]")' my.pod
51              
52             Find Methods:
53              
54             find_title;
55             find_method;
56             find_method_summary;
57             find_events;
58             find($query_sting);
59             find(@query_structs);
60              
61             =head1 DESCRIPTION
62              
63             This module takes a class name, extracts the POD
64             and provides methods to query specific information.
65              
66             =head1 SUBROUTINES/METHODS
67              
68             =cut
69              
70             #
71             # Method maker
72             #
73              
74             =head2 _has
75              
76             Generates class accessor methods (like Mojo::Base::attr)
77              
78             =cut
79              
80             sub _has {
81 7     7   10481 no strict 'refs';
  7         23  
  7         32909  
82 7     7   26 for my $attr ( @_ ) {
83             *$attr = sub {
84 155 100   155   613 return $_[0]{$attr} if @_ == 1; # Get: return $self-<{$attr}
85 27         82 $_[0]{$attr} = $_[1]; # Set: $self->{$attr} = $val
86 27         36 $_[0]; # return $self
87 28         231 };
88             }
89             }
90              
91             =head2 path
92              
93             Path to the pod class file
94              
95             =head2 lol
96              
97             List of lists (LOL) structure of the pod file.
98             Result of Pod::LOL.
99              
100             =head2 tree
101              
102             An hierarchy is added to the lol to create a
103             tree like structure of the pod file.
104              
105             =head2 class_is_path
106              
107             Flag to indicate if the class is really a path to the file.
108              
109             =cut
110              
111             sub import {
112 7     7   111 _has qw(
113             path
114             lol
115             tree
116             class_is_path
117             );
118             }
119              
120             #
121             # Debug
122             #
123              
124             sub _dumper {
125 0     0   0 require Data::Dumper;
126 0         0 my $data = Data::Dumper
127             ->new( [@_] )
128             ->Indent( 1 )
129             ->Sortkeys( 1 )
130             ->Terse( 1 )
131             ->Useqq( 1 )
132             ->Dump;
133 0 0       0 return $data if defined wantarray;
134 0         0 say $data;
135             }
136              
137             =head2 new
138              
139             Create a new object.
140             Return value is cached (based on the class of the pod file).
141              
142             use Pod::Query;
143             my $pod = Pod::Query->new('Pod::LOL', PATH_ONLY=0);
144              
145             PATH_ONLY can be used to determine the path to the pod
146             document without having to do much unnecessary work.
147              
148             =cut
149              
150             sub new {
151 9     9 1 8943 my ( $class, $pod_class, $path_only ) = @_;
152 9   50     56 $path_only //= 0;
153 9         12 state %CACHE;
154              
155 9         12 my $cached;
156 9 50       28 return $cached if $cached = $CACHE{$pod_class};
157              
158 9         34 my $s = bless {
159             pod_class => $pod_class,
160             lol => [],
161             tree => [],
162             }, $class;
163              
164 9         26 $s->path( $s->_class_to_path( $pod_class ) );
165              
166 9 50 33     30 return $s if $path_only or not $s->path;
167              
168 9 50       34 my $lol = $MOCK_ROOT ? _mock_root() : Pod::LOL->new_root( $s->path );
169 9         506 $lol = _flatten_for_tags( $lol );
170 9 50       37 if ( $DEBUG_LOL_DUMP ) {
171 0         0 say "DEBUG_LOL_DUMP: " . _dumper $lol;
172 0         0 exit;
173             }
174              
175 9         37 $s->lol( $lol );
176 9         21 $s->tree( _lol_to_tree( $lol ) );
177 9 50       22 if ( $DEBUG_TREE_DUMP ) {
178 0         0 say "DEBUG_TREE_DUMP: " . _dumper $s->tree();
179 0         0 exit;
180             }
181              
182 9         24 $CACHE{$pod_class} = $s;
183              
184 9         32 $s;
185             }
186              
187             =head2 _class_to_path
188              
189             Given a class name, returns the path to the pod file.
190             Return value is cached (based on the class of the pod file).
191              
192             If the class is not found in INC, it will be checked whether
193             the input is an existing file path.
194              
195             Returns an empty string if there are any errors.
196              
197             =cut
198              
199             sub _class_to_path {
200 4     4   2930 my ( $s, $pod_class ) = @_;
201 4         6 state %CACHE;
202 4         5 my $path;
203              
204 4 50       43 return $path if $path = $CACHE{$pod_class};
205              
206 4         26 my $partial_path = catfile( split /::/, $pod_class ) . '.pm';
207              
208             # Shortcut for files already used.
209 4         11 $path = $INC{$partial_path};
210 4 50 33     12 return $CACHE{$pod_class} = $path if $path and -f $path;
211              
212             # Otherwise find it ourselves.
213 4         8 for ( @INC ) {
214 22         106 $path = catfile( $_, $partial_path );
215 22 100 66     372 return $CACHE{$pod_class} = $path if $path and -f $path;
216             }
217              
218             # Check for it in PATH also.
219             # Maybe pod_class is the path.
220 2         15 for ( "", split /:/, $ENV{PATH} ) {
221              
222             # Absolute path or current folder means class is path.
223 11 100 66     63 $path = ( $_ and $_ ne "." ) ? catfile( $_, $pod_class ) : $pod_class;
224 11 100 66     158 if ( $path and -f $path ) {
225 1 50       5 $s->class_is_path( 1 ) if ref $s;
226 1         7 return $CACHE{$pod_class} = $path;
227             }
228             }
229              
230 1         6 return "";
231             }
232              
233             =head2 _mock_root
234              
235             For debugging and/or testing.
236             Builds a sample object (overwrite this in a test file).
237              
238             =cut
239              
240       0     sub _mock_root { }
241              
242             =head2 _flatten_for_tags
243              
244             Removes for tags from the lol and flattens
245             out the inner tags to be on the same level as the for
246             tag was.
247              
248             =cut
249              
250             sub _flatten_for_tags {
251 9     9   18 my ( $lol ) = @_;
252 9         13 my @flat;
253              
254 9         20 for ( @$lol ) {
255 322         494 my ( $tag, @data ) = @$_;
256 322   50     490 $tag //= '';
257              
258 322 50       582 push @flat, ( $tag eq "for" ) ? @data : $_;
259             }
260              
261 9         25 \@flat;
262             }
263              
264             =head2 _lol_to_tree
265              
266             Generates a tree from a Pod::LOL object.
267             The structure of the tree is based on the N (level) in "=headN".
268              
269             This example pod:
270              
271             =head1 FUNCTIONS
272              
273             =Para Description of Functions
274              
275             =head2 Function1
276              
277             =Para Description of Function1
278              
279             =head1 AUTHOR
280              
281             =cut
282              
283             This will be grouped as:
284              
285             =head1 FUNCTIONS
286             =Para Description of Functions
287             =head2 Function1
288             =Para Description of Function1
289             =head1 AUTHOR
290              
291             In summary:
292              
293             =over 2
294              
295             =item *
296              
297             Non "head" tags are always grouped "below".
298              
299             =item *
300              
301             HeadN tags with a higher N with also be grouped below.
302              
303             =item *
304              
305             HeadN tags with the same or lower N will be grouped higher.
306              
307             =back
308              
309             =cut
310              
311             sub _lol_to_tree {
312 14     14   25 my ( $lol ) = @_;
313 14         19 my ( $is_in, $is_out );
314 14         38 my %heads_table = __PACKAGE__->_define_heads_regex_table();
315 14         46 my $is_head = qr/ ^ head (\d) $ /x;
316 14         26 my $node = {};
317 14         21 my @tree;
318              
319             my $push = sub { # push to tree.
320 107 100   107   187 return if not %$node;
321 93         130 my $kids = $node->{kids};
322             $node->{kids} = _lol_to_tree( $kids )
323 93 100 100     435 if ref( $kids ) && first { $_->{tag} =~ /$is_head/ } @$kids;
  238         656  
324 93         251 push @tree, $node;
325 93         153 $node = {};
326 14         59 };
327              
328 14 50       46 say "\n_ROOT_TO_TREE()" if $DEBUG_TREE;
329              
330 14         35 for ( @$lol ) {
331 545 50       875 say "\n_=", _dumper $_ if $DEBUG_TREE;
332              
333 545         805 my $leaf = _make_leaf( $_ );
334 545 50       928 say "\nleaf=", _dumper $leaf if $DEBUG_TREE;
335              
336             # Outer tag.
337 545 100 100     2278 if ( not $is_in or $leaf->{tag} =~ /$is_out/ ) {
338 93         192 $push->();
339 93         132 $node = $leaf;
340 93 100       410 if ( $leaf->{tag} =~ /$is_head/ ) {
341 88         114 ( $is_in, $is_out ) = @{$heads_table{$1}};
  88         265  
342             }
343             }
344             else {
345 452         595 push @{$node->{kids}}, $leaf;
  452         767  
346 452 50       958 say "node: ", _dumper $node if $DEBUG_TREE;
347             }
348             }
349              
350 14         33 $push->();
351              
352 14         168 \@tree;
353             }
354              
355             =head2 _define_heads_regex_table
356              
357             Generates the regexes for head elements inside
358             and outside the current head.
359              
360             =cut
361              
362             sub _define_heads_regex_table {
363             map {
364 14     14   31 my $inner = join "", $_ + 1 .. 5; # num=2, inner=345
  56         201  
365 56         166 my $outer = join "", 0 .. $_; # num=2, outer=012
366              
367 56         102 $_ => [ map { qr/ ^ head ([$_]) $ /x } $inner, $outer ]
  112         1584  
368             } 1 .. 4;
369             }
370              
371             =head2 _make_leaf
372              
373             Creates a new node (aka leaf).
374              
375             =cut
376              
377             sub _make_leaf {
378 545     545   774 my ( $node ) = @_;
379 545 100       1305 return $node if ref $node eq ref {};
380              
381 322         639 my ( $tag, @text ) = @$node;
382 322         649 my $leaf = { tag => $tag };
383              
384 322 100       590 if ( $tag =~ / ^ over- /x ) {
385 7         15 $leaf->{kids} = _structure_over( \@text );
386 7         14 $leaf->{text} = "";
387             }
388             else {
389 315         874 $leaf->{text} = join "", @text;
390             }
391              
392 322         564 $leaf;
393             }
394              
395             =head2 _structure_over
396              
397             Restructures the text for an "over-text" element to be under it.
398             Also, "item-text" will be the first element of each group.
399              
400             =cut
401              
402             sub _structure_over {
403 7     7   12 my ( $text_list ) = @_;
404 7         8 my @struct;
405             my @nodes;
406              
407             my $push = sub {
408 74 100   74   139 return if not @nodes;
409              
410             # First is the parent node.
411 67         87 my $item_text = shift @nodes;
412              
413             # Treat the rest of the tags as kids.
414 67 100       363 push @struct,
415             { %$item_text, @nodes ? ( kids => [ splice @nodes ] ) : (), };
416 7         50 };
417              
418 7         16 for ( @$text_list ) {
419 242         440 my ( $tag, @text ) = @$_;
420 242 100       515 $push->() if $tag =~ / ^ item- /x;
421 242         1246 push @nodes,
422             {
423             tag => $tag,
424             text => join( "", @text ),
425             };
426             }
427              
428 7         16 $push->();
429              
430 7 50       18 if ( $DEBUG_STRUCT_OVER ) {
431 0         0 say "DEBUG_STRUCT_OVER-IN: " . _dumper $text_list;
432 0         0 say "DEBUG_STRUCT_OVER-OUT: " . _dumper \@struct;
433             }
434              
435 7         27 \@struct;
436             }
437              
438             =head2 find_title
439              
440             Extracts the title information.
441              
442             =cut
443              
444             sub find_title {
445 9     9 1 127275 my ( $s ) = @_;
446 9         29 scalar $s->find( 'head1=NAME[0]/Para[0]' );
447             }
448              
449             =head2 find_method
450              
451             Extracts the complete method information.
452              
453             =cut
454              
455             sub find_method {
456 10     10 1 6027 my ( $s, $method ) = @_;
457 10 50       23 my $m = $s->_clean_method_name( $method ) or return "";
458              
459 10         57 $s->find( sprintf '~head=~^%s\b.*$[0]**', $m );
460             }
461              
462             =head2 find_method_summary
463              
464             Extracts the method summary.
465              
466             =cut
467              
468             sub find_method_summary {
469 10     10 1 2982 my ( $s, $method ) = @_;
470 10 50       23 my $m = $s->_clean_method_name( $method ) or return "";
471              
472 10         50 scalar $s->find( sprintf '~head=~^%s\b.*$[0]/~(Data|Para)[0]', $m );
473             }
474              
475             =head2 _clean_method_name
476              
477             Returns a method name without any possible parenthesis.
478              
479             =cut
480              
481             sub _clean_method_name {
482 20     20   32 my ( $s, $name ) = @_;
483 20         67 my $safe_start = qr/ ^ [\w_] /x;
484 20         43 my $safe_end = qr/ [\w_()] $ /x;
485 20 50       121 return if $name !~ $safe_start;
486 20 50       76 return if $name !~ $safe_end;
487              
488 20         67 my $clean = quotemeta( $name =~ s/[^a-zA-Z0-9_]+//gr );
489 20 50       66 return if $clean !~ $safe_start;
490              
491 20         74 $clean;
492             }
493              
494             =head2 find_events
495              
496             Extracts a list of events with a description.
497              
498             Returns a list of key value pairs.
499              
500             =cut
501              
502             sub find_events {
503 6     6 1 1899 my ( $s ) = @_;
504 6         16 $s->find( '~head=EVENTS[0]/~head*/(Para)[0]' );
505             }
506              
507             =head2 find
508              
509             Generic extraction command.
510              
511             Note: This function is Scalar/List context sensitive!
512              
513             $query->find($condition)
514              
515             Where condtion is a string as described in L
516              
517             $query->find(@conditions)
518              
519             Where each condition can contain:
520              
521             {
522             tag => "TAG_NAME", # Find all matching tags.
523             text => "TEXT_NAME", # Find all matching texts.
524             keep => 1, # Capture the text.
525             keep_all => 1, # Capture entire section.
526             nth => 0, # Use only the nth match.
527             nth_in_group => 0, # Use only the nth matching group.
528             }
529              
530             Return contents of entire head section:
531              
532             find (
533             {tag => "head", text => "a", keep_all => 1},
534             )
535              
536             Results:
537              
538             [
539             " my \$app = a('/hel...",
540             {text => "Create a route with ...", wrap => 1},
541             " \$ perl -Mojo -E ...",
542             ]
543              
544             =cut
545              
546             sub find {
547 123     123 1 64292 my ( $s, @raw_conditions ) = @_;
548              
549 123         198 my $find_conditions;
550              
551             # If the find condition is a single string.
552 123 50 33     583 if ( @raw_conditions == 1 and not ref $raw_conditions[0] ) {
553 123         278 $find_conditions = $s->_query_string_to_struct( $raw_conditions[0] );
554             }
555             else {
556 0         0 $find_conditions = \@raw_conditions;
557             }
558 123 50       269 say "DEBUG_FIND_CONDITIONS: " . _dumper $find_conditions
559             if $DEBUG_FIND_CONDITIONS;
560              
561 123         278 _check_conditions( $find_conditions );
562 119         265 _set_condition_defaults( $find_conditions );
563 119 50       221 say "DEBUG_FIND_AFTER_DEFAULTS " . _dumper $find_conditions
564             if $DEBUG_FIND_AFTER_DEFAULTS;
565              
566 119         148 my @tree = @{$s->tree};
  119         258  
567 119         177 my $kept_all;
568 119 50       219 if ( $DEBUG_PRE_FIND_DUMP ) {
569 0         0 say "DEBUG_PRE_FIND_DUMP: " . _dumper \@tree;
570 0         0 exit;
571             }
572              
573 119         204 for ( @$find_conditions ) {
574 206         377 @tree = _find( $_, @tree );
575 206 100       460 if ( $_->{keep_all} ) {
576 18         29 $kept_all++;
577 18         27 last;
578             }
579             }
580 119 50       212 if ( $DEBUG_FIND_DUMP ) {
581 0         0 say "DEBUG_FIND_DUMP: " . _dumper \@tree;
582 0 0       0 exit if $DEBUG_FIND_DUMP > 1;
583             }
584              
585 119 100       262 if ( not $kept_all ) {
586 101         227 @tree = _invert( @tree );
587             }
588              
589 119         244 _render( $kept_all, @tree );
590             }
591              
592             =head2 _query_string_to_struct
593              
594             Convert a pod query string into a structure based on these rules:
595              
596             1. Split string by '/'.
597             Each piece is a separate list of conditions.
598              
599             2. Remove an optional '*' or '**' from the last condition.
600             Keep is set if we have '*'.
601             Keep all is set if we have '**'.
602              
603             3. Remove an optional [N] from the last condition.
604             (Where N is a decimal).
605             Set nth base on 'N'.
606             Set nth_in_group if previous word is surrounded by ():
607             (WORD)[N]
608              
609             4. Double and single quotes are removed from the ends (if matching).
610              
611             5. Split each list of conditions by "=".
612             First word is the tag.
613             Second word is the text (if any).
614             If either starts with a tilde, then the word:
615             - is treated like a pattern.
616             - is case Insensitive.
617              
618             Precedence:
619             If quoted and ~, left wins:
620             ~"head1" => qr/"head"/,
621             "~head1" => qr/head/,
622              
623             =cut
624              
625             sub _query_string_to_struct {
626 196     196   33684 my ( $s, $query_string ) = @_;
627 196         693 my $is_nth = qr/ \[ (-?\d+) \] $ /x;
628 196         1024 my $is_nth_in_group = qr/ ^ \( (.+) \) $is_nth /x;
629 196         486 my $is_keep = qr/ \* $ /x;
630 196         430 my $is_keep_all = qr/ \* \* $ /x;
631              
632             my @query_struct =
633             map {
634 316         802 my @condition = parse_line( '=', "1", $_ );
635 316         23707 my $set = {};
636              
637             # Set flags based on last condition.
638 316         646 for ( $condition[-1] ) {
639 316 100       1673 if ( s/$is_keep_all// ) {
    100          
640 25         65 $set->{keep_all}++;
641             }
642             elsif ( s/$is_keep// ) {
643 28         71 $set->{keep}++;
644             }
645              
646 316 100       2083 if ( s/$is_nth_in_group// ) {
    100          
647 30         73 $_ = $1;
648 30         92 $set->{nth_in_group} = $2;
649             }
650             elsif ( s/$is_nth// ) {
651 205         748 $set->{nth} = $1;
652             }
653             }
654              
655             # Remove outer quotes (if any).
656 316         553 for ( @condition ) {
657 435         588 for my $quote ( qw/ " ' / ) {
658 866 100 66     1999 if ( $quote eq substr( $_, 0, 1 )
659             and $quote eq substr( $_, -1 ) )
660             {
661 12         24 $_ = substr( $_, 1, -1 ); # Strip first and last characters.
662 12         26 last; # Skip multi quoting.
663             }
664             }
665             }
666              
667             # Regex or literal.
668 316         473 for ( qw/ tag text / ) {
669 632 100       1327 last if not @condition;
670 435         731 my $cond = shift @condition;
671 435 100       2652 $set->{$_} = ( $cond =~ s/^~// ) ? qr/$cond/i : $cond;
672             }
673              
674 316         920 $set;
675             }
676 196         557 grep { $_ } # Skip trailing and leading slashes.
  325         24700  
677             parse_line( '/', 1, $query_string );
678              
679 196         1119 \@query_struct;
680             }
681              
682             =head2 _check_conditions
683              
684             Check if queries are valid.
685              
686             =cut
687              
688             sub _check_conditions {
689 123     123   197 my ( $sections ) = @_;
690              
691 123         179 my $error_message = <<'ERROR';
692              
693             Invalid input: expecting a hash reference!
694              
695             Syntax:
696              
697             $pod->find( 'QUERY' ) # As explained in _query_string_to_struct().
698              
699             # OR:
700              
701             $pod->find(
702             # section1
703             {
704             tag => "TAG", # Search to look for.
705             text => "TEXT", # Text of the tag to find.
706             keep => 1, # Must only be in last section.
707             keep_all => 1, # Keep this tag and sub tags.
708             nth => 0, # Stop searching after find so many matches.
709             nth_in_group => 0, # Nth only in the current group.
710             },
711             # ...
712             # conditionN
713             );
714             ERROR
715              
716             die "$error_message"
717             if not $sections
718             or not @$sections
719 123 100 66     586 or grep { ref() ne ref {} } @$sections;
  206   66     789  
720              
721             # keep_all should only be in the last section
722 119         205 my $last = $#$sections;
723 119         452 while ( my ( $n, $section ) = each @$sections ) {
724             die "Error: keep_all is not in last query!\n"
725 206 50 66     717 if $section->{keep_all} and $n < $last;
726             }
727              
728             # Cannot use both nth and nth_in_group (makes no sense, plus may cause errors)
729 119         346 while ( my ( $n, $section ) = each @$sections ) {
730             die "Error: nth and nth_in_group are exclusive!\n"
731             if defined $section->{nth}
732 206 50 66     804 and defined $section->{nth_in_group};
733             }
734             }
735              
736             =head2 _set_condition_defaults
737              
738             Assigns default query options.
739              
740             =cut
741              
742             sub _set_condition_defaults {
743 119     119   187 my ( $conditions ) = @_;
744 119         206 for my $condition ( @$conditions ) {
745              
746             # Text Options
747 206         301 for ( qw/ tag text / ) {
748 412 100       764 if ( defined $condition->{$_} ) {
749 283 100       983 if ( ref $condition->{$_} ne ref qr// ) {
750 155         1396 $condition->{$_} = qr/^$condition->{$_}$/;
751             }
752             }
753             else {
754 129         323 $condition->{$_} = qr//;
755             }
756             }
757              
758             # Bit Options
759 206         332 for ( qw/ keep keep_all / ) {
760 412 100       654 if ( defined $condition->{$_} ) {
761 36         119 $condition->{$_} = !!$condition->{$_};
762             }
763             else {
764 376         597 $condition->{$_} = 0;
765             }
766             }
767              
768             # Range Options
769 206         429 my $is_digit = qr/ ^ -?\d+ $ /x;
770 206         359 for ( qw/ nth nth_in_group / ) {
771 412         681 my $v = $condition->{$_};
772 412 100 66     1685 if ( defined $v and $v =~ /$is_digit/ ) {
773 156   100     582 $v ||= "0 but true";
774 156 100       401 my $end = ( $v >= 0 ) ? "pos" : "neg"; # Set negative or
775 156         320 my $name = "_${_}_$end"; # positive form.
776 156         395 $condition->{$name} = $v;
777             }
778             }
779              
780             }
781              
782             # Last condition should be keep or keep_all.
783             # (otherwise, why even query for it?)
784 119         246 for ( $conditions->[-1] ) {
785 119 100 100     417 if ( not $_->{keep} || $_->{keep_all} ) {
786 93         171 $_->{keep} = 1;
787             }
788             }
789             }
790              
791             =head2 _find
792              
793             Lower level find command.
794              
795             =cut
796              
797             sub _find {
798 206     206   373 my ( $need, @groups ) = @_;
799 206 50       364 if ( $DEBUG_FIND ) {
800 0         0 say "\n_FIND()";
801 0         0 say "need: ", _dumper $need;
802 0         0 say "groups: ", _dumper \@groups;
803             }
804              
805 206         309 my $nth_p = $need->{_nth_pos}; # Simplify code by already
806 206         277 my $nth_n = $need->{_nth_neg}; # knowing if neg or pos.
807 206         275 my $nth_in_group_p = $need->{_nth_grou_pos}; # Set in _set_section_defaults.
808 206         258 my $nth_in_group_n = $need->{_nth_grou_neg};
809 206         264 my @found;
810              
811             GROUP:
812 206         306 for my $group ( @groups ) {
813 562         817 my @tries = ( $group ); # Assume single group to process.
814 562   100     630 my @prev = @{ $group->{prev} // [] };
  562         1598  
815 562         798 my $locked_prev = 0;
816 562         646 my @found_in_group;
817 562 50       907 if ( $DEBUG_FIND ) {
818 0         0 say "\nprev: ", _dumper \@prev;
819 0         0 say "group: ", _dumper $group;
820             }
821              
822             TRY:
823 562         1023 while ( my $try = shift @tries ) { # Can add to this queue if a sub tag.
824 4834 50       7261 say "\nTrying: try=", _dumper $try if $DEBUG_FIND;
825              
826 4834 50       7508 if ( defined $try->{text} ) { # over-text has no text (only kids).
827 4834 50 100     20934 if ( $DEBUG_FIND ) {
    100 100        
828 0         0 say "text=$try->{text}";
829 0         0 say "next->{tag}=$need->{tag}";
830 0         0 say "next->{text}=$need->{text}";
831             }
832              
833             elsif (
834             $try->{tag} =~ /$need->{tag}/
835             and $try->{text} =~ /$need->{text}/
836             and not defined $try->{keep} # Already found the node.
837             # Since nodes are checked again
838             # on next call to _find.
839             )
840             {
841 546 50       993 say "Found: tag=$try->{tag}, text=$try->{text}"
842             if $DEBUG_FIND;
843             push @found_in_group, {
844             %$try, # Copy current search options.
845             prev => \@prev, # Need this for the inversion step.
846             keep => $need->{keep}, # Remember for later.
847 546         1900 };
848              
849             # Specific match (positive)
850 546 50       1099 say "nth_p:$nth_p and found_in_group:"
851             . _dumper \@found_in_group
852             if $DEBUG_FIND;
853 546 100 100     1560 if ( $nth_p and @found + @found_in_group > $nth_p ) {
    50 33        
854 120 50       207 say "ENFORCING: nth=$nth_p" if $DEBUG_FIND;
855 120         197 @found = $found_in_group[-1];
856 120         303 last GROUP;
857             }
858              
859             # Specific group match (positive)
860             elsif ( $nth_in_group_p
861             and @found_in_group > $nth_in_group_p )
862             {
863 0 0       0 say "ENFORCING: nth_in_group=$nth_in_group_p"
864             if $DEBUG_FIND;
865 0         0 @found_in_group = $found_in_group[-1];
866 0         0 last TRY;
867             }
868              
869 426         960 next TRY;
870             }
871             }
872              
873 4288 100 100     11072 if ( $try->{kids} and not @found_in_group ) {
874 1227 50       1942 say "Got kids and nothing yet in queue" if $DEBUG_FIND;
875 1227         1403 unshift @tries, @{$try->{kids}}; # Process kids tags.
  1227         2088  
876 1227 100 66     2247 if ( $try->{keep} and not $locked_prev++ ) {
877             unshift @prev,
878             {
879 8         16 map { $_ => $try->{$_} }
  24         56  
880             qw/tag text keep/
881             };
882 8 50       18 say "prev changed: ", _dumper \@prev if $DEBUG_FIND;
883             }
884 1227 50       2836 say "locked_prev: $locked_prev" if $DEBUG_FIND;
885             }
886             }
887              
888             # Specific group match (negative)
889 442 50 33     766 if ( $nth_in_group_n and @found_in_group >= abs $nth_in_group_n ) {
890 0 0       0 say "ENFORCING: nth_in_group_n=$nth_in_group_n" if $DEBUG_FIND;
891 0         0 @found_in_group = $found_in_group[$nth_in_group_n];
892             }
893              
894 442 100       1045 push @found, splice @found_in_group if @found_in_group;
895             }
896              
897             # Specific match (negative)
898 206 100 66     417 if ( $nth_n and @found >= abs $nth_n ) {
899 4 50       10 say "ENFORCING: nth=$nth_n" if $DEBUG_FIND;
900 4         24 @found = $found[$nth_n];
901             }
902              
903 206 50       350 say "found: ", _dumper \@found if $DEBUG_FIND;
904              
905 206         570 @found;
906             }
907              
908             =head2 _invert
909              
910             Previous elements are inside of the child
911             (due to the way the tree is created).
912              
913             This method walks through each child and puts
914             the parent in its place.
915              
916             =cut
917              
918             sub _invert {
919 101     101   166 my ( @groups ) = @_;
920 101 50       165 if ( $DEBUG_INVERT ) {
921 0         0 say "\n_INVERT()";
922 0         0 say "groups: ", _dumper \@groups;
923             }
924              
925 101         138 my @tree;
926             my %navi;
927              
928 101         160 for my $group ( @groups ) {
929             push @tree, {
930 379         550 map { $_ => $group->{$_} }
  1516         2728  
931             qw/ tag text keep kids /
932             };
933 379 50       741 if ( $DEBUG_INVERT ) {
934 0         0 say "\nInverting: group=", _dumper $group;
935 0         0 say "tree: ", _dumper \@tree;
936             }
937              
938 379   50     659 my $prevs = $group->{prev} // [];
939 379         560 for my $prev ( @$prevs ) {
940 8         13 my $prev_node = $navi{$prev};
941 8 50       15 if ( $DEBUG_INVERT ) {
942 0         0 say "prev: ", _dumper $prev;
943 0         0 say "prev_node: ", _dumper $prev_node;
944             }
945 8 50       13 if ( $prev_node ) {
946 0         0 push @$prev_node, pop @tree;
947 0 0       0 if ( $DEBUG_INVERT ) {
948 0         0 say "FOUND: prev_node=", _dumper $prev_node;
949             }
950 0         0 last;
951             }
952             else {
953 8         23 $prev_node = $navi{$prev} = [ $tree[-1] ];
954 8         29 $tree[-1] = { %$prev, kids => $prev_node };
955 8 50       20 if ( $DEBUG_INVERT ) {
956 0         0 say "NEW: prev_node=", _dumper $prev_node;
957             }
958             }
959             }
960              
961 379 50       661 say "tree end: ", _dumper \@tree if $DEBUG_INVERT;
962             }
963              
964 101         387 @tree;
965             }
966              
967             =head2 _render
968              
969             Transforms a tree of found nodes in a simple list
970             or a string depending on context.
971              
972             Pod::Text formatter is used for C tags when C is set.
973              
974             =cut
975              
976             sub _render {
977 119     119   219 my ( $kept_all, @tree ) = @_;
978 119 50       211 if ( $DEBUG_RENDER ) {
979 0         0 say "\n_RENDER()";
980 0         0 say "tree: ", _dumper \@tree;
981 0         0 say "kept_all: ", _dumper $kept_all;
982             }
983              
984 119         302 my $formatter = Pod::Text->new( width => get_term_width(), );
985 119         13950 $formatter->{MARGIN} = 2;
986              
987 119         204 my @lines;
988             my $n;
989              
990 119         195 for my $group ( @tree ) {
991 419         587 my @tries = ( $group );
992 419 50       713 say "\ngroup: ", _dumper $group if $DEBUG_RENDER;
993              
994 419         730 while ( my $try = shift @tries ) {
995 2839 50       4310 say "\nTrying: try=", _dumper $try if $DEBUG_RENDER;
996              
997 2839         3604 my $_text = $try->{text};
998 2839 50       4144 say "_text=$_text" if $DEBUG_RENDER;
999              
1000 2839 100       4844 if ( $kept_all ) {
    100          
1001 548 100       906 $_text .= ":" if ++$n == 1; # Only for the first line.
1002 548 100       900 if ( $try->{tag} eq "Para" ) {
1003 164 50       258 say "USING FORMATTER" if $DEBUG_RENDER;
1004 164         342 $_text = $formatter->reformat( $_text );
1005             }
1006 548         7698 push @lines, $_text, "";
1007             }
1008             elsif ( $try->{keep} ) {
1009 387 50       580 say "keeping" if $DEBUG_RENDER;
1010 387         568 push @lines, $_text;
1011             }
1012              
1013 2839 100       5898 if ( $try->{kids} ) {
1014 738         860 unshift @tries, @{$try->{kids}};
  738         1151  
1015 738 50       1687 if ( $DEBUG_RENDER ) {
1016 0         0 say "Got kids";
1017 0         0 say "tries: ", _dumper \@tries;
1018             }
1019             }
1020             }
1021             }
1022              
1023 119 50       217 say "lines: ", _dumper \@lines if $DEBUG_RENDER;
1024              
1025 119 100       845 return @lines if wantarray;
1026 69         1180 join "\n", @lines;
1027             }
1028              
1029             =head2 get_term_width
1030              
1031             Determines, caches and returns the terminal width.
1032              
1033             =head3 Error: Unable to get Terminal Size
1034              
1035             If terminal width cannot be detected, 80 will be assumed.
1036              
1037             =cut
1038              
1039             sub get_term_width {
1040 6     6 1 9 state $term_width;
1041              
1042 6 100       11 if ( not $term_width ) {
1043 1         2 $term_width = eval { chars() };
  1         4  
1044 1   50     36 $term_width ||= 80; # Safe default.
1045 1         2 $term_width--; # Padding.
1046             }
1047              
1048 6         26 $term_width;
1049             }
1050              
1051             =head1 SEE ALSO
1052              
1053             L
1054              
1055             L
1056              
1057             L
1058              
1059              
1060             =head1 AUTHOR
1061              
1062             Tim Potapov, C<< >>
1063              
1064             =head1 BUGS
1065              
1066             Please report any bugs or feature requests to L.
1067              
1068             =head1 CAVEAT
1069              
1070             Nothing to report.
1071              
1072             =head1 SUPPORT
1073              
1074             You can find documentation for this module with the perldoc command.
1075              
1076             perldoc Pod::Query
1077              
1078              
1079             You can also look for information at:
1080              
1081             L
1082             L
1083              
1084              
1085             =head1 ACKNOWLEDGEMENTS
1086              
1087             TBD
1088              
1089             =head1 LICENSE AND COPYRIGHT
1090              
1091             This software is Copyright (c) 2022 by Tim Potapov.
1092              
1093             This is free software, licensed under:
1094              
1095             The Artistic License 2.0 (GPL Compatible)
1096              
1097              
1098             =cut
1099              
1100             1; # End of Pod::Query