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   417 use strictures 1;
  13         77  
  13         306  
4 13     13   845 use base qw(HTML::Zoom::SubObject);
  13         21  
  13         988  
5 13     13   59 use Carp qw(confess);
  13         23  
  13         18273  
6              
7             my $sel_char = '-\w_';
8             my $sel_meta_char = q-!"#$%&'()*+,./:;<=>?@[\]^`{|}~-;
9             my $sel_item = qr/(?:(?:\\[\Q$sel_meta_char\E])|[$sel_char])/;
10             my $sel_re = qr/($sel_item+)/;
11             my $match_value_re = qr/"?($sel_item*)"?/;
12              
13              
14 14     14 0 120 sub new { bless({}, shift) }
15              
16             sub _raw_parse_simple_selector {
17 380     380   475 for ($_[1]) { # same pos() as outside
18              
19             # '*' - match anything
20              
21             /\G\*/gc and
22 380 100   3   757 return sub { 1 };
  3         8  
23              
24             # 'element' - match on tag name
25              
26             /\G$sel_re/gc and
27 379 100       7218 return do {
28 98         232 my $name = $_[0]->_unescape($1);
29 286 50   286   1857 sub { $_[0]->{name} && $_[0]->{name} eq $name }
30 98         528 };
31              
32             # '#id' - match on id attribute
33              
34             /\G#$sel_re/gc and
35 281 100       1238 return do {
36 4         16 my $id = $_[0]->_unescape($1);
37 21 100   21   255 sub { $_[0]->{attrs}{id} && $_[0]->{attrs}{id} eq $id }
38 4         33 };
39              
40             # '.class1.class2' - match on intersection of classes
41              
42             /\G((?:\.$sel_re)+)/gc and
43 277 100       1342 return do {
44 83         155 my $cls = $1; $cls =~ s/^\.//;
  83         258  
45 83         375 my @cl = map $_[0]->_unescape($_), split(/(?
46             sub {
47 394 100   394   6940 $_[0]->{attrs}{class}
48             && !grep $_[0]->{attrs}{class} !~ /(^|\s+)\Q$_\E($|\s+)/, @cl
49             }
50 83         489 };
51              
52             # '[attr^=foo]' - match attribute with ^ anchored regex
53             /\G\[$sel_re\^=$match_value_re\]/gc and
54 194 100       1169 return do {
55 1         4 my $attribute = $_[0]->_unescape($1);
56 1         3 my $value = $_[0]->_unescape($2);
57             sub {
58 2 100   2   33 exists $_[0]->{attrs}{$attribute}
59             && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E/;
60             }
61 1         6 };
62              
63             # '[attr$=foo]' - match attribute with $ anchored regex
64             /\G\[$sel_re\$=$match_value_re\]/gc and
65 193 100       1078 return do {
66 1         4 my $attribute = $_[0]->_unescape($1);
67 1         2 my $value = $_[0]->_unescape($2);
68             sub {
69 2 100   2   28 exists $_[0]->{attrs}{$attribute}
70             && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E$/;
71             }
72 1         7 };
73              
74             # '[attr*=foo] - match attribute with regex:
75             /\G\[$sel_re\*=$match_value_re\]/gc and
76 192 100       1051 return do {
77 2         8 my $attribute = $_[0]->_unescape($1);
78 2         6 my $value = $_[0]->_unescape($2);
79             sub {
80 4 100   4   61 exists $_[0]->{attrs}{$attribute}
81             && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E/;
82             }
83 2         12 };
84              
85             # '[attr~=bar]' - match attribute contains word
86             /\G\[$sel_re~=$match_value_re\]/gc and
87 190 100       1043 return do {
88 1         3 my $attribute = $_[0]->_unescape($1);
89 1         16 my $value = $_[0]->_unescape($2);
90             sub {
91 2 100   2   26 exists $_[0]->{attrs}{$attribute}
92             && $_[0]->{attrs}{$attribute} =~ qr/\b\Q$value\E\b/;
93             }
94 1         5 };
95              
96             # '[attr!=bar]' - match attribute contains prefix (for language matches)
97             /\G\[$sel_re\|=$match_value_re\]/gc and
98 189 100       1074 return do {
99 1         3 my $attribute = $_[0]->_unescape($1);
100 1         3 my $value = $_[0]->_unescape($2);
101             sub {
102 5 100   5   55 exists $_[0]->{attrs}{$attribute}
103             && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E(?:-|$)/;
104             }
105 1         5 };
106              
107             # '[attr=bar]' - match attributes
108             /\G\[$sel_re=$match_value_re\]/gc and
109 188 100       1104 return do {
110 7         21 my $attribute = $_[0]->_unescape($1);
111 7         14 my $value = $_[0]->_unescape($2);
112             sub {
113 19 100   19   107 exists $_[0]->{attrs}{$attribute}
114             && $_[0]->{attrs}{$attribute} eq $value;
115             }
116 7         48 };
117              
118             # '[attr!=bar]' - attributes doesn't match
119             /\G\[$sel_re!=$match_value_re\]/gc and
120 181 100       1030 return do {
121 1         5 my $attribute = $_[0]->_unescape($1);
122 1         4 my $value = $_[0]->_unescape($2);
123             sub {
124 3   100 3   17 ! (exists $_[0]->{attrs}{$attribute}
125             && $_[0]->{attrs}{$attribute} eq $value);
126             }
127 1         10 };
128              
129             # '[attr]' - match attribute being present:
130             /\G\[$sel_re\]/gc and
131 180 100       744 return do {
132 2         7 my $attribute = $_[0]->_unescape($1);
133             sub {
134 5     5   17 exists $_[0]->{attrs}{$attribute};
135             }
136 2         15 };
137              
138             # none of the above matched, try catching some obvious errors:
139              
140             # indicate unmatched square bracket:
141 178 100       847 /\G\[[^\]]*/gc and $_[0]->_blam('Unmatched [');
142             }
143             }
144              
145             sub parse_selector {
146 176     176 0 408 my $self = $_[0];
147 176         185 my $sel = $_[1]; # my pos() only please
148 176 50       328 die "No selector provided" unless $sel;
149 176         331 local *_;
150 176         300 for ($sel) {
151 176         150 my @sub;
152 176         168 PARSE: { do {
  176         148  
153              
154 178         137 my @this_chain;
155              
156             # slurp selectors until we find something else:
157 178         345 while( my $sel = $self->_raw_parse_simple_selector($_) ){
158 202         461 push @this_chain, $sel;
159             }
160              
161 177 100       345 if( @this_chain == 1 )
162             {
163 152         188 push @sub, @this_chain;
164             }
165             else{
166             # make a compound match closure of everything
167             # in this chain of selectors:
168             push @sub, sub{
169 66     66   51 my $r;
170 66         88 for my $inner ( @this_chain ){
171 121 100       198 if( ! ($r = $inner->( @_ )) ){
172 39         199 return $r;
173             }
174             }
175 27         97 return $r;
176             }
177 25         83 }
178              
179             # now we're at the end or a delimiter:
180 177 100       555 last PARSE if( pos == length );
181 4 100       28 /\G\s*,\s*/gc or do {
182 2         5 /\G(.*)/;
183 2         5 $self->_blam( "Selectors not comma separated." );
184             }
185              
186             } until (pos == length) };
187 173 100       1014 return $sub[0] if (@sub == 1);
188             return sub {
189 17     17   22 foreach my $inner (@sub) {
190 32 100       56 if (my $r = $inner->(@_)) { return $r }
  4         18  
191             }
192 2         24 };
193             }
194             }
195              
196             sub _unescape {
197 215     215   359 my ($self, $escaped) = @_;
198 215         822 (my $unescaped = $escaped) =~ s/\\([\Q$sel_meta_char\E])/$1/g;
199 215         508 return $unescaped;
200             }
201              
202             sub _blam {
203 3     3   4 my ($self, $error) = @_;
204 3   50     12 my $hat = (' ' x (pos||0)).'^';
205 3         32 die "Error parsing dispatch specification: ${error}\n
206             ${_}
207             ${hat} here\n";
208             }
209              
210             1;