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 14     14   929 use strictures 1;
  14         104  
  14         387  
4 14     14   1182 use base qw(HTML::Zoom::SubObject);
  14         33  
  14         1156  
5 14     14   88 use Carp qw(confess);
  14         27  
  14         29691  
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 15     15 0 170 sub new { bless({}, shift) }
15              
16             sub _raw_parse_simple_selector {
17 380     380   753 for ($_[1]) { # same pos() as outside
18              
19             # '*' - match anything
20              
21             /\G\*/gc and
22 380 100   3   934 return sub { 1 };
  3         9  
23              
24             # 'element' - match on tag name
25              
26             /\G$sel_re/gc and
27 379 100       13097 return do {
28 98         291 my $name = $_[0]->_unescape($1);
29 286 50   286   2407 sub { $_[0]->{name} && $_[0]->{name} eq $name }
30 98         656 };
31              
32             # '#id' - match on id attribute
33              
34             /\G#$sel_re/gc and
35 281 100       2235 return do {
36 4         19 my $id = $_[0]->_unescape($1);
37 21 100   21   220 sub { $_[0]->{attrs}{id} && $_[0]->{attrs}{id} eq $id }
38 4         141 };
39              
40             # '.class1.class2' - match on intersection of classes
41              
42             /\G((?:\.$sel_re)+)/gc and
43 277 100       1882 return do {
44 83         187 my $cls = $1; $cls =~ s/^\.//;
  83         289  
45 83         398 my @cl = map $_[0]->_unescape($_), split(/(?
46             sub {
47 394 100   394   9291 $_[0]->{attrs}{class}
48             && !grep $_[0]->{attrs}{class} !~ /(^|\s+)\Q$_\E($|\s+)/, @cl
49             }
50 83         590 };
51              
52             # '[attr^=foo]' - match attribute with ^ anchored regex
53             /\G\[$sel_re\^=$match_value_re\]/gc and
54 194 100       1800 return do {
55 1         5 my $attribute = $_[0]->_unescape($1);
56 1         4 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         8 };
62              
63             # '[attr$=foo]' - match attribute with $ anchored regex
64             /\G\[$sel_re\$=$match_value_re\]/gc and
65 193 100       1703 return do {
66 1         3 my $attribute = $_[0]->_unescape($1);
67 1         5 my $value = $_[0]->_unescape($2);
68             sub {
69 2 100   2   34 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       1636 return do {
77 2         6 my $attribute = $_[0]->_unescape($1);
78 2         6 my $value = $_[0]->_unescape($2);
79             sub {
80 4 100   4   66 exists $_[0]->{attrs}{$attribute}
81             && $_[0]->{attrs}{$attribute} =~ qr/\Q$value\E/;
82             }
83 2         24 };
84              
85             # '[attr~=bar]' - match attribute contains word
86             /\G\[$sel_re~=$match_value_re\]/gc and
87 190 100       1625 return do {
88 1         5 my $attribute = $_[0]->_unescape($1);
89 1         23 my $value = $_[0]->_unescape($2);
90             sub {
91 2 100   2   36 exists $_[0]->{attrs}{$attribute}
92             && $_[0]->{attrs}{$attribute} =~ qr/\b\Q$value\E\b/;
93             }
94 1         8 };
95              
96             # '[attr!=bar]' - match attribute contains prefix (for language matches)
97             /\G\[$sel_re\|=$match_value_re\]/gc and
98 189 100       1591 return do {
99 1         6 my $attribute = $_[0]->_unescape($1);
100 1         5 my $value = $_[0]->_unescape($2);
101             sub {
102 5 100   5   113 exists $_[0]->{attrs}{$attribute}
103             && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E(?:-|$)/;
104             }
105 1         11 };
106              
107             # '[attr=bar]' - match attributes
108             /\G\[$sel_re=$match_value_re\]/gc and
109 188 100       1742 return do {
110 7         20 my $attribute = $_[0]->_unescape($1);
111 7         19 my $value = $_[0]->_unescape($2);
112             sub {
113 19 100   19   156 exists $_[0]->{attrs}{$attribute}
114             && $_[0]->{attrs}{$attribute} eq $value;
115             }
116 7         51 };
117              
118             # '[attr!=bar]' - attributes doesn't match
119             /\G\[$sel_re!=$match_value_re\]/gc and
120 181 100       4391 return do {
121 1         4 my $attribute = $_[0]->_unescape($1);
122 1         4 my $value = $_[0]->_unescape($2);
123             sub {
124 3   100 3   25 ! (exists $_[0]->{attrs}{$attribute}
125             && $_[0]->{attrs}{$attribute} eq $value);
126             }
127 1         9 };
128              
129             # '[attr]' - match attribute being present:
130             /\G\[$sel_re\]/gc and
131 180 100       1891 return do {
132 2         7 my $attribute = $_[0]->_unescape($1);
133             sub {
134 5     5   23 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       1355 /\G\[[^\]]*/gc and $_[0]->_blam('Unmatched [');
142             }
143             }
144              
145             sub parse_selector {
146 176     176 0 554 my $self = $_[0];
147 176         309 my $sel = $_[1]; # my pos() only please
148 176 50       459 die "No selector provided" unless $sel;
149 176         414 local *_;
150 176         332 for ($sel) {
151 176         201 my @sub;
152 176         261 PARSE: { do {
  176         227  
153              
154 178         186 my @this_chain;
155              
156             # slurp selectors until we find something else:
157 178         410 while( my $sel = $self->_raw_parse_simple_selector($_) ){
158 202         649 push @this_chain, $sel;
159             }
160              
161 177 100       624 if( @this_chain == 1 )
162             {
163 152         266 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   73 my $r;
170 66         107 for my $inner ( @this_chain ){
171 121 100       235 if( ! ($r = $inner->( @_ )) ){
172 39         294 return $r;
173             }
174             }
175 27         136 return $r;
176             }
177 25         109 }
178              
179             # now we're at the end or a delimiter:
180 177 100       927 last PARSE if( pos == length );
181 4 100       28 /\G\s*,\s*/gc or do {
182 2         8 /\G(.*)/;
183 2         6 $self->_blam( "Selectors not comma separated." );
184             }
185              
186             } until (pos == length) };
187 173 100       1596 return $sub[0] if (@sub == 1);
188             return sub {
189 17     17   130 foreach my $inner (@sub) {
190 32 100       59 if (my $r = $inner->(@_)) { return $r }
  4         20  
191             }
192 2         25 };
193             }
194             }
195              
196             sub _unescape {
197 215     215   447 my ($self, $escaped) = @_;
198 215         1043 (my $unescaped = $escaped) =~ s/\\([\Q$sel_meta_char\E])/$1/g;
199 215         689 return $unescaped;
200             }
201              
202             sub _blam {
203 3     3   6 my ($self, $error) = @_;
204 3   50     14 my $hat = (' ' x (pos||0)).'^';
205 3         43 die "Error parsing dispatch specification: ${error}\n
206             ${_}
207             ${hat} here\n";
208             }
209              
210             1;