File Coverage

blib/lib/Mojo/DOM58/_CSS.pm
Criterion Covered Total %
statement 154 159 96.8
branch 168 172 97.6
condition 65 69 94.2
subroutine 23 23 100.0
pod 0 5 0.0
total 410 428 95.7


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   14 use strict;
  2         5  
  2         57  
8 2     2   10 use warnings;
  2         3  
  2         5850  
9              
10             our $VERSION = '2.000';
11              
12             my $ESCAPE_RE = qr/\\[^0-9a-fA-F]|\\[0-9a-fA-F]{1,6}/;
13             my $ATTR_RE = qr/
14             \[
15             ((?:$ESCAPE_RE|[\w\-])+) # Key
16             (?:
17             (\W)?= # Operator
18             (?:"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)'|([^\]]+?)) # Value
19             (?:\s+(i))? # Case-sensitivity
20             )?
21             \]
22             /x;
23              
24             sub new {
25 982     982 0 1665 my $class = shift;
26 982 50 33     6719 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 50       0  
27             }
28              
29             sub tree {
30 982     982 0 1510 my $self = shift;
31 982 50       2958 return $self->{tree} unless @_;
32 0         0 $self->{tree} = shift;
33 0         0 return $self;
34             }
35              
36             sub matches {
37 44     44 0 102 my $tree = shift->tree;
38 44 100       139 return $tree->[0] ne 'tag' ? undef : _match(_compile(@_), $tree, $tree);
39             }
40              
41 402     402 0 1032 sub select { _select(0, shift->tree, _compile(@_)) }
42 536     536 0 1213 sub select_one { _select(1, shift->tree, _compile(@_)) }
43              
44             sub _ancestor {
45 1315     1315   2359 my ($selectors, $current, $tree, $one, $pos) = @_;
46              
47 1315         2763 while ($current = $current->[3]) {
48 1385 100 100     5045 return undef if $current->[0] eq 'root' || $current eq $tree;
49 1300 100       2616 return 1 if _combinator($selectors, $current, $tree, $pos);
50 207 100       554 last if $one;
51             }
52              
53 137         366 return undef;
54             }
55              
56             sub _attr {
57 1124     1124   1856 my ($name_re, $value_re, $current) = @_;
58              
59 1124         1560 my $attrs = $current->[2];
60 1124         2452 for my $name (keys %$attrs) {
61 972         1692 my $value = $attrs->{$name};
62 972 100 100     5509 next if $name !~ $name_re || (!defined $value && defined $value_re);
      100        
63 607 100 100     4639 return 1 if !(defined $value && defined $value_re) || $value =~ $value_re;
      100        
64             }
65              
66 738         3058 return undef;
67             }
68              
69             sub _combinator {
70 7369     7369   12265 my ($selectors, $current, $tree, $pos) = @_;
71              
72             # Selector
73 7369 100       14100 return undef unless my $c = $selectors->[$pos];
74 7367 100       13284 if (ref $c) {
75 7364 100       11195 return undef unless _selector($c, $current);
76 2934 100       11219 return 1 unless $c = $selectors->[++$pos];
77             }
78              
79             # ">" (parent only)
80 1416 100       3578 return _ancestor($selectors, $current, $tree, 1, ++$pos) if $c eq '>';
81              
82             # "~" (preceding siblings)
83 528 100       1051 return _sibling($selectors, $current, $tree, 0, ++$pos) if $c eq '~';
84              
85             # "+" (immediately preceding siblings)
86 481 100       877 return _sibling($selectors, $current, $tree, 1, ++$pos) if $c eq '+';
87              
88             # " " (ancestor)
89 427         868 return _ancestor($selectors, $current, $tree, 0, ++$pos);
90             }
91              
92             sub _compile {
93 1026     1026   3136 my ($css, %ns) = ('' . shift, @_);
94 1026         3370 $css =~ s/^\s+//;
95 1026         2710 $css =~ s/\s+$//;
96              
97 1026         1949 my $group = [[]];
98 1026         2815 while (my $selectors = $group->[-1]) {
99 3640 100 100     13057 push @$selectors, [] unless @$selectors && ref $selectors->[-1];
100 3640         5320 my $last = $selectors->[-1];
101              
102             # Separator
103 3640 100       23538 if ($css =~ /\G\s*,\s*/gc) { push @$group, [] }
  11 100       34  
    100          
    100          
    100          
    100          
104              
105             # Combinator
106 619         2177 elsif ($css =~ /\G\s*([ >+~])\s*/gc) { push @$selectors, $1 }
107              
108             # Class or ID
109             elsif ($css =~ /\G([.#])((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
110 140 100       515 my ($name, $op) = $1 eq '.' ? ('class', '~') : ('id', '');
111 140         350 push @$last, ['attr', _name($name), _value($op, $2)];
112             }
113              
114             # Attributes
115             elsif ($css =~ /\G$ATTR_RE/gco) {
116 245 100       622 push @$last, [
    100          
    100          
117             'attr', _name($1),
118             _value(
119             defined($2) ? $2 : '',
120             defined($3) ? $3 : defined($4) ? $4 : $5,
121             $6
122             ),
123             ];
124             }
125              
126             # Pseudo-class
127             elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
128 234         906 my ($name, $args) = (lc $1, $2);
129              
130             # ":matches" and ":not" (contains more selectors)
131 234 100 100     967 $args = _compile($args, %ns) if $name eq 'matches' || $name eq 'not';
132              
133             # ":nth-*" (with An+B notation)
134 234 100       714 $args = _equation($args) if $name =~ /^nth-/;
135              
136             # ":first-*" (rewrite to ":nth-*")
137 234 100       537 ($name, $args) = ("nth-$1", [0, 1]) if $name =~ /^first-(.+)$/;
138              
139             # ":last-*" (rewrite to ":nth-*")
140 234 100       483 ($name, $args) = ("nth-$name", [-1, 1]) if $name =~ /^last-/;
141              
142 234         927 push @$last, ['pc', $name, $args];
143             }
144              
145             # Tag
146             elsif ($css =~ /\G((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
147 1366 100 100     5857 my $alias = (my $name = $1) =~ s/^([^|]*)\|// && $1 ne '*' ? $1 : undef;
148 1366 100 100     3534 return [['invalid']] if defined $alias && length $alias && !defined $ns{$alias};
      100        
149 1365 100 100     2814 my $ns = defined $alias && length $alias ? $ns{$alias} : $alias;
150 1365 100       3957 push @$last, ['tag', $name eq '*' ? undef : _name($name), _unescape($ns)];
151             }
152              
153 1025         1939 else {last}
154             }
155              
156 1025         3503 return $group;
157             }
158              
159 58 100   58   226 sub _empty { $_[0][0] eq 'comment' || $_[0][0] eq 'pi' }
160              
161             sub _equation {
162 113 100   113   271 return [0, 0] unless my $equation = shift;
163              
164             # "even"
165 111 100       271 return [2, 2] if $equation =~ /^\s*even\s*$/i;
166              
167             # "odd"
168 102 100       255 return [2, 1] if $equation =~ /^\s*odd\s*$/i;
169              
170             # "4", "+4" or "-4"
171 90 100       446 return [0, $1] if $equation =~ /^\s*((?:\+|-)?\d+)\s*$/;
172              
173             # "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
174 51 100       259 return [0, 0]
175             unless $equation =~ /^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$/i;
176 50 100 100     386 return [$1 eq '-' ? -1 : !length $1 ? 1 : $1, join('', split(' ', $2 || 0))];
    100          
177             }
178              
179             sub _match {
180 5850     5850   10191 my ($group, $current, $tree) = @_;
181 5850   100     15776 _combinator([reverse @$_], $current, $tree, 0) and return 1 for @$group;
182 4333         13451 return undef;
183             }
184              
185 1729     1729   2944 sub _name {qr/(?:^|:)\Q@{[_unescape(shift)]}\E$/}
  1729         3162  
186              
187             sub _namespace {
188 77     77   133 my ($ns, $current) = @_;
189              
190 77 100       189 my $attr = $current->[1] =~ /^([^:]+):/ ? "xmlns:$1" : 'xmlns';
191 77         127 while ($current) {
192 121 100       174 last if $current->[0] eq 'root';
193 117 100       391 return $current->[2]{$attr} eq $ns if exists $current->[2]{$attr};
194              
195 44         66 $current = $current->[3];
196             }
197              
198             # Failing to match yields true if searching for no namespace, false otherwise
199 4         18 return !length $ns;
200             }
201              
202             sub _pc {
203 1463     1463   2504 my ($class, $args, $current) = @_;
204              
205             # ":checked"
206             return exists $current->[2]{checked} || exists $current->[2]{selected}
207 1463 100 100     3387 if $class eq 'checked';
208              
209             # ":not"
210 1259 100       2231 return !_match($args, $current, $current) if $class eq 'not';
211              
212             # ":matches"
213 1114 100       1917 return !!_match($args, $current, $current) if $class eq 'matches';
214              
215             # ":empty"
216 1105 100       1767 return !grep { !_empty($_) } @$current[4 .. $#$current] if $class eq 'empty';
  58         89  
217              
218             # ":root"
219 1077 100 66     2029 return $current->[3] && $current->[3][0] eq 'root' if $class eq 'root';
220              
221             # ":link" and ":visited"
222 1025 100 100     2891 if ($class eq 'link' || $class eq 'visited') {
223 26 100 66     128 return undef unless $current->[0] eq 'tag' && exists $current->[2]{href};
224 14         27 return !!grep { $current->[1] eq $_ } qw(a area link);
  42         110  
225             }
226              
227             # ":only-child" or ":only-of-type"
228 999 100 100     2467 if ($class eq 'only-child' || $class eq 'only-of-type') {
229 40 100       76 my $type = $class eq 'only-of-type' ? $current->[1] : undef;
230 40   100     45 $_ ne $current and return undef for @{_siblings($current, $type)};
  40         95  
231 7         23 return 1;
232             }
233              
234             # ":nth-child", ":nth-last-child", ":nth-of-type" or ":nth-last-of-type"
235 959 100       1702 if (ref $args) {
236 951 100 100     2623 my $type = $class eq 'nth-of-type'
237             || $class eq 'nth-last-of-type' ? $current->[1] : undef;
238 951         1229 my @siblings = @{_siblings($current, $type)};
  951         1489  
239 951 100 100     3178 @siblings = reverse @siblings
240             if $class eq 'nth-last-child' || $class eq 'nth-last-of-type';
241              
242 951         1979 for my $i (0 .. $#siblings) {
243 4156 100       8074 next if (my $result = $args->[0] * $i + $args->[1]) < 1;
244 2989 100       5861 return undef unless my $sibling = $siblings[$result - 1];
245 2811 100       7089 return 1 if $sibling eq $current;
246             }
247             }
248              
249             # Everything else
250 381         1597 return undef;
251             }
252              
253             sub _select {
254 938     938   1842 my ($one, $tree, $group) = @_;
255              
256 938         1341 my @results;
257 938 100       3245 my @queue = @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree];
258 938         2525 while (my $current = shift @queue) {
259 14157 100       30682 next unless $current->[0] eq 'tag';
260              
261 5653         11598 unshift @queue, @$current[4 .. $#$current];
262 5653 100       9991 next unless _match($group, $current, $tree);
263 1457 100       5318 $one ? return $current : push @results, $current;
264             }
265              
266 445 100       2878 return $one ? undef : \@results;
267             }
268              
269             sub _selector {
270 7364     7364   11030 my ($selector, $current) = @_;
271              
272 7364         11205 for my $s (@$selector) {
273 8471         11574 my $type = $s->[0];
274              
275             # Tag
276 8471 100       15245 if ($type eq 'tag') {
    100          
    50          
277 5884 100 100     37795 return undef if defined $s->[1] && $current->[1] !~ $s->[1];
278 3057 100 100     8019 return undef if defined $s->[2] && !_namespace($s->[2], $current);
279             }
280              
281             # Attribute
282 1124 100       2017 elsif ($type eq 'attr') { return undef unless _attr(@$s[1, 2], $current) }
283              
284             # Pseudo-class
285 1463 100       2792 elsif ($type eq 'pc') { return undef unless _pc(@$s[1, 2], $current) }
286              
287             # Invalid selector
288 0         0 else { return undef }
289             }
290              
291 2934         5686 return 1;
292             }
293              
294             sub _sibling {
295 101     101   195 my ($selectors, $current, $tree, $immediate, $pos) = @_;
296              
297 101         125 my $found;
298 101         120 for my $sibling (@{_siblings($current)}) {
  101         164  
299 267 100       821 return $found if $sibling eq $current;
300              
301             # "+" (immediately preceding sibling)
302 191 100       301 if ($immediate) { $found = _combinator($selectors, $sibling, $tree, $pos) }
  110         187  
303              
304             # "~" (preceding sibling)
305 81 100       130 else { return 1 if _combinator($selectors, $sibling, $tree, $pos) }
306             }
307              
308 0         0 return undef;
309             }
310              
311             sub _siblings {
312 1092     1092   1788 my ($current, $type) = @_;
313              
314 1092         1501 my $parent = $current->[3];
315 1092 100       3174 my @siblings = grep { $_->[0] eq 'tag' }
  14887         25103  
316             @$parent[($parent->[0] eq 'root' ? 1 : 4) .. $#$parent];
317 1092 100       2465 @siblings = grep { $type eq $_->[1] } @siblings if defined $type;
  643         1053  
318              
319 1092         2619 return \@siblings;
320             }
321              
322             sub _unescape {
323 3431 100   3431   11373 return undef unless defined(my $value = shift);
324              
325             # Remove escaped newlines
326 2134         3339 $value =~ s/\\\n//g;
327              
328             # Unescape Unicode characters
329 2134         2999 $value =~ s/\\([0-9a-fA-F]{1,6})\s?/pack 'U', hex $1/ge;
  35         228  
330              
331             # Remove backslash
332 2134         3036 $value =~ s/\\//g;
333              
334 2134         31168 return $value;
335             }
336              
337             sub _value {
338 385     385   1458 my ($op, $value, $insensitive) = @_;
339 385 100       1034 return undef unless defined $value;
340 337 100       822 $value = ($insensitive ? '(?i)' : '') . quotemeta _unescape($value);
341              
342             # "~=" (word)
343 337 100       1681 return qr/(?:^|\s+)$value(?:\s+|$)/ if $op eq '~';
344              
345             # "|=" (hyphen-separated)
346 274 100       630 return qr/^$value(?:-|$)/ if $op eq '|';
347              
348             # "*=" (contains)
349 264 100       1126 return qr/$value/ if $op eq '*';
350              
351             # "^=" (begins with)
352 253 100       674 return qr/^$value/ if $op eq '^';
353              
354             # "$=" (ends with)
355 223 100       624 return qr/$value$/ if $op eq '$';
356              
357             # Everything else
358 192         2091 return qr/^$value$/;
359             }
360              
361             1;
362              
363             =for Pod::Coverage *EVERYTHING*
364              
365             =cut