File Coverage

blib/lib/HTML/Zoom/SelectorParser.pm
Criterion Covered Total %
statement 101 101 100.0
branch 56 58 96.5
condition 4 5 80.0
subroutine 22 22 100.0
pod 0 2 0.0
total 183 188 97.3


line stmt bran cond sub pod time code
1             package HTML::Zoom::SelectorParser;
2              
3 13     13   618 use strictures 1;
  13         82  
  13         346  
4 13     13   985 use base qw(HTML::Zoom::SubObject);
  13         21  
  13         1166  
5 13     13   74 use Carp qw(confess);
  13         62  
  13         25727  
6              
7             my $sel_char = '-\w_';
8             my $sel_meta_char = q-!"#$%&'()*+,./:;<=>?@[\]^`{|}~-;
9             my $sel_re = qr/((?:(?:\\[\Q$sel_meta_char\E])|[$sel_char])+)/;
10             my $match_value_re = qr/"?$sel_re"?/;
11              
12              
13 14     14 0 148 sub new { bless({}, shift) }
14              
15             sub _raw_parse_simple_selector {
16 340     340   706 for ($_[1]) { # same pos() as outside
17              
18             # '*' - match anything
19              
20             /\G\*/gc and
21 340 100   3   1190 return sub { 1 };
  3         9  
22              
23             # 'element' - match on tag name
24              
25             /\G$sel_re/gc and
26 339 100       13572 return do {
27 88         549 my $name = $_[0]->_unescape($1);
28 260 50   260   2370 sub { $_[0]->{name} && $_[0]->{name} eq $name }
29 88         598 };
30              
31             # '#id' - match on id attribute
32              
33             /\G#$sel_re/gc and
34 251 100       2556 return do {
35 4         16 my $id = $_[0]->_unescape($1);
36 21 100   21   207 sub { $_[0]->{attrs}{id} && $_[0]->{attrs}{id} eq $id }
37 4         33 };
38              
39             # '.class1.class2' - match on intersection of classes
40              
41             /\G((?:\.$sel_re)+)/gc and
42 247 100       1918 return do {
43 69         324 my $cls = $1; $cls =~ s/^\.//;
  69         241  
44 69         477 my @cl = map $_[0]->_unescape($_), split(/(?<!\\)\./, $cls);
45             sub {
46 348 100   348   9370 $_[0]->{attrs}{class}
47             && !grep $_[0]->{attrs}{class} !~ /(^|\s+)\Q$_\E($|\s+)/, @cl
48             }
49 69         550 };
50              
51             # '[attr^=foo]' - match attribute with ^ anchored regex
52             /\G\[$sel_re\^=$match_value_re\]/gc and
53 178 100       1728 return do {
54 1         5 my $attribute = $_[0]->_unescape($1);
55 1         5 my $value = $_[0]->_unescape($2);
56             sub {
57 2 100   2   44 $_[0]->{attrs}{$attribute}
58             && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E/;
59             }
60 1         8 };
61              
62             # '[attr$=foo]' - match attribute with $ anchored regex
63             /\G\[$sel_re\$=$match_value_re\]/gc and
64 177 100       1975 return do {
65 1         6 my $attribute = $_[0]->_unescape($1);
66 1         5 my $value = $_[0]->_unescape($2);
67             sub {
68 2 100   2   39 $_[0]->{attrs}{$attribute}
69             && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E$/;
70             }
71 1         6 };
72              
73             # '[attr*=foo] - match attribute with regex:
74             /\G\[$sel_re\*=$match_value_re\]/gc and
75 176 100       1741 return do {
76 2         7 my $attribute = $_[0]->_unescape($1);
77 2         8 my $value = $_[0]->_unescape($2);
78             sub {
79 4 100   4   76 $_[0]->{attrs}{$attribute}
80             && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E/;
81             }
82 2         13 };
83              
84             # '[attr~=bar]' - match attribute contains word
85             /\G\[$sel_re~=$match_value_re\]/gc and
86 174 100       7794 return do {
87 1         4 my $attribute = $_[0]->_unescape($1);
88 1         23 my $value = $_[0]->_unescape($2);
89             sub {
90 2 100   2   37 $_[0]->{attrs}{$attribute}
91             && $_[0]->{attrs}{$attribute} =~ qr/\b\Q$value\E\b/;
92             }
93 1         8 };
94              
95             # '[attr!=bar]' - match attribute contains prefix (for language matches)
96             /\G\[$sel_re\|=$match_value_re\]/gc and
97 173 100       1870 return do {
98 1         4 my $attribute = $_[0]->_unescape($1);
99 1         4 my $value = $_[0]->_unescape($2);
100             sub {
101 5 100   5   84 $_[0]->{attrs}{$attribute}
102             && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E(?:-|$)/;
103             }
104 1         8 };
105              
106             # '[attr=bar]' - match attributes
107             /\G\[$sel_re=$match_value_re\]/gc and
108 172 100       1920 return do {
109 7         21 my $attribute = $_[0]->_unescape($1);
110 7         19 my $value = $_[0]->_unescape($2);
111             sub {
112 19 100   19   148 $_[0]->{attrs}{$attribute}
113             && $_[0]->{attrs}{$attribute} eq $value;
114             }
115 7         70 };
116              
117             # '[attr!=bar]' - attributes doesn't match
118             /\G\[$sel_re!=$match_value_re\]/gc and
119 165 100       1637 return do {
120 1         4 my $attribute = $_[0]->_unescape($1);
121 1         4 my $value = $_[0]->_unescape($2);
122             sub {
123 3   100 3   26 ! ($_[0]->{attrs}{$attribute}
124             && $_[0]->{attrs}{$attribute} eq $value);
125             }
126 1         8 };
127              
128             # '[attr]' - match attribute being present:
129             /\G\[$sel_re\]/gc and
130 164 100       1196 return do {
131 2         7 my $attribute = $_[0]->_unescape($1);
132             sub {
133 5     5   24 exists $_[0]->{attrs}{$attribute};
134             }
135 2         15 };
136              
137             # none of the above matched, try catching some obvious errors:
138              
139             # indicate unmatched square bracket:
140 162 100       1146 /\G\[[^\]]*/gc and $_[0]->_blam('Unmatched [');
141             }
142             }
143              
144             sub parse_selector {
145 160     160 0 688 my $self = $_[0];
146 160         227 my $sel = $_[1]; # my pos() only please
147 160 50       364 die "No selector provided" unless $sel;
148 160         404 local *_;
149 160         298 for ($sel) {
150 160         338 my @sub;
151 160         577 PARSE: { do {
  160         203  
152              
153 162         181 my @this_chain;
154              
155             # slurp selectors until we find something else:
156 162         572 while( my $sel = $self->_raw_parse_simple_selector($_) ){
157 178         570 push @this_chain, $sel;
158             }
159              
160 161 100       384 if( @this_chain == 1 )
161             {
162 144         252 push @sub, @this_chain;
163             }
164             else{
165             # make a compound match closure of everything
166             # in this chain of selectors:
167             push @sub, sub{
168 42     42   44 my $r;
169 42         69 for my $inner ( @this_chain ){
170 81 100       147 if( ! ($r = $inner->( @_ )) ){
171 23         158 return $r;
172             }
173             }
174 19         95 return $r;
175             }
176 17         79 }
177              
178             # now we're at the end or a delimiter:
179 161 100       785 last PARSE if( pos == length );
180 4 100       30 /\G\s*,\s*/gc or do {
181 2         8 /\G(.*)/;
182 2         8 $self->_blam( "Selectors not comma separated." );
183             }
184              
185             } until (pos == length) };
186 157 100       1270 return $sub[0] if (@sub == 1);
187             return sub {
188 17     17   148 foreach my $inner (@sub) {
189 32 100       64 if (my $r = $inner->(@_)) { return $r }
  4         21  
190             }
191 2         25 };
192             }
193             }
194              
195             sub _unescape {
196 191     191   423 my ($self, $escaped) = @_;
197 191         945 (my $unescaped = $escaped) =~ s/\\([\Q$sel_meta_char\E])/$1/g;
198 191         616 return $unescaped;
199             }
200              
201             sub _blam {
202 3     3   8 my ($self, $error) = @_;
203 3   50     26 my $hat = (' ' x (pos||0)).'^';
204 3         56 die "Error parsing dispatch specification: ${error}\n
205             ${_}
206             ${hat} here\n";
207             }
208              
209             1;