File Coverage

blib/lib/Data/CSel.pm
Criterion Covered Total %
statement 206 248 83.0
branch 187 248 75.4
condition 17 44 38.6
subroutine 13 14 92.8
pod 3 3 100.0
total 426 557 76.4


line stmt bran cond sub pod time code
1             package Data::CSel;
2              
3 3     3   59432 use 5.020000;
  3         27  
4 3     3   13 use strict;
  3         5  
  3         54  
5 3     3   11 use warnings;
  3         5  
  3         74  
6              
7 3     3   1227 use Code::Includable::Tree::NodeMethods;
  3         6003  
  3         90  
8             #use List::Util qw(first);
9 3     3   16 use Scalar::Util qw(blessed refaddr looks_like_number);
  3         5  
  3         245  
10              
11 3     3   17 use Exporter qw(import);
  3         5  
  3         5542  
12              
13             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
14             our $DATE = '2022-06-07'; # DATE
15             our $DIST = 'Data-CSel'; # DIST
16             our $VERSION = '0.128'; # VERSION
17              
18             our @EXPORT_OK = qw(
19             csel
20             csel_each
21             parse_csel
22             );
23              
24             our @CLASS_PREFIXES;
25              
26             our $_i1;
27              
28 0     0   0 sub _fail { die __PACKAGE__.": $_[0] at offset ".pos()."\n" }
29              
30             our $RE =
31             qr{
32             (?&SELECTORS) (?{ $_ = $^R->[1] })
33              
34             (?(DEFINE)
35             (?
36             (?{ [$^R, []] })
37             (?&SELECTOR) # [[$^R, []], $selector]
38             (?{ [$^R->[0][0], [$^R->[1]]] })
39             (?:
40             \s*,\s*
41             (?&SELECTOR)
42             (?{
43             push @{$^R->[0][1]}, $^R->[1];
44             $^R->[0];
45             })
46             )*
47             \s*
48             ) # SELECTORS
49              
50             (?
51             (?{ [$^R, []] })
52             (?&SIMPLE_SELECTOR) # [[$^R, []], $simple_selector]
53             (?{ [$^R->[0][0], [$^R->[1]]] })
54             (?:
55             (\s*>\s*|\s*\+\s*|\s*~\s*|\s+)
56             (?{
57             my $comb = $^N;
58             $comb =~ s/^\s+//; $comb =~ s/\s+$//;
59             $comb = " " if $comb eq '';
60             push @{$^R->[1]}, {combinator=>$comb};
61             $^R;
62             })
63              
64             (?&SIMPLE_SELECTOR)
65             (?{
66             push @{$^R->[0][1]}, $^R->[1];
67             $^R->[0];
68             })
69             )*
70             ) # SELECTOR
71              
72             (?
73             (?:
74             (?:
75             # type selector + optional filters
76             ((?&TYPE_NAME))
77             (?{ [$^R, {type=>$^N}] })
78             (?:
79             (?&FILTER) # [[$^R, $simple_selector], $filter]
80             (?{
81             push @{ $^R->[0][1]{filters} }, $^R->[1];
82             $^R->[0];
83             })
84             (?:
85             \s*
86             (?&FILTER)
87             (?{
88             push @{ $^R->[0][1]{filters} }, $^R->[1];
89             $^R->[0];
90             })
91             )*
92             )?
93             )
94             |
95             (?:
96             # optional type selector + one or more filters
97             ((?&TYPE_NAME))?
98             (?{
99             # XXX sometimes $^N is ' '?
100             my $t = $^N // '*';
101             $t = '*' if $t eq ' ';
102             [$^R, {type=>$t}] })
103             (?&FILTER) # [[$^R, $simple_selector], $filter]
104             (?{
105             push @{ $^R->[0][1]{filters} }, $^R->[1];
106             $^R->[0];
107             })
108             (?:
109             \s*
110             (?&FILTER)
111             (?{
112             push @{ $^R->[0][1]{filters} }, $^R->[1];
113             $^R->[0];
114             })
115             )*
116             )
117             )
118             ) # SIMPLE_SELECTOR
119              
120             (?
121             [A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z0-9_]+)*|\*
122             )
123              
124             (?
125             (?{ [$^R, {}] })
126             (
127             (?&ATTR_SELECTOR) # [[$^R0, {}], [$attr, $op, $val]]
128             (?{
129             $^R->[0][1]{type} = 'attr_selector';
130             $^R->[0][1]{attr} = $^R->[1][0];
131             $^R->[0][1]{op} = $^R->[1][1] if defined $^R->[1][1];
132             $^R->[0][1]{value} = $^R->[1][2] if @{ $^R->[1] } > 2;
133             $^R->[0];
134             })
135             |
136             \.((?&TYPE_NAME))
137             (?{
138             $^R->[1]{type} = 'class_selector';
139             $^R->[1]{class} = $^N;
140             $^R;
141             })
142             |
143             \#(\w+)
144             (?{
145             $^R->[1]{type} = 'id_selector';
146             $^R->[1]{id} = $^N;
147             $^R;
148             })
149             |
150             (?&PSEUDOCLASS) # [[$^R, {}], [$pseudoclass, \@args]]
151             (?{
152             $^R->[0][1]{type} = 'pseudoclass';
153             $^R->[0][1]{pseudoclass} = $^R->[1][0];
154             $^R->[0][1]{args} = $^R->[1][1] if @{ $^R->[1] } > 1;
155             $^R->[0];
156             })
157             )
158             ) # FILTER
159              
160             (?
161             \[\s*
162             (?{ [$^R, []] }) # [$^R, [$subjects, $op, $literal]]
163             (?&ATTR_SUBJECTS) # [[$^R, [{name=>$name, args=>$args}, ...]]
164             (?{
165             #use Data::Dmp; say "D:setting subjects: ", dmp $^R->[1];
166             push @{ $^R->[0][1] }, $^R->[1];
167             $^R->[0];
168             })
169              
170             (?:
171             (
172             \s*(?:=~|!~)\s* |
173             \s*(?:!=|<>|>=?|<=?|==?)\s* |
174             \s++(?:eq|ne|lt|gt|le|ge)\s++ |
175             \s+(?:isnt|is|has|hasnt|in|notin)\s+
176             )
177             (?{
178             my $op = $^N;
179             $op =~ s/^\s+//; $op =~ s/\s+$//;
180             $^R->[1][1] = $op;
181             $^R;
182             })
183              
184             (?:
185             (?&LITERAL) # [[$^R0, [$attr, $op]], $literal]
186             (?{
187             push @{ $^R->[0][1] }, $^R->[1];
188             $^R->[0];
189             })
190             |
191             (\w[^\s\]]*) # allow unquoted string
192             (?{
193             $^R->[1][2] = $^N;
194             $^R;
195             })
196             )
197             )?
198             \s*\]
199             ) # ATTR_SELECTOR
200              
201             (?
202             [A-Za-z_][A-Za-z0-9_]*
203             )
204              
205             (?
206             (?{ [$^R, []] }) # [$^R, [name, \@args]]
207             ((?&ATTR_NAME))
208             (?{
209             #say "D:pushing attribute subject: $^N";
210             push @{ $^R->[1] }, $^N;
211             $^R;
212             })
213             (?:
214             # attribute arguments
215             \s*\(\s* (*PRUNE)
216             (?{
217             $^R->[1][1] = [];
218             $^R;
219             })
220             (?:
221             (?&LITERAL)
222             (?{
223             #use Data::Dmp; say "D:pushing argument: ", dmp $^R->[1];
224             push @{ $^R->[0][1][1] }, $^R->[1];
225             $^R->[0];
226             })
227             (?:
228             \s*,\s*
229             (?&LITERAL)
230             (?{
231             #use Data::Dmp; say "D:pushing argument: ", dmp $^R->[1];
232             push @{ $^R->[0][1][1] }, $^R->[1];
233             $^R->[0];
234             })
235             )*
236             )?
237             \s*\)\s*
238             )?
239             ) # ATTR_SUBJECT
240              
241             (?
242             (?{ $_i1 = 0; [$^R, []] })
243             (?&ATTR_SUBJECT) # [[$^R, [$name, \@args]]
244             (?{
245             $_i1++;
246             unless ($_i1 > 1) { # to prevent backtracking from executing tihs code block twice
247             #say "D:pushing subject(1)";
248             push @{ $^R->[0][1] }, {
249             name => $^R->[1][0],
250             (args => $^R->[1][1]) x !!defined($^R->[1][1]),
251             };
252             }
253             $^R->[0];
254             })
255             (?:
256             \s*\.\s*
257             (?{ $_i1 = 0; $^R })
258             (?&ATTR_SUBJECT) # [[$^R, $name, \@args]]
259             (?{
260             $_i1++;
261             unless ($_i1 > 1) { # to prevent backtracking from executing this code block twice
262             #say "D:pushing subject(2)";
263             push @{ $^R->[0][1] }, {
264             name => $^R->[1][0],
265             (args => $^R->[1][1]) x !!defined($^R->[1][1]),
266             };
267             }
268             $^R->[0];
269             })
270             )*
271             ) # ATTR_SUBJECTS
272              
273             (?
274             (?&LITERAL_ARRAY)
275             |
276             (?&LITERAL_NUMBER)
277             |
278             (?&LITERAL_STRING_DQUOTE)
279             |
280             (?&LITERAL_STRING_SQUOTE)
281             |
282             (?&LITERAL_REGEX)
283             |
284             true (?{ [$^R, 1] })
285             |
286             false (?{ [$^R, 0] })
287             |
288             null (?{ [$^R, undef] })
289             ) # LITERAL
290              
291             (?
292             \[\s*
293             (?{ [$^R, []] })
294             (?:
295             (?&LITERAL) # [[$^R, []], $val]
296             (?{ [$^R->[0][0], [$^R->[1]]] })
297             \s*
298             (?:
299             (?:
300             ,\s* (?&LITERAL)
301             (?{ push @{$^R->[0][1]}, $^R->[1]; $^R->[0] })
302             )*
303             |
304             (?: [^,\]]|\z ) (?{ _fail "Expected ',' or '\x5d'" })
305             )
306             )?
307             \s*
308             (?:
309             \]
310             |
311             (?:.|\z) (?{ _fail "Expected closing of array" })
312             )
313             ) # LITERAL_ARRAY
314              
315             (?
316             (
317             -?
318             (?: 0 | [1-9]\d* )
319             (?: \. \d+ )?
320             (?: [eE] [-+]? \d+ )?
321             )
322             (?{ [$^R, 0+$^N] })
323             )
324              
325             (?
326             (
327             "
328             (?:
329             [^\\"]+
330             |
331             \\ [0-7]{1,3}
332             |
333             \\ x [0-9A-Fa-f]{1,2}
334             |
335             \\ ["\\'tnrfbae]
336             )*
337             "
338             )
339             (?{ [$^R, eval $^N] })
340             )
341              
342             (?
343             (
344             '
345             (?:
346             [^\\']+
347             |
348             \\ .
349             )*
350             '
351             )
352             (?{ [$^R, eval $^N] })
353             )
354              
355             (?
356             (
357             (?:
358             (?:
359             /
360             (?:
361             [^/\\]+
362             |
363             \\ .
364             )*
365             /
366             ) |
367             (?:
368             qr\(
369             (?:
370             [^\)\\]+
371             |
372             \\ .
373             )*
374             \)
375             )
376             )
377             [ims]*
378             )
379             (?{ my $code = substr($^N, 0, 2) eq "qr" ? $^N : "qr$^N"; my $re = eval $code; die if $@; [$^R, $re] })
380             )
381              
382             (?
383             [A-Za-z_][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)*
384             )
385              
386             (?
387             :
388             (?:
389             (?:
390             (has|not)
391             (?{ [$^R, [$^N]] })
392             \(\s*
393             (?:
394             (?&LITERAL)
395             (?{
396             push @{ $^R->[0][1][1] }, $^R->[1];
397             $^R->[0];
398             })
399             |
400             ((?&SELECTORS))
401             (?{
402             push @{ $^R->[0][1][1] }, $^N;
403             $^R->[0];
404             })
405             )
406             \s*\)
407             )
408             |
409             (?:
410             ((?&PSEUDOCLASS_NAME))
411             (?{ [$^R, [$^N]] })
412             (?:
413             \(\s*
414             (?&LITERAL)
415             (?{
416             push @{ $^R->[0][1][1] }, $^R->[1];
417             $^R->[0];
418             })
419             (?:
420             \s*,\s*
421             (?&LITERAL)
422             (?{
423             push @{ $^R->[0][1][1] }, $^R->[1];
424             $^R->[0];
425             })
426             )*
427             \s*\)
428             )?
429             )
430             )
431             ) # PSEUDOCLASS
432             ) # DEFINE
433             }x;
434              
435             sub parse_csel {
436 107     107 1 97332 state $re = qr{\A\s*$RE\s*\z};
437              
438 107         182 local $_ = shift;
439 107         124 local $^R;
440 107 100       133 eval { $_ =~ $re } and return $_;
  107         751  
441 3 50       7 die $@ if $@;
442 3         7 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
443             }
444              
445             sub _uniq_objects {
446 158     158   203 my @uniq;
447             my %mem;
448 158         200 for (@_) {
449 727 100       1597 push @uniq, $_ unless $mem{refaddr($_)}++;
450             }
451 158         385 @uniq;
452             }
453              
454             sub _simpsel {
455 3     3   22 no warnings 'numeric', 'uninitialized';
  3         5  
  3         6141  
456              
457 81     81   185 my ($opts, $simpsel, $is_recursive, @nodes) = @_;
458              
459             #use Data::Dmp; say "D: _simpsel(expr", dmp($simpsel), ", recursive=$is_recursive, nodes=[".join(",",map {$_->{id}} @nodes)."])";
460              
461 81         88 my @res;
462 81 100       122 if ($is_recursive) {
463 78         95 @res = (@nodes, map {Code::Includable::Tree::NodeMethods::descendants($_)} @nodes);
  112         2119  
464             } else {
465 3         5 @res = @nodes;
466             }
467             #say "D: intermediate result (after walk): [".join(",",map {$_->{id}} @res)."]";
468              
469 81 100       11666 unless ($simpsel->{type} eq '*') {
470 22         29 my @fres;
471              
472             my @types_to_match;
473 22   100     25 for (@{ $opts->{class_prefixes} // [] }, @CLASS_PREFIXES) {
  22         92  
474 3 50       14 push @types_to_match, $_ . (/::$/ ? "" : "::") . $simpsel->{type};
475             }
476 22         46 push @types_to_match, $simpsel->{type};
477              
478             ELEM:
479 22         27 for my $o (@res) {
480 187         199 my $ref = ref($o);
481 187         193 for (@types_to_match) {
482 194 100       284 if ($ref eq $_) {
483 71         74 push @fres, $o;
484 71         424 next ELEM;
485             }
486             }
487             }
488 22         43 @res = @fres;
489             }
490              
491 81         145 @res = _uniq_objects(@res);
492             #say "D: intermediate result (after type): [".join(",",map {$_->{id}} @res)."]";
493              
494 81   100     99 for my $f (@{ $simpsel->{filters} // [] }) {
  81         190  
495 66 50       115 last unless @res;
496              
497 66         93 my $type = $f->{type};
498              
499 66 100       151 if ($type eq 'attr_selector') {
    100          
    100          
    50          
500              
501 34         36 my @attrs = @{ $f->{attr} };
  34         53  
502 34         46 my $op = $f->{op};
503 34         40 my $opv = $f->{value};
504              
505 34         38 my @newres;
506             ITEM:
507 34         43 for my $o0 (@res) {
508 168         186 my $o = $o0;
509 168         230 for my $i (0..$#attrs) {
510 175         242 my $attr_name = $attrs[$i]{name};
511 175         184 my $attr_args = $attrs[$i]{args};
512 175 100 100     674 next ITEM unless blessed($o) && $o->can($attr_name);
513 162 50 33     332 if ($attr_args && $i == $#attrs) {
514             # T[meth()] means T[meth() is true]
515 0 0       0 unless ($op) {
516 0         0 $op = 'is';
517 0         0 $opv = 1;
518             }
519 0         0 $o = $o->$attr_name(@$attr_args);
520             } else {
521 162         265 $o = $o->$attr_name;
522             }
523             }
524 155 100       804 goto PASS unless $op;
525 146         152 my $val = $o;
526              
527 146 100 100     729 if ($op eq '=' || $op eq '==') {
    100 100        
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
    100          
    50          
528 15 100       25 if (looks_like_number($opv)) {
529 10 100       24 next ITEM unless $val == $opv;
530             } else {
531 5 100       11 next ITEM unless $val eq $opv;
532             }
533             } elsif ($op eq 'eq') {
534 8 100       17 next ITEM unless $val eq $opv;
535             } elsif ($op eq '!=' || $op eq '<>') {
536 15 100       26 if (looks_like_number($opv)) {
537 10 100       19 next ITEM unless $val != $opv;
538             } else {
539 5 100       10 next ITEM unless $val ne $opv;
540             }
541             } elsif ($op eq 'ne') {
542 5 100       9 next ITEM unless $val ne $opv;
543             } elsif ($op eq '>') {
544 10 100       19 if (looks_like_number($opv)) {
545 5 100       13 next ITEM unless $val > $opv;
546             } else {
547 5 100       9 next ITEM unless $val gt $opv;
548             }
549             } elsif ($op eq 'gt') {
550 5 100       10 next ITEM unless $val gt $opv;
551             } elsif ($op eq '>=') {
552 10 100       19 if (looks_like_number($opv)) {
553 5 100       13 next ITEM unless $val >= $opv;
554             } else {
555 5 100       11 next ITEM unless $val ge $opv;
556             }
557             } elsif ($op eq 'ge') {
558 5 100       9 next ITEM unless $val ge $opv;
559             } elsif ($op eq '<') {
560 9 100       16 if (looks_like_number($opv)) {
561 4 100       11 next ITEM unless $val < $opv;
562             } else {
563 5 100       13 next ITEM unless $val lt $opv;
564             }
565             } elsif ($op eq 'lt') {
566 5 100       12 next ITEM unless $val lt $opv;
567             } elsif ($op eq '<=') {
568 9 100       17 if (looks_like_number($opv)) {
569 4 50       11 next ITEM unless $val <= $opv;
570             } else {
571 5 100       10 next ITEM unless $val le $opv;
572             }
573             } elsif ($op eq 'le') {
574 5 100       10 next ITEM unless $val le $opv;
575             } elsif ($op eq 'is') {
576 15 100       24 if (!defined($opv)) {
    100          
577 5 100       11 next ITEM unless !defined($val);
578             } elsif ($opv) {
579 5 100       12 next ITEM unless $val;
580             } else {
581 5 100       12 next ITEM unless !$val;
582             }
583             } elsif ($op eq 'isnt') {
584 15 100       26 if (!defined($opv)) {
    100          
585 5 100       11 next ITEM unless defined($val);
586             } elsif ($opv) {
587 5 100       14 next ITEM unless !$val;
588             } else {
589 5 100       8 next ITEM unless $val;
590             }
591             } elsif ($op eq 'has') {
592 0 0 0     0 next ITEM unless defined $val && ref($val) eq 'ARRAY'
      0        
593             && defined $opv;
594 0 0       0 if (looks_like_number($opv)) {
595 0 0       0 next ITEM unless grep { $_ == $opv } @$val;
  0         0  
596             } else {
597 0 0       0 next ITEM unless grep { $_ eq $opv } @$val;
  0         0  
598             }
599             } elsif ($op eq 'hasnt') {
600 0 0 0     0 next ITEM unless defined $val && ref($val) eq 'ARRAY'
      0        
601             && defined $opv;
602 0 0       0 if (looks_like_number($opv)) {
603 0 0       0 next ITEM if grep { $_ == $opv } @$val;
  0         0  
604             } else {
605 0 0       0 next ITEM if grep { $_ eq $opv } @$val;
  0         0  
606             }
607             } elsif ($op eq 'in') {
608 0 0 0     0 next ITEM unless defined $val && defined $opv &&
      0        
609             ref($opv) eq 'ARRAY';
610 0 0       0 if (looks_like_number($val)) {
611 0 0       0 next ITEM unless grep { $_ == $val } @$opv;
  0         0  
612             } else {
613 0 0       0 next ITEM unless grep { $_ eq $val } @$opv;
  0         0  
614             }
615             } elsif ($op eq 'notin') {
616 0 0 0     0 next ITEM unless defined $val && defined $opv &&
      0        
617             ref($opv) eq 'ARRAY';
618 0 0       0 if (looks_like_number($val)) {
619 0 0       0 next ITEM if grep { $_ == $val } @$opv;
  0         0  
620             } else {
621 0 0       0 next ITEM if grep { $_ eq $val } @$opv;
  0         0  
622             }
623             } elsif ($op eq '=~') {
624 10 100       37 next ITEM unless $val =~ $opv;
625             } elsif ($op eq '!~') {
626 5 100       20 next ITEM unless $val !~ $opv;
627             } else {
628 0         0 die "BUG: Unsupported operator '$op' in attr_selector";
629             }
630              
631 81         119 PASS:
632             # pass all attribute filters, add to new result
633             #say "D: adding to result: ".$o->{id};
634             push @newres, $o0;
635             } # for each item
636 34         77 @res = @newres;
637              
638             } elsif ($type eq 'class_selector') {
639              
640 6         10 my $class = $f->{class};
641              
642 6         8 my @classes_to_match;
643 6   100     6 for (@{ $opts->{class_prefixes} // [] }, @CLASS_PREFIXES) {
  6         22  
644 1 50       10 push @classes_to_match, $_ . (/::$/ ? "" : "::") . $class;
645             }
646 6         12 push @classes_to_match, $class;
647 6         6 my @filtered_res;
648             RES:
649 6         8 for my $res (@res) {
650 45         47 for (@classes_to_match) {
651 47 100       110 if ($res->isa($_)) {
652 15         18 push @filtered_res, $res;
653 15         19 next RES;
654             }
655             }
656             }
657 6         15 @res = @filtered_res;
658              
659             } elsif ($type eq 'id_selector') {
660              
661 6   50     18 my $method = $opts->{id_method} // 'id';
662 6         8 my $id = $f->{id};
663              
664 6 50       7 @res = grep { $_->can($method) && $_->$method eq $id } @res;
  42         270  
665              
666             } elsif ($type eq 'pseudoclass') {
667              
668 20         30 my $pc = $f->{pseudoclass};
669              
670 20 100       118 if ($pc eq 'first') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    100          
    100          
    50          
    0          
671 1         2 @res = ($res[0]);
672             } elsif ($pc eq 'last') {
673 1         3 @res = ($res[-1]);
674             } elsif ($pc eq 'first-child') {
675 3         5 @res = grep { Code::Includable::Tree::NodeMethods::is_first_child($_) } @res;
  30         637  
676             } elsif ($pc eq 'last-child') {
677 1         3 @res = grep { Code::Includable::Tree::NodeMethods::is_last_child($_) } @res;
  10         215  
678             } elsif ($pc eq 'only-child') {
679 1         3 @res = grep { Code::Includable::Tree::NodeMethods::is_only_child($_) } @res;
  10         228  
680             } elsif ($pc eq 'nth-child') {
681 1         3 @res = grep { Code::Includable::Tree::NodeMethods::is_nth_child($_, $f->{args}[0]) } @res;
  10         231  
682             } elsif ($pc eq 'nth-last-child') {
683 1         2 @res = grep { Code::Includable::Tree::NodeMethods::is_nth_last_child($_, $f->{args}[0]) } @res;
  10         233  
684             } elsif ($pc eq 'first-of-type') {
685 1         3 @res = grep { Code::Includable::Tree::NodeMethods::is_first_child_of_type($_) } @res;
  10         271  
686             } elsif ($pc eq 'last-of-type') {
687 1         7 @res = grep { Code::Includable::Tree::NodeMethods::is_last_child_of_type($_) } @res;
  10         268  
688             } elsif ($pc eq 'only-of-type') {
689 1         3 @res = grep { Code::Includable::Tree::NodeMethods::is_only_child_of_type($_) } @res;
  10         261  
690             } elsif ($pc eq 'nth-of-type') {
691 1         2 @res = grep { Code::Includable::Tree::NodeMethods::is_nth_child_of_type($_, $f->{args}[0]) } @res;
  10         287  
692             } elsif ($pc eq 'nth-last-of-type') {
693 1         3 @res = grep { Code::Includable::Tree::NodeMethods::is_nth_last_child_of_type($_, $f->{args}[0]) } @res;
  10         292  
694             } elsif ($pc eq 'root') {
695 2         3 @res = grep { Code::Includable::Tree::NodeMethods::is_root($_) } @res;
  16         117  
696             } elsif ($pc eq 'has-min-children') {
697 0         0 @res = grep { Code::Includable::Tree::NodeMethods::has_min_children($_, $f->{args}[0]) } @res;
  0         0  
698             } elsif ($pc eq 'has-max-children') {
699 0         0 @res = grep { Code::Includable::Tree::NodeMethods::has_max_children($_, $f->{args}[0]) } @res;
  0         0  
700             } elsif ($pc eq 'has-children-between') {
701 0         0 @res = grep { Code::Includable::Tree::NodeMethods::has_children_between($_, $f->{args}[0], $f->{args}[1]) } @res;
  0         0  
702             } elsif ($pc eq 'empty') {
703 1         2 @res = grep { my @c = Code::Includable::Tree::NodeMethods::_children_as_list($_); !@c } @res;
  10         14  
  10         175  
704             } elsif ($pc eq 'has') {
705             @res = _uniq_objects(
706 2         29 map { reverse Code::Includable::Tree::NodeMethods::ancestors($_) }
707 1         6 csel($opts, $f->{args}[0], @res)
708             );
709             } elsif ($pc eq 'not') {
710             #say "D: res=(".join(",", map {$_->{id}} @res).")";
711 2         9 my @matches = csel($opts, $f->{args}[0], @res);
712             #say "D: all_matches=(".join(",", map {$_->{id}} @all_matches).")";
713 2         3 my %matches_refaddrs;
714 2         5 for (@matches) { $matches_refaddrs{refaddr($_)}++ }
  8         14  
715 2         3 @res = grep { !$matches_refaddrs{refaddr($_)} } @res;
  20         36  
716             } elsif ($pc eq 'parent') {
717 0         0 @res = _uniq_objects(map { Code::Includable::Tree::NodeMethods::retrieve_parent($_) } @res);
  0         0  
718             } else {
719 0         0 die "Unsupported pseudo-class '$pc'";
720             }
721              
722             }
723              
724             #say "D: intermediate result (after filter): [".join(",",map {$_->{id}} @res)."]";
725             } # for each filter
726 81         587 @res;
727             }
728              
729             sub _sel {
730 77     77   141 my ($opts, $sel, @nodes) = @_;
731              
732 77         99 my @simpsels = @$sel;
733 77         92 my @res;
734              
735 77         81 my $i = 0;
736 77         131 while (@simpsels) {
737 81 100       145 if ($i++ == 0) {
738 77         93 my $simpsel = shift @simpsels;
739 77         135 @res = _simpsel($opts, $simpsel, 1, @nodes);
740             } else {
741 4         7 my $combinator = shift @simpsels;
742 4         5 my $simpsel = shift @simpsels;
743 4 50       8 last unless @res;
744 4 100       16 if ($combinator->{combinator} eq ' ') { # descendant
    100          
    100          
    50          
745             @res = _simpsel($opts, $simpsel, 1,
746 1         3 map { Code::Includable::Tree::NodeMethods::_children_as_list($_) } @res);
  5         67  
747             } elsif ($combinator->{combinator} eq '>') { # child
748             @res = _simpsel($opts, $simpsel, 0,
749 1         3 map { Code::Includable::Tree::NodeMethods::_children_as_list($_) } @res);
  5         68  
750             } elsif ($combinator->{combinator} eq '~') { # sibling
751             @res = _simpsel($opts, $simpsel, 0,
752 1         3 map { Code::Includable::Tree::NodeMethods::next_siblings($_) } @res);
  5         104  
753             } elsif ($combinator->{combinator} eq '+') { # adjacent sibling
754             @res = _simpsel($opts, $simpsel, 0,
755 5         40 grep {defined}
756 1         3 map { Code::Includable::Tree::NodeMethods::next_sibling($_) }
  5         102  
757             @res);
758             } else {
759 0         0 die "BUG: Unknown combinator '$combinator->{combinator}'";
760             }
761             }
762             }
763              
764 77         210 @res;
765             }
766              
767             sub csel {
768 76     76 1 169899 my $opts;
769 76 100       186 if (ref($_[0]) eq 'HASH') {
770 75         94 $opts = shift;
771             } else {
772 1         3 $opts = {};
773             }
774 76         83 my $expr = shift;
775 76         110 my @nodes = @_;
776              
777 76         139 my $pexpr = parse_csel($expr);
778 76 50       162 $pexpr or die "Invalid CSel expression '$expr'";
779              
780 76 50       136 local $Code::Includable::Tree::NodeMethods::GET_PARENT_METHOD = $opts->{get_parent_method} if $opts->{get_parent_method};
781 76 50       124 local $Code::Includable::Tree::NodeMethods::SET_PARENT_METHOD = $opts->{set_parent_method} if $opts->{set_parent_method};
782 76 50       115 local $Code::Includable::Tree::NodeMethods::GET_CHILDREN_METHOD = $opts->{get_children_method} if $opts->{get_children_method};
783 76 50       109 local $Code::Includable::Tree::NodeMethods::SET_CHILDREN_METHOD = $opts->{set_children_method} if $opts->{set_children_method};
784              
785 76         121 my @res = _uniq_objects(map { _sel($opts, $_, @nodes) } @$pexpr );
  77         163  
786              
787 76 50       142 if ($opts->{wrap}) {
788 0         0 require Data::CSel::Selection;
789 0         0 return Data::CSel::Selection->new(\@res);
790             } else {
791 76         286 return @res;
792             }
793             }
794              
795             sub csel_each(&;@) { ## no critic: Subroutines::ProhibitSubroutinePrototypes
796 1     1 1 639 my $cb = shift;
797 1         4 for my $node (csel(@_)) {
798 3         20 local $_ = $node;
799 3         4 $cb->($_);
800             }
801 1         7 undef;
802             }
803              
804             1;
805             # ABSTRACT: Select tree node objects using CSS Selector-like syntax
806              
807             __END__