File Coverage

blib/lib/Mojo/DOM58/_CSS.pm
Criterion Covered Total %
statement 187 192 97.4
branch 187 190 98.4
condition 89 95 93.6
subroutine 28 29 96.5
pod 0 5 0.0
total 491 511 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   15 use strict;
  2         5  
  2         63  
8 2     2   10 use warnings;
  2         2  
  2         58  
9 2     2   11 use Carp 'croak';
  2         4  
  2         102  
10 2     2   1292 use Data::Dumper ();
  2         14095  
  2         117  
11              
12 2   50 2   14 use constant DEBUG => $ENV{MOJO_DOM58_CSS_DEBUG} || 0;
  2         4  
  2         6869  
13              
14             our $VERSION = '3.000';
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 1083     1083 0 2132 my $class = shift;
30 1083 50 33     8341 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 50       0  
31             }
32              
33             sub tree {
34 1083     1083 0 1717 my $self = shift;
35 1083 50       3574 return $self->{tree} unless @_;
36 0         0 $self->{tree} = shift;
37 0         0 return $self;
38             }
39              
40             sub matches {
41 44     44 0 99 my $tree = shift->tree;
42 44 100       155 return $tree->[0] ne 'tag' ? undef : _match(_compile(@_), $tree, $tree, _root($tree));
43             }
44              
45 427     427 0 1030 sub select { _select(0, shift->tree, _compile(@_)) }
46 612     612 0 1460 sub select_one { _select(1, shift->tree, _compile(@_)) }
47              
48 47 100   47   85 sub _absolutize { [map { _is_scoped($_) ? $_ : [[['pc', 'scope']], ' ', @$_] } @{shift()}] }
  50         91  
  47         82  
49              
50             sub _ancestor {
51 1470     1470   2851 my ($selectors, $current, $tree, $scope, $one, $pos) = @_;
52              
53 1470   100     7554 while ($current ne $scope && $current->[0] ne 'root' && ($current = $current->[3])) {
      66        
54 1554 100       2917 return 1 if _combinator($selectors, $current, $tree, $scope, $pos);
55 366 100       1089 return undef if $current eq $scope;
56 257 100       860 last if $one;
57             }
58              
59 173         501 return undef;
60             }
61              
62             sub _attr {
63 1226     1226   2019 my ($name_re, $value_re, $current) = @_;
64              
65 1226         1697 my $attrs = $current->[2];
66 1226         2925 for my $name (keys %$attrs) {
67 1035         1961 my $value = $attrs->{$name};
68 1035 100 100     6417 next if $name !~ $name_re || (!defined $value && defined $value_re);
      100        
69 664 100 100     5267 return 1 if !(defined $value && defined $value_re) || $value =~ $value_re;
      100        
70             }
71              
72 817         3720 return undef;
73             }
74              
75             sub _combinator {
76 8430     8430   14802 my ($selectors, $current, $tree, $scope, $pos) = @_;
77              
78             # Selector
79 8430 100       16490 return undef unless my $c = $selectors->[$pos];
80 8425 100       15975 if (ref $c) {
81 8422 100       12895 return undef unless _selector($c, $current, $tree, $scope);
82 3266 100       12744 return 1 unless $c = $selectors->[++$pos];
83             }
84              
85             # ">" (parent only)
86 1601 100       4297 return _ancestor($selectors, $current, $tree, $scope, 1, ++$pos) if $c eq '>';
87              
88             # "~" (preceding siblings)
89 615 100       1273 return _sibling($selectors, $current, $tree, $scope, 0, ++$pos) if $c eq '~';
90              
91             # "+" (immediately preceding siblings)
92 550 100       1045 return _sibling($selectors, $current, $tree, $scope, 1, ++$pos) if $c eq '+';
93              
94             # " " (ancestor)
95 484         1168 return _ancestor($selectors, $current, $tree, $scope, 0, ++$pos);
96             }
97              
98             sub _compile {
99 1137     1137   3606 my ($css, %ns) = ('' . shift, @_);
100 1137         3983 $css =~ s/^\s+//;
101 1137         2969 $css =~ s/\s+$//;
102              
103 1137         2403 my $group = [[]];
104 1137         3090 while (my $selectors = $group->[-1]) {
105 4041 100 100     14743 push @$selectors, [] unless @$selectors && ref $selectors->[-1];
106 4041         6109 my $last = $selectors->[-1];
107              
108             # Separator
109 4041 100       27364 if ($css =~ /\G\s*,\s*/gc) { push @$group, [] }
  14 100       51  
    100          
    100          
    100          
    100          
110              
111             # Combinator
112             elsif ($css =~ /\G\s*([ >+~])\s*/gc) {
113 703 100       1620 push @$last, ['pc', 'scope'] unless @$last;
114 703         2359 push @$selectors, $1;
115             }
116              
117             # Class or ID
118             elsif ($css =~ /\G([.#])((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
119 148 100       661 my ($name, $op) = $1 eq '.' ? ('class', '~') : ('id', '');
120 148         386 push @$last, ['attr', _name($name), _value($op, $2)];
121             }
122              
123             # Attributes
124             elsif ($css =~ /\G$ATTR_RE/gco) {
125 254 100       697 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 276         1289 my ($name, $args) = (lc $1, $2);
138              
139             # ":is" and ":not" (contains more selectors)
140 276 100 100     1835 $args = _compile($args, %ns) if $name eq 'has' || $name eq 'is' || $name eq 'not';
      100        
141              
142             # ":nth-*" (with An+B notation)
143 276 100       954 $args = _equation($args) if $name =~ /^nth-/;
144              
145             # ":first-*" (rewrite to ":nth-*")
146 276 100       689 ($name, $args) = ("nth-$1", [0, 1]) if $name =~ /^first-(.+)$/;
147              
148             # ":last-*" (rewrite to ":nth-*")
149 276 100       638 ($name, $args) = ("nth-$name", [-1, 1]) if $name =~ /^last-/;
150              
151 276         1096 push @$last, ['pc', $name, $args];
152             }
153              
154             # Tag
155             elsif ($css =~ /\G((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
156 1510 100 100     6268 my $alias = (my $name = $1) =~ s/^([^|]*)\|// && $1 ne '*' ? $1 : undef;
157 1510 100 100     3952 return [['invalid']] if defined $alias && length $alias && !defined $ns{$alias};
      100        
158 1509 100 100     3793 my $ns = defined $alias && length $alias ? $ns{$alias} : $alias;
159 1509 100       4157 push @$last, ['tag', $name eq '*' ? undef : _name($name), _unescape($ns)];
160             }
161              
162 1136 100       3750 else { pos $css < length $css ? croak "Unknown CSS selector: $css" : last }
163             }
164              
165 1134         1547 warn qq{-- CSS Selector ($css)\n@{[_dumper($group)]}} if DEBUG;
166 1134         3953 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 116 100   116   390 return [0, 0] unless my $equation = shift;
173              
174             # "even"
175 114 100       345 return [2, 2] if $equation =~ /^\s*even\s*$/i;
176              
177             # "odd"
178 105 100       319 return [2, 1] if $equation =~ /^\s*odd\s*$/i;
179              
180             # "4", "+4" or "-4"
181 93 100       531 return [0, $1] if $equation =~ /^\s*((?:\+|-)?\d+)\s*$/;
182              
183             # "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
184 51 100       282 return [0, 0]
185             unless $equation =~ /^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$/i;
186 50 100 100     422 return [$1 eq '-' ? -1 : !length $1 ? 1 : $1, join('', split(' ', $2 || 0))];
    100          
187             }
188              
189             sub _is_scoped {
190 1195     1195   1893 my $selector = shift;
191              
192 1195 100       1955 for my $pc (grep { $_->[0] eq 'pc' } map { ref $_ ? @$_ : () } @$selector) {
  2295         5897  
  2725         6503  
193              
194             # Selector with ":scope"
195 345 100       1050 return 1 if $pc->[1] eq 'scope';
196              
197             # Argument of functional pseudo-class with ":scope"
198 251 100 100     1385 return 1 if ($pc->[1] eq 'has' || $pc->[1] eq 'is' || $pc->[1] eq 'not') && grep { _is_scoped($_) } @{$pc->[2]};
  69   100     164  
  65         151  
199             }
200              
201 1091         3054 return undef;
202             }
203              
204             sub _match {
205 6609     6609   11689 my ($group, $current, $tree, $scope) = @_;
206 6609   100     18414 _combinator([reverse @$_], $current, $tree, $scope, 0) and return 1 for @$group;
207 4946         15431 return undef;
208             }
209              
210 1888     1888   3185 sub _name {qr/(?:^|:)\Q@{[_unescape(shift)]}\E$/}
  1888         3986  
211              
212             sub _namespace {
213 77     77   175 my ($ns, $current) = @_;
214              
215 77 100       270 my $attr = $current->[1] =~ /^([^:]+):/ ? "xmlns:$1" : 'xmlns';
216 77         165 while ($current) {
217 121 100       226 last if $current->[0] eq 'root';
218 117 100       526 return $current->[2]{$attr} eq $ns if exists $current->[2]{$attr};
219              
220 44         95 $current = $current->[3];
221             }
222              
223             # Failing to match yields true if searching for no namespace, false otherwise
224 4         29 return !length $ns;
225             }
226              
227             sub _pc {
228 1678     1678   3027 my ($class, $args, $current, $tree, $scope) = @_;
229              
230             # ":scope" (root can only be a :scope)
231 1678 100       3499 return $current eq $scope if $class eq 'scope';
232 1556 100       2767 return undef if $current->[0] eq 'root';
233              
234             # ":checked"
235             return exists $current->[2]{checked} || exists $current->[2]{selected}
236 1555 100 100     3434 if $class eq 'checked';
237              
238             # ":not"
239 1351 100       2441 return !_match($args, $current, $current, $scope) if $class eq 'not';
240              
241             # ":is"
242 1182 100       2011 return !!_match($args, $current, $current, $scope) if $class eq 'is';
243              
244             # ":has"
245 1169 100       2039 return !!_select(1, $current, $args) if $class eq 'has';
246              
247             # ":empty"
248 1140 100 100     1923 return !grep { !($_->[0] eq 'comment' || $_->[0] eq 'pi') } @$current[4 .. $#$current] if $class eq 'empty';
  58         212  
249              
250             # ":root"
251 1112 100 66     1993 return $current->[3] && $current->[3][0] eq 'root' if $class eq 'root';
252              
253             # ":any-link", ":link" and ":visited"
254 1060 100 100     3989 if ($class eq 'any-link' || $class eq 'link' || $class eq 'visited') {
      100        
255 39 100 66     195 return undef unless $current->[0] eq 'tag' && exists $current->[2]{href};
256 21         35 return !!grep { $current->[1] eq $_ } qw(a area link);
  63         164  
257             }
258              
259             # ":only-child" or ":only-of-type"
260 1021 100 100     2659 if ($class eq 'only-child' || $class eq 'only-of-type') {
261 40 100       75 my $type = $class eq 'only-of-type' ? $current->[1] : undef;
262 40   100     52 $_ ne $current and return undef for @{_siblings($current, $type)};
  40         67  
263 7         24 return 1;
264             }
265              
266             # ":nth-child", ":nth-last-child", ":nth-of-type" or ":nth-last-of-type"
267 981 100       1859 if (ref $args) {
268 973 100 100     2669 my $type = $class eq 'nth-of-type'
269             || $class eq 'nth-last-of-type' ? $current->[1] : undef;
270 973         1229 my @siblings = @{_siblings($current, $type)};
  973         1567  
271 973 100 100     3202 @siblings = reverse @siblings
272             if $class eq 'nth-last-child' || $class eq 'nth-last-of-type';
273              
274 973         2008 for my $i (0 .. $#siblings) {
275 4204 100       8138 next if (my $result = $args->[0] * $i + $args->[1]) < 1;
276 3026 100       5927 return undef unless my $sibling = $siblings[$result - 1];
277 2848 100       7361 return 1 if $sibling eq $current;
278             }
279             }
280              
281             # Everything else
282 393         1805 return undef;
283             }
284              
285             sub _root {
286 90     90   144 my $tree = shift;
287 90         347 $tree = $tree->[3] while $tree->[0] ne 'root';
288 90         200 return $tree;
289             }
290              
291             sub _select {
292 1066     1066   2429 my ($one, $scope, $group) = @_;
293              
294             # Scoped selectors require the whole tree to be searched
295 1066         1499 my $tree = $scope;
296 1066 100       2029 ($group, $tree) = (_absolutize($group), _root($scope)) if grep { _is_scoped($_) } @$group;
  1076         2123  
297              
298 1066         1810 my @results;
299 1066 100       4187 my @queue = @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree];
300 1066         3298 while (my $current = shift @queue) {
301 16010 100       35621 next unless $current->[0] eq 'tag';
302              
303 6384         14535 unshift @queue, @$current[4 .. $#$current];
304 6384 100       11368 next unless _match($group, $current, $tree, $scope);
305 1587 100       6409 $one ? return $current : push @results, $current;
306             }
307              
308 491 100       2953 return $one ? undef : \@results;
309             }
310              
311             sub _selector {
312 8422     8422   13778 my ($selector, $current, $tree, $scope) = @_;
313              
314             # The root might be the scope
315 8422         12581 my $is_tag = $current->[0] eq 'tag';
316 8422         11984 for my $s (@$selector) {
317 9583         13864 my $type = $s->[0];
318              
319             # Tag
320 9583 100 100     31050 if ($is_tag && $type eq 'tag') {
    100 100        
    100          
321 6575 100 100     43939 return undef if defined $s->[1] && $current->[1] !~ $s->[1];
322 3354 100 100     8948 return undef if defined $s->[2] && !_namespace($s->[2], $current);
323             }
324              
325             # Attribute
326 1226 100       2383 elsif ($is_tag && $type eq 'attr') { return undef unless _attr(@$s[1, 2], $current) }
327              
328             # Pseudo-class
329 1678 100       3196 elsif ($type eq 'pc') { return undef unless _pc(@$s[1, 2], $current, $tree, $scope) }
330              
331             # No match
332 104         340 else { return undef }
333             }
334              
335 3266         6835 return 1;
336             }
337              
338             sub _sibling {
339 131     131   267 my ($selectors, $current, $tree, $scope, $immediate, $pos) = @_;
340              
341 131         190 my $found;
342 131         184 for my $sibling (@{_siblings($current)}) {
  131         226  
343 324 100       976 return $found if $sibling eq $current;
344              
345             # "+" (immediately preceding sibling)
346 223 100       374 if ($immediate) { $found = _combinator($selectors, $sibling, $tree, $scope, $pos) }
  124         219  
347              
348             # "~" (preceding sibling)
349 99 100       178 else { return 1 if _combinator($selectors, $sibling, $tree, $scope, $pos) }
350             }
351              
352 0         0 return undef;
353             }
354              
355             sub _siblings {
356 1144     1144   1837 my ($current, $type) = @_;
357              
358 1144         1760 my $parent = $current->[3];
359 1144 100       3405 my @siblings = grep { $_->[0] eq 'tag' }
  15218         25675  
360             @$parent[($parent->[0] eq 'root' ? 1 : 4) .. $#$parent];
361 1144 100       2721 @siblings = grep { $type eq $_->[1] } @siblings if defined $type;
  643         1067  
362              
363 1144         2801 return \@siblings;
364             }
365              
366             sub _unescape {
367 3751 100   3751   12879 return undef unless defined(my $value = shift);
368              
369             # Remove escaped newlines
370 2310         4004 $value =~ s/\\\n//g;
371              
372             # Unescape Unicode characters
373 2310         3405 $value =~ s/\\([0-9a-fA-F]{1,6})\s?/pack 'U', hex $1/ge;
  35         217  
374              
375             # Remove backslash
376 2310         3432 $value =~ s/\\//g;
377              
378 2310         36705 return $value;
379             }
380              
381             sub _value {
382 402     402   1588 my ($op, $value, $insensitive) = @_;
383 402 100       1114 return undef unless defined $value;
384 354 100       818 $value = ($insensitive ? '(?i)' : '') . quotemeta _unescape($value);
385              
386             # "~=" (word)
387 354 100       1869 return qr/(?:^|\s+)$value(?:\s+|$)/ if $op eq '~';
388              
389             # "|=" (hyphen-separated)
390 286 100       754 return qr/^$value(?:-|$)/ if $op eq '|';
391              
392             # "*=" (contains)
393 276 100       660 return qr/$value/ if $op eq '*';
394              
395             # "^=" (begins with)
396 264 100       720 return qr/^$value/ if $op eq '^';
397              
398             # "$=" (ends with)
399 232 100       677 return qr/$value$/ if $op eq '$';
400              
401             # Everything else
402 201         2254 return qr/^$value$/;
403             }
404              
405             1;
406              
407             =for Pod::Coverage *EVERYTHING*
408              
409             =cut