File Coverage

blib/lib/Mojo/DOM58/_CSS.pm
Criterion Covered Total %
statement 192 197 97.4
branch 191 194 98.4
condition 94 101 93.0
subroutine 28 29 96.5
pod 0 5 0.0
total 505 526 96.0


line stmt bran cond sub pod time code
1             package Mojo::DOM58::_CSS;
2              
3             # This file is part of Mojo::DOM58 which is released under:
4             # The Artistic License 2.0 (GPL Compatible)
5             # See the documentation for Mojo::DOM58 for full license details.
6              
7 2     2   18 use strict;
  2         4  
  2         118  
8 2     2   16 use warnings;
  2         4  
  2         121  
9 2     2   10 use Carp 'croak';
  2         4  
  2         124  
10 2     2   1137 use Data::Dumper ();
  2         18766  
  2         98  
11              
12 2   50 2   15 use constant DEBUG => $ENV{MOJO_DOM58_CSS_DEBUG} || 0;
  2         4  
  2         8871  
13              
14             our $VERSION = '3.002';
15              
16             my $ESCAPE_RE = qr/\\[^0-9a-fA-F]|\\[0-9a-fA-F]{1,6}/;
17             my $ATTR_RE = qr/
18             \[
19             ((?:$ESCAPE_RE|[\w\-])+) # Key
20             (?:
21             (\W)?= # Operator
22             (?:"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)'|([^\]]+?)) # Value
23             (?:\s+(?:(i|I)|s|S))? # Case-sensitivity
24             )?
25             \]
26             /x;
27              
28             sub new {
29 1118     1118 0 2201 my $class = shift;
30 1118 50 33     10071 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 50       0  
31             }
32              
33             sub tree {
34 1118     1118 0 1700 my $self = shift;
35 1118 50       4128 return $self->{tree} unless @_;
36 0         0 $self->{tree} = shift;
37 0         0 return $self;
38             }
39              
40             sub matches {
41 44     44 0 118 my $tree = shift->tree;
42 44 100       179 return $tree->[0] ne 'tag' ? undef : _match(_compile(@_), $tree, $tree, _root($tree));
43             }
44              
45 428     428 0 1516 sub select { _select(0, shift->tree, _compile(@_)) }
46 646     646 0 1919 sub select_one { _select(1, shift->tree, _compile(@_)) }
47              
48 47 100   47   70 sub _absolutize { [map { _is_scoped($_) ? $_ : [[['pc', 'scope']], ' ', @$_] } @{shift()}] }
  50         85  
  47         76  
49              
50             sub _ancestor {
51 1491     1491   3114 my ($selectors, $current, $tree, $scope, $one, $pos) = @_;
52              
53 1491   100     20197 while ($current ne $scope && $current->[0] ne 'root' && ($current = $current->[3])) {
      66        
54 1587 100       3633 return 1 if _combinator($selectors, $current, $tree, $scope, $pos);
55 393 100       1386 return undef if $current eq $scope;
56 269 100       1004 last if $one;
57             }
58              
59 173         470 return undef;
60             }
61              
62             sub _attr {
63 1234     1234   2170 my ($name_re, $value_re, $current) = @_;
64              
65 1234         1904 my $attrs = $current->[2];
66 1234         3098 for my $name (keys %$attrs) {
67 1025         2174 my $value = $attrs->{$name};
68 1025 100 100     7951 next if $name !~ $name_re || (!defined $value && defined $value_re);
      100        
69 669 100 100     5661 return 1 if !(defined $value && defined $value_re) || $value =~ $value_re;
      100        
70             }
71              
72 824         4256 return undef;
73             }
74              
75             sub _combinator {
76 8608     8608   16573 my ($selectors, $current, $tree, $scope, $pos) = @_;
77              
78             # Selector
79 8608 100       17793 return undef unless my $c = $selectors->[$pos];
80 8603 100       16777 if (ref $c) {
81 8600 100       15883 return undef unless _selector($c, $current, $tree, $scope);
82 3328 100       15658 return 1 unless $c = $selectors->[++$pos];
83             }
84              
85             # ">" (parent only)
86 1622 100       4405 return _ancestor($selectors, $current, $tree, $scope, 1, ++$pos) if $c eq '>';
87              
88             # "~" (preceding siblings)
89 636 100       1587 return _sibling($selectors, $current, $tree, $scope, 0, ++$pos) if $c eq '~';
90              
91             # "+" (immediately preceding siblings)
92 571 100       1217 return _sibling($selectors, $current, $tree, $scope, 1, ++$pos) if $c eq '+';
93              
94             # " " (ancestor)
95 505         1404 return _ancestor($selectors, $current, $tree, $scope, 0, ++$pos);
96             }
97              
98             sub _compile {
99 1172     1172   4121 my ($css, %ns) = ('' . shift, @_);
100 1172         8892 $css =~ s/^\s+//;
101 1172         3517 $css =~ s/\s+$//;
102              
103 1172         2785 my $group = [[]];
104 1172         3583 while (my $selectors = $group->[-1]) {
105 4126 100 100     16155 push @$selectors, [] unless @$selectors && ref $selectors->[-1];
106 4126         6234 my $last = $selectors->[-1];
107              
108             # Separator
109 4126 100       34022 if ($css =~ /\G\s*,\s*/gc) { push @$group, [] }
  14 100       47  
    100          
    100          
    100          
    100          
110              
111             # Combinator
112             elsif ($css =~ /\G\s*([ >+~])\s*/gc) {
113 708 100       1760 push @$last, ['pc', 'scope'] unless @$last;
114 708         2807 push @$selectors, $1;
115             }
116              
117             # Class or ID
118             elsif ($css =~ /\G([.#])((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
119 149 100       744 my ($name, $op) = $1 eq '.' ? ('class', '~') : ('id', '');
120 149         447 push @$last, ['attr', _name($name), _value($op, $2)];
121             }
122              
123             # Attributes
124             elsif ($css =~ /\G$ATTR_RE/gco) {
125 254 100       695 push @$last, [
    100          
    100          
126             'attr', _name($1),
127             _value(
128             defined($2) ? $2 : '',
129             defined($3) ? $3 : defined($4) ? $4 : $5,
130             $6
131             ),
132             ];
133             }
134              
135             # Pseudo-class
136             elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
137 298         1578 my ($name, $args) = (lc $1, $2);
138              
139             # ":text" (raw text)
140 298 100       1097 $args = [$args =~ m!^/(.+)/$! ? qr/$1/ : qr/\Q$args\E/i] if $name eq 'text';
    100          
141              
142             # ":is" and ":not" (contains more selectors)
143 298 100 100     1903 $args = _compile($args, %ns) if $name eq 'has' || $name eq 'is' || $name eq 'not';
      100        
144              
145             # ":nth-*" (with An+B notation)
146 298 100       982 $args = _equation($args) if $name =~ /^nth-/;
147              
148             # ":first-*", ":last-*" (rewrite to ":nth-(last-)*")
149 298 100       1041 ($name, $args) = ("nth-$+", [0, 1]) if $name =~ /^(?:first-(.+)|(last-.+))$/;
150              
151 298         1305 push @$last, ['pc', $name, $args];
152             }
153              
154             # Tag
155             elsif ($css =~ /\G((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
156 1532 100 100     8275 my $alias = (my $name = $1) =~ s/^([^|]*)\|// && $1 ne '*' ? $1 : undef;
157 1532 100 100     4102 return [['invalid']] if defined $alias && length $alias && !defined $ns{$alias};
      100        
158 1531 100 100     4283 my $ns = defined $alias && length $alias ? $ns{$alias} : $alias;
159 1531 100       5155 push @$last, ['tag', $name eq '*' ? undef : _name($name), _unescape($ns)];
160             }
161              
162 1171 100       4143 else { pos $css < length $css ? croak "Unknown CSS selector: $css" : last }
163             }
164              
165 1169         1854 warn qq{-- CSS Selector ($css)\n@{[_dumper($group)]}} if DEBUG;
166 1169         4422 return $group;
167             }
168              
169 0     0   0 sub _dumper { Data::Dumper->new([@_])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)->Dump }
170              
171             sub _equation {
172 117 100   117   418 return [0, 0] unless my $equation = shift;
173              
174             # "even"
175 115 100       377 return [2, 0] if $equation =~ /^\s*even\s*$/i;
176              
177             # "odd"
178 106 100       422 return [2, 1] if $equation =~ /^\s*odd\s*$/i;
179              
180             # "4", "+4" or "-4"
181 94 100       620 return [0, $1] if $equation =~ /^\s*((?:\+|-)?\d+)\s*$/;
182              
183             # "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
184 52 100       332 return [0, 0]
185             unless $equation =~ /^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$/i;
186 51 100 100     517 return [$1 eq '-' ? -1 : !length $1 ? 1 : $1, join('', split(' ', $2 || 0))];
    100          
187             }
188              
189             sub _is_scoped {
190 1230     1230   1845 my $selector = shift;
191              
192 1230 100       2237 for my $pc (grep { $_->[0] eq 'pc' } map { ref $_ ? @$_ : () } @$selector) {
  2340         6256  
  2770         6270  
193              
194             # Selector with ":scope"
195 367 100       1154 return 1 if $pc->[1] eq 'scope';
196              
197             # Argument of functional pseudo-class with ":scope"
198 273 100 100     1960 return 1 if ($pc->[1] eq 'has' || $pc->[1] eq 'is' || $pc->[1] eq 'not') && grep { _is_scoped($_) } @{$pc->[2]};
  69   100     163  
  65         207  
199             }
200              
201 1126         3667 return undef;
202             }
203              
204             sub _match {
205 6754     6754   13891 my ($group, $current, $tree, $scope) = @_;
206 6754   100     31408 _combinator([reverse @$_], $current, $tree, $scope, 0) and return 1 for @$group;
207 5050         18107 return undef;
208             }
209              
210 1911     1911   3402 sub _name {qr/(?:^|:)\Q@{[_unescape(shift)]}\E$/}
  1911         4309  
211              
212             sub _namespace {
213 77     77   159 my ($ns, $current) = @_;
214              
215 77 100       206 my $attr = $current->[1] =~ /^([^:]+):/ ? "xmlns:$1" : 'xmlns';
216 77         172 while ($current) {
217 121 100       248 last if $current->[0] eq 'root';
218 117 100       559 return $current->[2]{$attr} eq $ns if exists $current->[2]{$attr};
219              
220 44         97 $current = $current->[3];
221             }
222              
223             # Failing to match yields true if searching for no namespace, false otherwise
224 4         35 return !length $ns;
225             }
226              
227             sub _pc {
228 1791     1791   3975 my ($class, $args, $current, $tree, $scope) = @_;
229              
230             # ":scope" (root can only be a :scope)
231 1791 100       3978 return $current eq $scope if $class eq 'scope';
232 1669 100       3520 return undef if $current->[0] eq 'root';
233              
234             # ":checked"
235             return exists $current->[2]{checked} || exists $current->[2]{selected}
236 1664 100 100     4120 if $class eq 'checked';
237              
238             # ":not"
239 1460 100       3112 return !_match($args, $current, $current, $scope) if $class eq 'not';
240              
241             # ":is"
242 1281 100       2558 return !!_match($args, $current, $current, $scope) if $class eq 'is';
243              
244             # ":has"
245 1268 100       2686 return !!_select(1, $current, $args) if $class eq 'has';
246              
247             # ":empty"
248 1239 100 100     2447 return !grep { !($_->[0] eq 'comment' || $_->[0] eq 'pi') } @$current[4 .. $#$current] if $class eq 'empty';
  58         201  
249              
250             # ":root"
251 1211 100 66     2533 return $current->[3] && $current->[3][0] eq 'root' if $class eq 'root';
252              
253             # ":text"
254 1159 100 66     2423 return grep { ($_->[0] eq 'text' || $_->[0] eq 'raw') && $_->[1] =~ $args->[0] } @$current[4 .. $#$current]
  232 100       911  
255             if $class eq 'text';
256              
257             # ":any-link", ":link" and ":visited"
258 1079 100 100     6917 if ($class eq 'any-link' || $class eq 'link' || $class eq 'visited') {
      100        
259 39 100 66     301 return undef unless $current->[0] eq 'tag' && exists $current->[2]{href};
260 21         46 return !!grep { $current->[1] eq $_ } qw(a area link);
  63         202  
261             }
262              
263             # ":only-child" or ":only-of-type"
264 1040 100 100     3541 if ($class eq 'only-child' || $class eq 'only-of-type') {
265 40 100       160 my $type = $class eq 'only-of-type' ? $current->[1] : undef;
266 40   100     65 $_ ne $current and return undef for @{_siblings($current, $type)};
  40         86  
267 7         36 return 1;
268             }
269              
270             # ":nth-child", ":nth-last-child", ":nth-of-type" or ":nth-last-of-type"
271 1000 100       2360 if (ref $args) {
272 992 100 100     3150 my $type = $class eq 'nth-of-type'
273             || $class eq 'nth-last-of-type' ? $current->[1] : undef;
274 992         1401 my @siblings = @{_siblings($current, $type)};
  992         2040  
275 992         2055 my $index;
276 992         2847 for my $i (0 .. $#siblings) {
277 3648 100       10730 $index = $i, last if $siblings[$i] eq $current;
278             }
279 992 100 100     3913 $index = $#siblings - $index if $class eq 'nth-last-child' || $class eq 'nth-last-of-type';
280 992         1785 $index++;
281              
282 992         10967 my $delta = $index - $args->[1];
283 992 100       2750 return 1 if $delta == 0;
284 800   100     7225 return $args->[0] != 0 && ($delta < 0) == ($args->[0] < 0) && $delta % $args->[0] == 0;
285             }
286              
287             # Everything else
288 8         55 return undef;
289             }
290              
291             sub _root {
292 90     90   144 my $tree = shift;
293 90         402 $tree = $tree->[3] while $tree->[0] ne 'root';
294 90         192 return $tree;
295             }
296              
297             sub _select {
298 1101     1101   2671 my ($one, $scope, $group) = @_;
299              
300             # Scoped selectors require the whole tree to be searched
301 1101         1615 my $tree = $scope;
302 1101 100       2610 ($group, $tree) = (_absolutize($group), _root($scope)) if grep { _is_scoped($_) } @$group;
  1111         2812  
303              
304 1101         1674 my @results;
305 1101 100       5219 my @queue = @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree];
306 1101         3208 while (my $current = shift @queue) {
307 16388 100       59528 next unless $current->[0] eq 'tag';
308              
309 6519         24605 unshift @queue, @$current[4 .. $#$current];
310 6519 100       13657 next unless _match($group, $current, $tree, $scope);
311 1625 100       7043 $one ? return $current : push @results, $current;
312             }
313              
314 499 100       3679 return $one ? undef : \@results;
315             }
316              
317             sub _selector {
318 8600     8600   15768 my ($selector, $current, $tree, $scope) = @_;
319              
320             # The root might be the scope
321 8600         15301 my $is_tag = $current->[0] eq 'tag';
322 8600         15131 for my $s (@$selector) {
323 9780         15163 my $type = $s->[0];
324              
325             # Tag
326 9780 100 100     45160 if ($is_tag && $type eq 'tag') {
    100 100        
    100          
327 6640 100 100     51894 return undef if defined $s->[1] && $current->[1] !~ $s->[1];
328 3382 100 100     13582 return undef if defined $s->[2] && !_namespace($s->[2], $current);
329             }
330              
331             # Attribute
332 1234 100       2653 elsif ($is_tag && $type eq 'attr') { return undef unless _attr(@$s[1, 2], $current) }
333              
334             # Pseudo-class
335 1791 100       4335 elsif ($type eq 'pc') { return undef unless _pc(@$s[1, 2], $current, $tree, $scope) }
336              
337             # No match
338 115         466 else { return undef }
339             }
340              
341 3328         9932 return 1;
342             }
343              
344             sub _sibling {
345 131     131   345 my ($selectors, $current, $tree, $scope, $immediate, $pos) = @_;
346              
347 131         206 my $found;
348 131         187 for my $sibling (@{_siblings($current)}) {
  131         303  
349 324 100       1344 return $found if $sibling eq $current;
350              
351             # "+" (immediately preceding sibling)
352 223 100       468 if ($immediate) { $found = _combinator($selectors, $sibling, $tree, $scope, $pos) }
  124         301  
353              
354             # "~" (preceding sibling)
355 99 100       298 else { return 1 if _combinator($selectors, $sibling, $tree, $scope, $pos) }
356             }
357              
358 0         0 return undef;
359             }
360              
361             sub _siblings {
362 1163     1163   2406 my ($current, $type) = @_;
363              
364 1163         2072 my $parent = $current->[3];
365 1163 100       4485 my @siblings = grep { $_->[0] eq 'tag' }
  15440         48854  
366             @$parent[($parent->[0] eq 'root' ? 1 : 4) .. $#$parent];
367 1163 100       3325 @siblings = grep { $type eq $_->[1] } @siblings if defined $type;
  643         1483  
368              
369 1163         3696 return \@siblings;
370             }
371              
372             sub _unescape {
373 3797 100   3797   32656 return undef unless defined(my $value = shift);
374              
375             # Remove escaped newlines
376 2334         4139 $value =~ s/\\\n//g;
377              
378             # Unescape Unicode characters
379 2334         4061 $value =~ s/\\([0-9a-fA-F]{1,6})\s?/pack 'U', hex $1/ge;
  35         225  
380              
381             # Remove backslash
382 2334         4231 $value =~ s/\\//g;
383              
384 2334         67329 return $value;
385             }
386              
387             sub _value {
388 403     403   1635 my ($op, $value, $insensitive) = @_;
389 403 100       1169 return undef unless defined $value;
390 355 100       846 $value = ($insensitive ? '(?i)' : '') . quotemeta _unescape($value);
391              
392             # "~=" (word)
393 355 100       3539 return qr/(?:^|\s+)$value(?:\s+|$)/ if $op eq '~';
394              
395             # "|=" (hyphen-separated)
396 286 100       862 return qr/^$value(?:-|$)/ if $op eq '|';
397              
398             # "*=" (contains)
399 276 100       697 return qr/$value/ if $op eq '*';
400              
401             # "^=" (begins with)
402 264 100       898 return qr/^$value/ if $op eq '^';
403              
404             # "$=" (ends with)
405 232 100       790 return qr/$value$/ if $op eq '$';
406              
407             # Everything else
408 201         2649 return qr/^$value$/;
409             }
410              
411             1;
412              
413             =for Pod::Coverage *EVERYTHING*
414              
415             =cut