File Coverage

blib/lib/Data/ZPath/_Lexer.pm
Criterion Covered Total %
statement 192 211 91.0
branch 76 92 82.6
condition 67 79 84.8
subroutine 15 16 93.7
pod 0 5 0.0
total 350 403 86.8


line stmt bran cond sub pod time code
1 9     9   236196 use strict;
  9         39  
  9         367  
2 9     9   57 use warnings;
  9         25  
  9         700  
3              
4             package Data::ZPath::_Lexer;
5              
6 9     9   68 use Carp qw(croak);
  9         18  
  9         28275  
7              
8             our $VERSION = '0.001000';
9              
10             sub new {
11 437     437 0 502974 my ( $class, $src ) = @_;
12 437         3813 my $self = bless {
13             src => $src,
14             i => 0,
15             toks => [],
16             pos => 0,
17             }, $class;
18              
19 437         1412 $self->{toks} = $self->_tokenize($src);
20 432         1569 return $self;
21             }
22              
23 17342     17342 0 53941 sub peek_kind { $_[0]->{toks}->[$_[0]->{pos}]->{k} }
24 751     751 0 3047 sub peek_kind_n { $_[0]->{toks}->[$_[0]->{pos} + $_[1]]->{k} }
25              
26             sub next_tok {
27 2510     2510 0 3946 my ( $self ) = @_;
28 2510         6097 return $self->{toks}->[$self->{pos}++];
29             }
30              
31             sub expect {
32 768     768 0 1559 my ( $self, $k ) = @_;
33 768         1354 my $t = $self->next_tok;
34 768 100       4110 croak "Expected $k, got $t->{k}" unless $t->{k} eq $k;
35 763         1296 return $t;
36             }
37              
38             sub _is_ws {
39 1463     1463   2244 my ( $c ) = @_;
40 1463   100     7158 return defined $c && $c =~ /\s/;
41             }
42              
43             sub _tokenize {
44 437     437   905 my ( $self, $src ) = @_;
45 437         664 my @t;
46              
47 437         1890 my @c = split //, $src;
48 437         721 my $n = @c;
49 437         697 my $i = 0;
50              
51 437     2106   1877 my $push = sub { push @t, @_ };
  2106         4183  
52             my $prev_sig = sub {
53 1999     1999   3873 my ( $idx ) = @_;
54 1999         5676 for ( my $j = $idx - 1; $j >= 0; $j-- ) {
55 1777 100       4295 next if $c[$j] =~ /\s/;
56 1562         3537 return $c[$j];
57             }
58 437         810 return undef;
59 437         1734 };
60             my $next_sig = sub {
61 1999     1999   2976 my ( $idx ) = @_;
62 1999         7061 for ( my $j = $idx + 1; $j < $n; $j++ ) {
63 1907 100       4708 next if $c[$j] =~ /\s/;
64 1771         4099 return $c[$j];
65             }
66 228         391 return undef;
67 437         1467 };
68             my $ws_on_both = sub {
69 586     586   1246 my ( $left, $right ) = @_;
70 586   100     1702 return ( _is_ws($left) and _is_ws($right) );
71 437         1373 };
72              
73 437         1162 while ( $i < $n ) {
74 2436         3707 my $ch = $c[$i];
75              
76 2436 100       5922 if ( $ch =~ /\s/ ) {
77 325         494 $i++;
78 325         639 next;
79             }
80              
81 2111 100       7631 my $prev = $i > 0 ? $c[$i - 1] : undef;
82 2111 100       4644 my $next = $i + 1 < $n ? $c[$i + 1] : undef;
83 2111 100       4490 my $pair = $i + 1 < $n ? $ch . $c[$i + 1] : '';
84              
85 2111         8236 my %two_char = (
86             '&&' => 'ANDAND',
87             '||' => 'OROR',
88             '==' => 'EQEQ',
89             '!=' => 'NEQ',
90             '>=' => 'GE',
91             '<=' => 'LE',
92             );
93 2111 100       4353 if ( exists $two_char{$pair} ) {
94 90         355 $push->({ k => $two_char{$pair}, v => $pair });
95 90         172 $i += 2;
96 90         260 next;
97             }
98              
99 2021         8040 my %one_char = (
100             '+' => 'PLUS',
101             '-' => 'MINUS',
102             '%' => 'PCT',
103             '^' => 'BXOR',
104             '&' => 'BAND',
105             '|' => 'BOR',
106             '>' => 'GT',
107             '<' => 'LT',
108             );
109 2021 100       3863 if ( exists $one_char{$ch} ) {
110             # Keep arithmetic operators strict to avoid ambiguity with path syntax.
111 22 100 100     120 if ( $ch eq '+' || $ch eq '-' || $ch eq '%' ) {
      100        
112 15 100       51 if ( $ws_on_both->( $prev, $next ) ) {
113 13         54 $push->({ k => $one_char{$ch}, v => $ch });
114 13         21 $i++;
115 13         60 next;
116             }
117 2         2050 croak "Binary operator '$ch' requires whitespace around it";
118             }
119              
120 7         27 $push->({ k => $one_char{$ch}, v => $ch });
121 7         13 $i++;
122 7         27 next;
123             }
124              
125 1999         4330 my $prev_nonws = $prev_sig->($i);
126 1999         3781 my $next_nonws = $next_sig->($i);
127              
128 1999 50 100     4892 if ( $ch eq '/' and $ws_on_both->( $prev, $next )
      66        
      66        
      33        
      33        
129             and defined $prev_nonws and defined $next_nonws
130             and $prev_nonws !~ m{[\[\(,:?/]}
131             and $next_nonws !~ m{[\]\),:?/]} ) {
132 5         23 $push->({ k => 'SLASH', v => '/' });
133 5         7 $i++;
134 5         20 next;
135             }
136              
137 1994 100 75     4394 if ( $ch eq '/' and ( _is_ws($prev) xor _is_ws($next) )
      100        
      100        
      100        
      100        
      66        
138             and defined $prev_nonws and defined $next_nonws
139             and $prev_nonws !~ m{[\[\(,:?/]}
140             and $next_nonws !~ m{[\]\),:?/]} ) {
141 3         1377 croak "Binary operator '/' requires whitespace around it";
142             }
143              
144 1991 100       3744 if ( $ch eq '/' ) { $push->({ k => 'SLASH_PATH', v => '/' }); $i++; next; }
  407         1534  
  407         605  
  407         1770  
145 1584 100       2775 if ( $ch eq '(' ) { $push->({ k => 'LPAREN', v => '(' }); $i++; next; }
  121         504  
  121         215  
  121         532  
146 1463 100       2922 if ( $ch eq ')' ) { $push->({ k => 'RPAREN', v => ')' }); $i++; next; }
  121         424  
  121         180  
  121         423  
147 1342 100       3023 if ( $ch eq '[' ) { $push->({ k => 'LBRACK', v => '[' }); $i++; next; }
  96         352  
  96         139  
  96         417  
148 1246 100       2195 if ( $ch eq ']' ) { $push->({ k => 'RBRACK', v => ']' }); $i++; next; }
  96         981  
  96         194  
  96         381  
149 1150 100       2181 if ( $ch eq ',' ) { $push->({ k => 'COMMA', v => ',' }); $i++; next; }
  46         141  
  46         59  
  46         146  
150              
151 1104 100       2001 if ( $ch eq '.' ) {
152 31 100 100     193 if ( $i + 2 < $n and $c[$i + 1] eq '.' and $c[$i + 2] eq '*' ) {
      100        
153 2         15 $push->({ k => 'DOTDOTSTAR', v => '..*' });
154 2         4 $i += 3;
155 2         14 next;
156             }
157 29 100 100     140 if ( $i + 1 < $n and $c[$i + 1] eq '.' ) {
158 11         53 $push->({ k => 'DOTDOT', v => '..' });
159 11         19 $i += 2;
160 11         57 next;
161             }
162 18         92 $push->({ k => 'DOT', v => '.' });
163 18         32 $i++;
164 18         127 next;
165             }
166              
167 1073 100 100     5226 if ( $ch eq '*' and $ws_on_both->( $prev, $next ) ) {
168 10         46 $push->({ k => 'STAR', v => '*' });
169 10         16 $i++;
170 10         49 next;
171             }
172              
173 1063 100       2086 if ( $ch eq '*' ) {
174 132 100 100     694 if ( $i + 1 < $n and $c[$i + 1] eq '*' ) {
175 85         402 $push->({ k => 'STARSTAR', v => '**' });
176 85         164 $i += 2;
177 85         433 next;
178             }
179 47         200 $push->({ k => 'STAR_PATH', v => '*' });
180 47         77 $i++;
181 47         219 next;
182             }
183              
184 931 100       2106 if ( $ch eq '!' ) { $push->({ k => 'NOT', v => '!' }); $i++; next; }
  7         30  
  7         11  
  7         32  
185 924 100       1843 if ( $ch eq '~' ) { $push->({ k => 'BNOT', v => '~' }); $i++; next; }
  1         8  
  1         2  
  1         5  
186              
187 923 100 100     3666 if ( $ch eq '?' || $ch eq ':' ) {
188 14 50       20 if ( $ws_on_both->( $prev, $next ) ) {
189 14 100       71 $push->({ k => ( $ch eq '?' ) ? 'QMARK' : 'COLON', v => $ch });
190 14         16 $i++;
191 14         35 next;
192             }
193 0         0 croak "Ternary operator '$ch' requires whitespace around it";
194             }
195              
196 909 100 100     4302 if ( $ch eq '"' || $ch eq "'" ) {
197 137         200 my $quote = $ch;
198 137         176 $i++;
199 137         173 my $s = '';
200 137         180 my $esc = 0;
201 137         277 while ( $i < $n ) {
202 1040         1183 my $cc = $c[$i++];
203 1040 50       1477 if ( $esc ) {
204 0         0 $s .= _unescape_char($cc);
205 0         0 $esc = 0;
206 0         0 next;
207             }
208 1040 50       1580 if ( $cc eq '\\' ) { $esc = 1; next; }
  0         0  
  0         0  
209 1040 100       1485 last if $cc eq $quote;
210 903         1257 $s .= $cc;
211             }
212 137         485 $push->({ k => 'STRING', v => $s });
213 137         569 next;
214             }
215              
216 772 100       2119 if ( $ch eq '#' ) {
217 7         16 my $j = $i + 1;
218 7 50 33     63 croak "Invalid index '#'" unless $j < $n and $c[$j] =~ /\d/;
219 7         15 my $num = '';
220 7   100     42 while ( $j < $n and $c[$j] =~ /\d/ ) { $num .= $c[$j++]; }
  7         48  
221 7         35 $push->({ k => 'INDEX', v => 0 + $num });
222 7         13 $i = $j;
223 7         33 next;
224             }
225              
226 765 100       1891 if ( $ch =~ /[0-9]/ ) {
227 173         289 my $j = $i;
228 173         323 my $num = '';
229 173   100     988 while ( $j < $n and $c[$j] =~ /[0-9.]/ ) { $num .= $c[$j++]; }
  231         933  
230 173         864 $push->({ k => 'NUMBER', v => 0 + $num });
231 173         930 $i = $j;
232 173         882 next;
233             }
234              
235 592         1468 my $name = _read_name( \@c, $i );
236 592 50 33     2510 if ( defined $name->{v} and length $name->{v} ) {
237 592         2227 $push->({ k => 'NAME', v => $name->{v} });
238 592         960 $i = $name->{i};
239 592         4283 next;
240             }
241              
242 0         0 croak "Unexpected character '$ch' at position $i";
243             }
244              
245 432         1234 push @t, { k => 'EOF', v => '' };
246 432         5591 return \@t;
247             }
248              
249             sub _unescape_char {
250 0     0   0 my ( $c ) = @_;
251 0 0       0 return "\n" if $c eq 'n';
252 0 0       0 return "\r" if $c eq 'r';
253 0 0       0 return "\t" if $c eq 't';
254 0         0 return $c;
255             }
256              
257             sub _read_name {
258 592     592   1106 my ( $chars, $i ) = @_;
259 592         942 my $n = @$chars;
260              
261 592         3208 my %delim = map { $_ => 1 } split //, "\n\r\t()[]/,=&|!<># ";
  10064         19693  
262 592         2393 my $buf = '';
263 592         876 my $esc = 0;
264              
265 592         810 my $start = $i;
266 592         1389 while ( $i < $n ) {
267 3481         4994 my $c = $chars->[$i];
268              
269 3481 50       5751 if ( $esc ) {
270 0         0 $buf .= $c;
271 0         0 $esc = 0;
272 0         0 $i++;
273 0         0 next;
274             }
275              
276 3481 50       6179 if ( $c eq '\\' ) {
277 0         0 $esc = 1;
278 0         0 $i++;
279 0         0 next;
280             }
281              
282 3481 100       11939 last if $delim{$c};
283 3013 50       5881 last if $c =~ /\s/;
284 3013         4045 $buf .= $c;
285 3013         5322 $i++;
286             }
287              
288 592 50       1210 return { v => '', i => $start } unless length $buf;
289 592         3544 return { v => $buf, i => $i };
290             }
291              
292             1;