File Coverage

blib/lib/DOM/Tiny/_CSS.pm
Criterion Covered Total %
statement 136 140 97.1
branch 144 150 96.0
condition 23 27 85.1
subroutine 22 22 100.0
pod 0 5 0.0
total 325 344 94.4


line stmt bran cond sub pod time code
1             package DOM::Tiny::_CSS;
2              
3 1     1   3 use strict;
  1         2  
  1         21  
4 1     1   2 use warnings;
  1         1  
  1         1707  
5              
6             our $VERSION = '0.003';
7              
8             my $ESCAPE_RE = qr/\\[^0-9a-fA-F]|\\[0-9a-fA-F]{1,6}/;
9             my $ATTR_RE = qr/
10             \[
11             ((?:$ESCAPE_RE|[\w\-])+) # Key
12             (?:
13             (\W)?= # Operator
14             (?:"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)'|([^\]]+?)) # Value
15             (?:\s+(i))? # Case-sensitivity
16             )?
17             \]
18             /x;
19              
20             sub new {
21 871     871 0 838 my $class = shift;
22 871 50 33     6855 bless @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {}, ref $class || $class;
  0 50       0  
23             }
24              
25             sub tree {
26 871     871 0 808 my $self = shift;
27 871 50       2519 return $self->{tree} unless @_;
28 0         0 $self->{tree} = shift;
29 0         0 return $self;
30             }
31              
32             sub matches {
33 29     29 0 39 my $tree = shift->tree;
34 29 100       78 return $tree->[0] ne 'tag' ? undef : _match(_compile(shift), $tree, $tree);
35             }
36              
37 389     389 0 618 sub select { _select(0, shift->tree, _compile(@_)) }
38 453     453 0 739 sub select_one { _select(1, shift->tree, _compile(@_)) }
39              
40             sub _ancestor {
41 1295     1295   1437 my ($selectors, $current, $tree, $one, $pos) = @_;
42              
43 1295         2216 while ($current = $current->[3]) {
44 1363 100 100     5500 return undef if $current->[0] eq 'root' || $current eq $tree;
45 1281 100       1576 return 1 if _combinator($selectors, $current, $tree, $pos);
46 205 100       438 last if $one;
47             }
48              
49 137         299 return undef;
50             }
51              
52             sub _attr {
53 981     981   783 my ($name_re, $value_re, $current) = @_;
54              
55 981         848 my $attrs = $current->[2];
56 981         1560 for my $name (keys %$attrs) {
57 848 100       3591 next unless $name =~ $name_re;
58 531 100 100     1978 return 1 unless defined $attrs->{$name} && defined $value_re;
59 497 100       2367 return 1 if $attrs->{$name} =~ $value_re;
60             }
61              
62 638         2576 return undef;
63             }
64              
65             sub _combinator {
66 6877     6877   6413 my ($selectors, $current, $tree, $pos) = @_;
67              
68             # Selector
69 6877 50       10650 return undef unless my $c = $selectors->[$pos];
70 6877 50       9899 if (ref $c) {
71 6877 100       8246 return undef unless _selector($c, $current);
72 2794 100       10396 return 1 unless $c = $selectors->[++$pos];
73             }
74              
75             # ">" (parent only)
76 1392 100       3082 return _ancestor($selectors, $current, $tree, 1, ++$pos) if $c eq '>';
77              
78             # "~" (preceding siblings)
79 518 100       909 return _sibling($selectors, $current, $tree, 0, ++$pos) if $c eq '~';
80              
81             # "+" (immediately preceding siblings)
82 473 100       858 return _sibling($selectors, $current, $tree, 1, ++$pos) if $c eq '+';
83              
84             # " " (ancestor)
85 421         766 return _ancestor($selectors, $current, $tree, 0, ++$pos);
86             }
87              
88             sub _compile {
89 898     898   1542 my $css = "$_[0]";
90 898         2257 $css =~ s/^\s+//;
91 898         1737 $css =~ s/\s+$//;
92              
93 898         1370 my $group = [[]];
94 898         2266 while (my $selectors = $group->[-1]) {
95 3244 100 100     10933 push @$selectors, [] unless @$selectors && ref $selectors->[-1];
96 3244         2948 my $last = $selectors->[-1];
97              
98             # Separator
99 3244 100       20066 if ($css =~ /\G\s*,\s*/gc) { push @$group, [] }
  7 100       17  
    100          
    100          
    100          
    100          
100              
101             # Combinator
102 592         1979 elsif ($css =~ /\G\s*([ >+~])\s*/gc) { push @$selectors, $1 }
103              
104             # Class or ID
105             elsif ($css =~ /\G([.#])((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
106 131 100       355 my ($name, $op) = $1 eq '.' ? ('class', '~') : ('id', '');
107 131         201 push @$last, ['attr', _name($name), _value($op, $2)];
108             }
109              
110             # Attributes
111             elsif ($css =~ /\G$ATTR_RE/gco) {
112 197 100       356 push @$last, [
    100          
    100          
113             'attr', _name($1),
114             _value(
115             defined($2) ? $2 : '',
116             defined($3) ? $3 : defined($4) ? $4 : $5,
117             $6
118             ),
119             ];
120             }
121              
122             # Pseudo-class
123             elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
124 179         602 my ($name, $args) = (lc $1, $2);
125              
126             # ":not" (contains more selectors)
127 179 100       397 $args = _compile($args) if $name eq 'not';
128              
129             # ":nth-*" (with An+B notation)
130 179 100       535 $args = _equation($args) if $name =~ /^nth-/;
131              
132             # ":first-*" (rewrite to ":nth-*")
133 179 100       336 ($name, $args) = ("nth-$1", [0, 1]) if $name =~ /^first-(.+)$/;
134              
135             # ":last-*" (rewrite to ":nth-*")
136 179 100       324 ($name, $args) = ("nth-$name", [-1, 1]) if $name =~ /^last-/;
137              
138 179         633 push @$last, ['pc', $name, $args];
139             }
140              
141             # Tag
142             elsif ($css =~ /\G((?:$ESCAPE_RE\s|\\.|[^,.#:[ >~+])+)/gco) {
143 1240 100       4103 push @$last, ['tag', _name($1)] unless $1 eq '*';
144             }
145              
146 898         1240 else {last}
147             }
148              
149 898         2025 return $group;
150             }
151              
152 58 100   58   194 sub _empty { $_[0][0] eq 'comment' || $_[0][0] eq 'pi' }
153              
154             sub _equation {
155 98 100   98   264 return [0, 0] unless my $equation = shift;
156              
157             # "even"
158 96 100       252 return [2, 2] if $equation =~ /^\s*even\s*$/i;
159              
160             # "odd"
161 87 100       222 return [2, 1] if $equation =~ /^\s*odd\s*$/i;
162              
163             # "4", "+4" or "-4"
164 75 100       363 return [0, $1] if $equation =~ /^\s*((?:\+|-)?\d+)\s*$/;
165              
166             # "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
167 51 100       280 return [0, 0]
168             unless $equation =~ /^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$/i;
169 50 100 100     425 return [$1 eq '-' ? -1 : !length $1 ? 1 : $1, join('', split(' ', $2 || 0))];
    100          
170             }
171              
172             sub _match {
173 5391     5391   5384 my ($group, $current, $tree) = @_;
174 5391   100     13684 _combinator([reverse @$_], $current, $tree, 0) and return 1 for @$group;
175 3993         11409 return undef;
176             }
177              
178 1554     1554   1294 sub _name {qr/(?:^|:)\Q@{[_unescape(shift)]}\E$/}
  1554         2046  
179              
180             sub _pc {
181 1354     1354   1286 my ($class, $args, $current) = @_;
182              
183             # ":checked"
184             return exists $current->[2]{checked} || exists $current->[2]{selected}
185 1354 100 66     2656 if $class eq 'checked';
186              
187             # ":not"
188 1153 100       1447 return !_match($args, $current, $current) if $class eq 'not';
189              
190             # ":empty"
191 1027 100       1294 return !grep { !_empty($_) } @$current[4 .. $#$current] if $class eq 'empty';
  58         47  
192              
193             # ":root"
194 1003 100 66     1409 return $current->[3] && $current->[3][0] eq 'root' if $class eq 'root';
195              
196             # ":nth-child", ":nth-last-child", ":nth-of-type" or ":nth-last-of-type"
197 956 100 100     1337 if (ref $args) {
    100          
198 908 100       1515 my $type = $class =~ /of-type$/ ? $current->[1] : undef;
199 908         653 my @siblings = @{_siblings($current, $type)};
  908         1083  
200 908 100       2053 @siblings = reverse @siblings if $class =~ /^nth-last/;
201              
202 908         1564 for my $i (0 .. $#siblings) {
203 3951 100       6486 next if (my $result = $args->[0] * $i + $args->[1]) < 1;
204 2848 100       4136 last unless my $sibling = $siblings[$result - 1];
205 2670 100       6453 return 1 if $sibling eq $current;
206             }
207             }
208              
209             # ":only-child" or ":only-of-type"
210             elsif ($class eq 'only-child' || $class eq 'only-of-type') {
211 40 100       53 my $type = $class eq 'only-of-type' ? $current->[1] : undef;
212 40   100     33 $_ ne $current and return undef for @{_siblings($current, $type)};
  40         42  
213 7         19 return 1;
214             }
215              
216 535         2486 return undef;
217             }
218              
219             sub _select {
220 842     842   1157 my ($one, $tree, $group) = @_;
221              
222 842         739 my @results;
223 842 100       3008 my @queue = @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree];
224 842         2306 while (my $current = shift @queue) {
225 13226 100       26180 next unless $current->[0] eq 'tag';
226              
227 5237         8532 unshift @queue, @$current[4 .. $#$current];
228 5237 100       6749 next unless _match($group, $current, $tree);
229 1349 100       5289 $one ? return $current : push @results, $current;
230             }
231              
232 424 100       2211 return $one ? undef : \@results;
233             }
234              
235             sub _selector {
236 6877     6877   5557 my ($selector, $current) = @_;
237              
238 6877         6676 for my $s (@$selector) {
239 7763         6916 my $type = $s->[0];
240              
241             # Tag
242 7763 100       10951 if ($type eq 'tag') { return undef unless $current->[1] =~ $s->[1] }
  5428 100       31885  
    100          
    50          
243              
244             # Attribute
245 981 100       1192 elsif ($type eq 'attr') { return undef unless _attr(@$s[1, 2], $current) }
246              
247             # Pseudo-class
248 1354 100       1900 elsif ($type eq 'pc') { return undef unless _pc(@$s[1, 2], $current) }
249             }
250              
251 2794         5062 return 1;
252             }
253              
254             sub _sibling {
255 97     97   97 my ($selectors, $current, $tree, $immediate, $pos) = @_;
256              
257 97         68 my $found;
258 97         70 for my $sibling (@{_siblings($current)}) {
  97         93  
259 261 100       962 return $found if $sibling eq $current;
260              
261             # "+" (immediately preceding sibling)
262 187 100       183 if ($immediate) { $found = _combinator($selectors, $sibling, $tree, $pos) }
  108         128  
263              
264             # "~" (preceding sibling)
265 79 100       98 else { return 1 if _combinator($selectors, $sibling, $tree, $pos) }
266             }
267              
268 0         0 return undef;
269             }
270              
271             sub _siblings {
272 1045     1045   1128 my ($current, $type) = @_;
273              
274 1045         928 my $parent = $current->[3];
275 1045 100       2518 my @siblings = grep { $_->[0] eq 'tag' }
  14313         16156  
276             @$parent[($parent->[0] eq 'root' ? 1 : 4) .. $#$parent];
277 1045 100       2229 @siblings = grep { $type eq $_->[1] } @siblings if defined $type;
  643         879  
278              
279 1045         2249 return \@siblings;
280             }
281              
282             sub _unescape {
283 1855     1855   2141 my $value = shift;
284              
285             # Remove escaped newlines
286 1855         1943 $value =~ s/\\\n//g;
287              
288             # Unescape Unicode characters
289 1855         1713 $value =~ s/\\([0-9a-fA-F]{1,6})\s?/pack 'U', hex $1/ge;
  35         145  
290              
291             # Remove backslash
292 1855         1545 $value =~ s/\\//g;
293              
294 1855         29406 return $value;
295             }
296              
297             sub _value {
298 328     328   601 my ($op, $value, $insensitive) = @_;
299 328 100       647 return undef unless defined $value;
300 301 100       489 $value = ($insensitive ? '(?i)' : '') . quotemeta _unescape($value);
301              
302             # "~=" (word)
303 301 100       1389 return qr/(?:^|\s+)$value(?:\s+|$)/ if $op eq '~';
304              
305             # "*=" (contains)
306 238 100       521 return qr/$value/ if $op eq '*';
307              
308             # "^=" (begins with)
309 227 100       546 return qr/^$value/ if $op eq '^';
310              
311             # "$=" (ends with)
312 197 100       492 return qr/$value$/ if $op eq '$';
313              
314             # Everything else
315 166         1692 return qr/^$value$/;
316             }
317              
318             1;
319              
320             =for Pod::Coverage *EVERYTHING*
321              
322             =cut