File Coverage

blib/lib/HTML/Selector/Element.pm
Criterion Covered Total %
statement 236 319 73.9
branch 157 244 64.3
condition 64 123 52.0
subroutine 24 33 72.7
pod 3 17 17.6
total 484 736 65.7


line stmt bran cond sub pod time code
1             package HTML::Selector::Element;
2             our $VERSION = '0.97';
3              
4             ## Adapted from HTML::Selector::XPath
5             ## The parser is basically the same, the difference is in what it produces.
6              
7 11     11   3594600 use Carp;
  11         75  
  11         1144  
8 11     11   76 use strict;
  11         22  
  11         63981  
9              
10             sub import {
11 8     8   86 my($class) = shift;
12 8 50       45 if(@_) {
13 8         6226 require HTML::Element;
14             package HTML::Element;
15 8         176405 local $^W; # no warnings 'redefined' doesn't work over there in Exporter
16 8         904 HTML::Selector::Element::Trait->import(@_);
17             }
18             }
19            
20             my $ident = qr/(?![0-9]|-[-0-9])[-_a-zA-Z0-9]+/;
21              
22             my $reg = {
23             # tag name/id/class. Caveat: no namespace
24             element => qr/^([#.]?)([^\s'"#.\/:@,=~>()\[\]|+]+)/i,
25             # attribute presence
26             attr1 => qr/^\[ \s* ($ident) \s* \]/x,
27             # attribute value match
28             attr2 => qr/^\[ \s* ($ident) \s*
29             ( [~|*^\$!]? = ) \s*
30             (?: ($ident) | "([^"]*)" | '([^']*)') \s* \] /x,
31             badattr => qr/^\[/,
32             pseudoN => qr/^:(not|has|is)\(/i, # we chop off the closing parenthesis below in the code
33             pseudo => qr/^:([()a-z0-9_+-]+)/i,
34             # adjacency/direct descendance (test for comma first)
35             combinator => qr/^\s*([>+~])\s*|^\s+/i, # doesn't capture matched whitespace
36             # rule separator
37             comma => qr/^\s*,\s*/i,
38             };
39              
40             sub new {
41 44     44 1 32791 my($class, @expr) = @_;
42 44         111 my $self = bless {}, $class;
43 44         154 $self->selector(@expr);
44 44         180 return $self;
45             }
46              
47             sub selector {
48 44     44 0 69 my $self = shift;
49 44 50       114 if (@_) {
50 44         65 delete @{$self}{qw(find is)};
  44         123  
51 44         148 $self->{selector} = join ', ', @_;
52 44         92 $self->{parsed} = \my @parsed;
53 44         86 foreach (@_) {
54 44         121 my($parsed, $leftover) = $self->consume($_);
55 44 50       119 length $leftover
56             and die "Invalid rule, couldn't parse '$leftover'";
57 44         129 push @parsed, @$parsed;
58             }
59             }
60 44         80 return $self->{selector};
61             }
62              
63             sub convert_attribute_match {
64 21     21 0 64 my ($left,$op,$right) = @_;
65             # negation (e.g. [input!="text"]) isn't implemented in CSS, but include it anyway:
66 21 100       103 if ($op eq '!=') {
    100          
    100          
    100          
    100          
    100          
67 1         29 $left, qr/^(?!\Q$right\E$)/
68             } elsif ($op eq '~=') { # substring attribute match
69 13         603 $left, qr/(?
70             } elsif ($op eq '*=') { # real substring attribute match
71 1         657 $left, qr/\Q$right\E/
72             } elsif ($op eq '|=') {
73 1         25 $left, qr/^\Q$right\E(?![^-])/
74             } elsif ($op eq '^=') {
75 1         22 $left, qr/^\Q$right\E/
76             } elsif ($op eq '$=') {
77 1         16 $left, qr/\Q$right\E$/
78             } else { # exact match
79 3         10 $left, $right
80             }
81             }
82              
83             # for our purpose, "siblings" includes the element itself
84             sub siblings {
85 66     66 0 85 my($this) = @_;
86 66   50     112 return children($this->{_parent}||return $this);
87             }
88              
89             sub nth_of_type {
90 11     11 0 21 my($of_type, $backward, $n, $cycle) = @_;
91             # nth_child = nth_of_type without type filter
92 11   100     27 $cycle ||= 0;
93 11 50 33     21 if($n <= 0 && $cycle > 0) {
94             # permanent correction
95 0   0     0 $n %= $cycle ||= $cycle; # first value above 0
96             }
97 66     66   1444 return sub { my($this) = @_;
98 66         79 my @sibling = siblings($this);
99 66 100       234 @sibling = grep $_->{_tag} eq $this->{_tag}, @sibling if $of_type;
100 66 50 33     174 for(my $n = # lexical scratch copy
      100        
      100        
101             $n > @sibling && $cycle < 0
102             ? ($n-@sibling) % $cycle + @sibling # first value below upper bound as modulo <= 0
103             : $n ; # no correction
104             $n > 0 && $n <= @sibling; # give up as soon as we get out of range
105             $n += $cycle || last) # loop only once if $cycle is zero
106             {
107 142 100       376 return 1 if $this == $sibling[$backward ? -$n : $n - 1];
    100          
108             }
109 41         71 return;
110             }
111 11         60 }
112              
113             sub only_child {
114 0     0 0 0 my($this) = @_;
115 0         0 return 1 == siblings($this);
116             }
117              
118             # A hacky recursive descent
119             # Only descends for :not(...) and :has(...)
120             sub consume {
121 44     44 0 86 my ($self, $rule) = @_;
122              
123 44         57 my @alt;
124 44         109 my $set = { static => my $static = [] };
125 44         138 my $hold; # last valid set
126             my $sibling_root; # root element of search space is sibling of start element
127 44         0 my $root_child; # root element of search space is child of start element
128 44         0 my($any); # flags
129 44         88 my $start_combinators = '';
130              
131 44         159 $rule =~ s/^\s+//;
132 44         85 my $original = $rule;
133             # Loop through each "unit" of the rule
134 44         83 while() {
135             # Match elements
136 91         175 for($any = 0;; $any++) { # endless loop
137 197 100       2687 if ($rule =~ s/$reg->{element}//) {
    100          
    100          
    50          
    50          
    100          
138 86         294 my ($id_class,$name) = ($1,$2);
139 86 100 33     449 if ($id_class eq '#') { # ID
    100          
    50          
140 1         5 unshift @$static, id => $name;
141             # a condition very likely to fail, so try this first
142             } elsif ($id_class eq '.') { # class
143 12         56 push @$static, convert_attribute_match('class', '~=', $name);
144             } elsif (!$set->{tag} && $name ne '*') {
145             # we're not adding '*' yet as that's a very loose condition that seldom fails
146             # It's often not even necessary to test when we have other, more stringent conditions.
147 73         161 $set->{tag} = $name;
148 73         176 push @$static, _tag => $name;
149             }
150             }
151             # Match attribute selectors
152             elsif ($rule =~ s/$reg->{attr2}//) {
153 8         24 push @$static, convert_attribute_match( $1, $2, $^N );
154             } elsif ($rule =~ s/$reg->{attr1}//) {
155             # any value, as long as it's defined
156 1         4 push @$static, convert_attribute_match( $1, '', qr// );
157             } elsif ($rule =~ $reg->{badattr}) {
158 0         0 Carp::croak "Invalid attribute-value selector '$rule'";
159             }
160             # Match :not and :has
161             elsif ($rule =~ s/$reg->{pseudoN}//) {
162 0         0 my $which = lc $1;
163             # Now we parse the rest, and after parsing the subexpression
164             # has stopped, we must find a matching closing parenthesis:
165 0         0 my( $subset, $leftover ) = $self->consume( $rule );
166 0         0 $rule = $leftover;
167 0 0       0 $rule =~ s!^\s*\)!!
168             or die "Unbalanced parentheses at '$rule'";
169 0 0       0 if($which eq 'not') {
    0          
    0          
170 0         0 my @params = criteria($subset, undef);
171 0     0   0 push @$static, sub { not look_self($_[0], @params) };
  0         0  
172             } elsif($which eq 'is') {
173 0         0 my @params = criteria($subset, undef);
174 0     0   0 push @$static, sub { look_self($_[0], @params) };
  0         0  
175             } elsif($which eq 'has') {
176             # This is possibly very slow, especially when executed very often, so we keep this criterium for last
177 0         0 push @{$set->{has}}, find_closure($subset);
  0         0  
178             }
179             }
180             # other pseudoclasses/pseudoelements
181             # "else" because there could be more than one :not/:has
182             elsif ($rule =~ s/$reg->{pseudo}//) {
183 11         27 my $simple = ":$1";
184 11 50       24 if ( my @expr = $self->parse_pseudo($1, \$rule) ) {
    50          
    50          
    0          
    0          
    0          
185 0         0 push @$static, @expr;
186             } elsif ( $1 eq 'only-child') {
187 0         0 push @$static, only_child();
188             } elsif (my @m = $1 =~ /^((?:first|last)-(?:child|of-type)$) | ^(nth-(?:last-)?(?:child|of-type)) \((odd|even|(\d+)|(-?\d*)n([\+\-]\d+)?)\)$/x) {
189             # Matches all pseudoelements of the following lists:
190             # - first-child, last-child, first-of-type, last-of-type: without expression
191             # - nth-child, nth-of-type, nth-last-child, nth-last-of-type: with an expression between parens
192             # of one of these types: odd, even, and an+b
193             # with a lot of freedom for that last one, for example:
194             # 3, 3n, 3n+1, n+5, -n+5, -3n+5, 3n-1
195 11         22 my($pseudo, $nth, $expr, $n, $cycle, $offset) = @m;
196 11 100       19 if($nth) {
197 9 100       15 if(defined $cycle) {
    100          
198 4 100       35 $cycle .= '1' if $cycle =~ /^(-?)$/;
199 4   66     12 $n = $offset || $cycle;
200             }
201             elsif(!defined $n) {
202             # even / odd
203 3         4 $cycle = 2;
204 3 100       6 $n = $expr eq 'odd' ? 1 : 2;
205             }
206 9         11 $pseudo = $nth;
207             }
208             else {
209             # first / last
210 2         3 $n = 1;
211             }
212 11         24 my $of_type = $pseudo =~ /of-type/;
213 11         29 my $backward = $pseudo =~ /last/;
214 11         30 push @$static, nth_of_type($of_type, $backward, $n+0, $cycle);
215             } elsif ($1 =~ /^contains\($/) {
216             # not sure if this will work well in practise, in regards to whitespace
217 0 0       0 if( $rule =~ s/^\s*"([^"]*)"\s*\)// ) { # "#stupid syntax highlighter
    0          
218 0         0 my $fragment = $1;
219 0     0   0 push @$static, sub { $_[0]->as_text() =~ /\Q$fragment/ };
  0         0  
220             } elsif( $rule =~ s/^\s*'([^']*)'\s*\)// ) { #'#stupid syntax highlighter
221 0         0 my $fragment = $1;
222 0     0   0 push @$static, sub { $_[0]->as_text() =~ /\Q$fragment/ };
  0         0  
223             } else {
224 0         0 return( $set, $rule );
225 0         0 die "Malformed string in :contains(): '$rule'";
226             };
227             } elsif ( $1 eq 'root') {
228             # matches document root, or starting element
229 0         0 $set->{is_root} = 1;
230             } elsif ( $1 eq 'empty') {
231 0     0   0 push @$static, sub { (shift)->is_empty };
  0         0  
232             } else {
233 0         0 Carp::croak "Can't translate '$1' pseudo-class";
234             }
235             }
236             else {
237             # failed to match anything
238 91         202 last;
239             }
240 106         169 $any++;
241 106 50       203 die "Endless loop?"if $any > 20000;
242 106         204 undef $hold;
243             }
244              
245             # Match commas
246 91 50       542 if ($rule =~ s/$reg->{comma}//o) {
    100          
247             # ending one rule and beginning another
248 0   0     0 $set->{tag} ||= do { push @$static, _tag => qr/^(?!~)/; '*' };
  0         0  
  0         0  
249 0 0 0     0 $set->{sibling_root} ||= $sibling_root if $sibling_root;
250 0 0 0     0 $set->{root_child} ||= $root_child if $root_child;
251 0 0       0 ($set->{selector} = length $rule ? substr($original, 0, -length $rule) : $original) =~ s/[,\s]+$//;
252 0         0 $original = $rule;
253 0         0 push @alt, $set;
254 0         0 $set = { static => $static = [] };
255 0         0 ($any, $hold, $sibling_root, $root_child) = ();
256             }
257             # Match combinators (whitespace, >, + and ~)
258             elsif ($rule =~ s/$reg->{combinator}//) {
259 47   100     221 my $combinator = $1 || ' ';
260 47 100       107 unless($any) {
261 12 50       31 unless($set->{chained}) {
262             # rule starts with a combinator
263             # add match for start element
264 12         53 $set->{is_root} = 1; # root element / start element
265             } else {
266             # 2 subsequent combinators: interject a '*'
267 0         0 push @$static, _tag => qr/^(?!~)/;
268 0         0 $set->{tag} = '*';
269             }
270             }
271             # new context
272 47         83 ($any, $hold) = ();
273 47 100       107 $hold = $set unless $1;
274 47 50       322 ($set->{selector} = length $rule ? substr($original, 0, -length $rule) : $original) =~ s/[+~>\s]+$//;
275 47         205 $set = { static => $static = [], chained => my $chained = $set, combinator => $combinator };
276 47 100 100     259 if(($chained->{is_root} || $chained->{sibling_root}) && $combinator =~ /([+~])/) {
      100        
277 7   100     42 $set->{sibling_root} = ($chained->{sibling_root} || '') . $1;
278 7         14 $sibling_root = $set;
279             }
280 47 100 100     337 if($chained->{is_root} && $combinator =~ />/) {
    100 100        
281 6         12 $set->{root_child} = '>';
282 6         13 $root_child = $set;
283             }
284             elsif($chained->{root_child} && $combinator =~ /([+~])/) {
285 3         13 $set->{root_child} = $chained->{root_child} . $1;
286 3         6 $root_child = $set;
287             }
288             }
289             else {
290 44 50       252 ($set->{selector} = length $rule ? substr($original, 0, -length $rule) : $original) =~ s/\s+$//;
291 44         63 last;
292             }
293             }
294             # wrap up
295             # rule ended in whitespace - This can only happen in nested rules such as :not( ... )
296 44 50       110 $set = $hold if $hold;
297 44   66     117 $set->{tag} ||= do { push @$static, _tag => qr/^(?!~)/; '*' };
  3         7  
  3         11  
298 44 100 66     105 $set->{sibling_root} ||= $sibling_root if $sibling_root;
299 44 100 66     90 $set->{root_child} ||= $root_child if $root_child;
300              
301 44         84 push @alt, $set;
302 44         153 return \@alt, $rule;
303             }
304              
305             sub criteria {
306             # returns criteria for look_down, with bound closures
307 41     41 1 84 my($set, $refroot, $strategy) = @_;
308 41   100     207 $strategy ||= !$refroot && 'banroot';
      66        
309 41         53 my($rootparent);
310             my $recurse;
311 41 100       105 for my $root ($refroot ? $$refroot : 0) {
312             $recurse = sub {
313             # embeds $root
314 75     75   118 my($set, $banroot) = @_;
315 75 50       98 my @params = @{$set->{static}||[]};
  75         232  
316              
317 75 100       149 if($set->{is_root}) {
318 10         16 $banroot = 0;
319 10 50       36 if($refroot) {
320             # relative, top of branch
321 10         46 unshift @params, sub { $_[0] == $root }; # unlikely to succeed, so fail fast
  93         202  
322             }
323             else {
324             # absolute, root of DOM
325 0         0 unshift @params, _parent => undef; # unlikely to succeed, so fail fast
326             }
327             }
328              
329 75 100 66     211 if($set->{chained}) {
    100          
330 37 100       117 $banroot = 0 if $set->{chained}{is_root}; # unnecessary as we know where root should be
331 37         54 push @params, do {
332             # Value is an anonymous sub
333             # Recurse into linked list
334 37         224 my @params = $recurse->($set->{chained}, $banroot);
335 37         73 my $combinator = $set->{combinator};
336             # we're in a chained set, so we have to wrap the criteria into a test in a closure.
337 37 100       206 if ($combinator =~ /^\s+$/) {
    100          
    100          
    50          
338 67         1986 $banroot ? sub { my($this) = @_;
339 67   100     176 my $rootparent = $root->{_parent} || 0;
340 67   66     224 while($this = $this->{_parent} and $this != $rootparent) {
341 112 100       233 look_self($this, @params)
342             and return 1;
343             }
344 5         13 return 0;
345             }
346 0         0 : sub { my($this) = @_;
347 0         0 while($this = $this->{_parent}) {
348 0 0       0 look_self($this, @params)
349             and return 1;
350             }
351 0         0 return 0;
352             }
353 15 50       94 }
354             elsif ($combinator =~ />/) {
355 11         546 $banroot ? sub { my($this) = @_;
356 11   100     25 my $rootparent = $root->{_parent} || 0;
357 11 50 33     40 if($this = $this->{_parent} and $this != $rootparent) {
358 11 50       34 look_self($this, @params)
359             and return 1;
360             }
361 0         0 return 0;
362             }
363 43         57 : sub { my($this) = @_;
364 43 50       95 if($this = $this->{_parent}) {
365 43 50       74 look_self($this, @params)
366             and return 1;
367             }
368 0         0 return 0;
369 10 100       109 };
370             }
371             elsif ($combinator =~ /\+/) {
372 27         629 sub { my($this) = @_;
373 27         79 my @left = $this->left;
374 27         520 while(@left) {
375 21         33 $this = pop @left;
376 21 50 33     163 ref $this && $this->{_tag} && $this->{_tag} !~ /^~/ or next;
      33        
377 21 50       74 look_self($this, @params)
378             and return 1;
379 0         0 return 0;
380             }
381 6         19 return 0;
382 8         69 };
383             }
384             elsif ($combinator =~ /\~/) {
385 21         22 sub { my($this) = @_;
386 21         33 my @left = $this->left;
387 21         455 while(@left) {
388 51         52 $this = pop @left;
389 51 50 33     112 ref $this && $this->{_tag} !~ /^~/ or next;
390 51 100       55 look_self($this, @params)
391             and return 1;
392             }
393 0         0 return 0;
394             }
395 4         14 }
396             else {
397 0         0 die "Weird combinator '$combinator'"
398             }
399             };
400             }
401             elsif($banroot && !$set->{is_root}) {
402             # if :root was not specified, $root should never match
403 25         63 push @params, sub { $_[0] != $root }; # likely to succeed, so fail late
  151         3770  
404             }
405              
406             # do the :has tests last, because it's a complete subtree scan and that may be very slow.
407 75 50       155 push @params, @{$set->{has}} if $set->{has};
  0         0  
408 75 50       404 return wantarray ? @params : \@params;
409 41         385 };
410             }
411              
412 41 100       171 if(ref $set eq 'ARRAY') {
413 38 50       88 if(@$set > 1) {
414 0         0 my %tags;
415 0   0     0 my @alt = map { $tags{$_->{tag}||'*'} = 1; [ $recurse->($_, $refroot) ] } @$set;
  0         0  
  0         0  
416 0   0 0   0 my @params = sub { my($this) = @_; look_self($this, @$_) and return 1 foreach @alt; return 0 };;
  0         0  
  0         0  
  0         0  
417 0 0   0   0 unshift @params, sub { $tags{(shift)->{_tag}} } unless $tags{'*'};
  0         0  
418 0 0       0 return wantarray ? @params : \@params;
419             }
420 38         64 ($set) = @$set; # non-destructive
421             }
422 41 100       134 if(ref $set eq 'HASH') {
    50          
423 38         77 return $recurse->($set, $refroot);
424             }
425             elsif(ref $set) {
426             # assumed method call
427 3         35 return criteria($set->{parsed}, $refroot);
428             }
429             }
430              
431       11 0   sub parse_pseudo {
432             # nop, for subclassing
433             }
434              
435             sub find_closure {
436 32     32 0 39 my $sets = shift;
437 32 50       71 $sets = $sets->{parsed} if ref $sets ne 'ARRAY';
438 32         36 my $root; # The embedded variable
439 32         45 my(@down, @via_right, $right_down, @right_filter);
440 32         54 foreach my $set (@$sets) {
441 32 100       58 unless($set->{sibling_root}) {
    100          
442 29         57 push @down, $set;
443             }
444 0         0 elsif(ref $set->{sibling_root}) {
445 1         2 push @via_right, $set;
446 1         2 push @right_filter, $set->{sibling_root};
447 1         2 $right_down = 1;
448             }
449             else {
450 2         5 push @via_right, $set;
451 2         6 push @right_filter, $set;
452             }
453             }
454 32         52 foreach my $array(\@down, \@via_right, \@right_filter) {
455 96 100       188 @$array = criteria($array, \$root) if @$array;
456             }
457              
458 32 100       50 unless(@via_right) {
459             # the most common case: down only
460 29 100       72 if(grep { !$_->{root_child} or ref $_->{root_child} } @$sets) {
  29 100       107  
461             # keep it simple
462             return sub {
463 28     28   34 $root = shift; return $root->look_down(@down)
  28         126  
464 26         123 };
465             }
466             else {
467             # only direct children
468             return sub {
469 5     5   7 $root = shift;
470 5 50       19 my @result = grep { ref and look_self($_, @down) } $root->content_list;
  34         114  
471 5 100       50 return wantarray ? @result : shift @result;
472 3         20 };
473             }
474             }
475             else {
476             return sub {
477 6     6   10 $root = shift;
478 6         10 my($result, @result);
479 6 50       15 if(@down) {
480             # unlikely, but possible
481 0 0       0 unless(wantarray) {
482 0 0       0 $result = $root->look_down(@down) and return $result;
483             }
484             else {
485 0         0 @result = $root->look_down(@down);
486             }
487             }
488 6 50       23 if(my @right = grep { ref and look_self($_, @right_filter) } $root->right) {
  18 50       230  
489 6 100       53 unless($right_down) {
490 4 100       37 return wantarray ? (@result, @right) : shift @right;
491             }
492 2 100       5 unless(wantarray) {
493 1   50     5 $result = $_->look_down(@via_right) and return $result foreach @right;
494             }
495             else {
496 1         6 push @result, $_->look_down(@via_right) foreach @right;
497             }
498             }
499 1 50       57 wantarray and return @result;
500 0         0 return;
501 3         20 };
502             }
503             }
504              
505             # flipped
506             sub find {
507 39     39 1 63 my($self, $element) = @_;
508 39   66     153 return ($self->{find} ||= find_closure($self->{parsed}))->($element)
509             }
510              
511             package HTML::Selector::Element::Trait;
512             # core methods for trait that adds or overrides Selector support in HTML::Element
513             # use as a superclass in a subclass of HTML::Element, putting it before HTML::Element in @ISA
514             # or monkeypatch HTML::Element: import it into the HTML::Element package
515              
516             require Carp;
517              
518 11     11   134 use Exporter 'import';
  11         28  
  11         9626  
519             our @EXPORT = qw(&find is closest);
520             our @EXPORT_OK = qw(look_self siblings children &select &query);
521             our @CARP_NOT = qw(HTML::Selector::Element);
522              
523             sub children { # child elements, no fake elements
524 66     66 0 66 my($this) = @_;
525 66 50 33     57 return grep { ref and $_->{_tag} and $_->{_tag} !~ /^~/ } @{$this->{_content}||return};
  462 50       1277  
  66         102  
526             }
527              
528             sub look_self {
529 291     291 0 361 my $this = shift;
530 291         340 my($attr, $value, $matcher);
531 291         468 while(@_) {
532             # For speed reasons, no nested scopes and no block scope lexical variables
533             ref ($attr = shift) or
534 440 0 50     1743 2 != (defined($matcher = shift) + defined($value = $this->{$attr}) || next) ? return
    100 0        
    100 66        
    100          
    50          
    100          
    100          
535             : ref $value ? # identical class and stringification or fail
536             ref $matcher eq ref $value && $matcher eq $value ? next : return
537             : ref $matcher
538             ? ref $matcher eq 'Regexp' && $value =~ $matcher ? next : return
539             : $value eq $matcher ? next : return;
540 237 100       478 ref $attr eq 'CODE' and $attr->($this) ? next : return;
    50          
541             # standard processing ends here
542 0 0       0 if(ref $attr eq 'ARRAY') {
543 0         0 my $success;
544 0         0 foreach my $rule (@$attr) {
545 0 0       0 next if ref $rule ne 'ARRAY';
546 0 0       0 $success = look_self($this, @$rule) and last;
547             }
548 0 0       0 $success and next;
549             }
550             # unknown doesn't match
551 0         0 return;
552             }
553 208         523 return $this; # matches
554             }
555              
556             my %store;
557             sub find {
558             # backward compatible with find_by_tag_name in HTMl::Element if you stick to normal tags
559             # If you do need special tags (= starting with "~"), find_by_tag_name is still available, and faster than look_down anyway
560 35     35 0 82173 my($element) = shift;
561 35   66     223 my $selector = $store{join ', ', @_} ||= HTML::Selector::Element->new(@_);
562 35         78 return $selector->find($element);
563             }
564              
565             sub is {
566 1     1 0 20612 my($element) = shift;
567 1 50       6 @_ or return;
568 1   33     16 my $selector = $store{join ', ', @_} ||= HTML::Selector::Element->new(@_);
569 1   50     11 $selector->{is} ||= [$selector->criteria];
570 1         3 return look_self($element, @{$selector->{is}});
  1         6  
571             }
572              
573             sub closest {
574 0     0 0 0 my($element) = shift;
575 0   0     0 my $selector = $store{join ', ', @_} ||= HTML::Selector::Element->new(@_);
576 0   0     0 $selector->{is} ||= [$selector->criteria];
577 0         0 return $element->look_up(@{$selector->{is}});
  0         0  
578             }
579              
580             sub select {
581             # same as find except the criteria are absolute in the DOM, instead of relative to the start element
582             # only searches down, never below siblings
583 2     2 0 32720 my($element) = shift;
584 2   33     32 my $selector = $store{join ', ', @_} ||= HTML::Selector::Element->new(@_);
585 2   50     17 $selector->{is} ||= [$selector->criteria];
586 2         5 return $element->look_down(@{$selector->{is}});
  2         23  
587             }
588              
589             # alias
590             *query = \&select;
591              
592             package HTML::Selector::Element;
593             # round up: import subs from Trait
594              
595             HTML::Selector::Element::Trait->import(qw(look_self children));
596              
597             1;