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