File Coverage

blib/lib/Test/Pcuke/Gherkin/Lexer.pm
Criterion Covered Total %
statement 86 86 100.0
branch 13 14 92.8
condition n/a
subroutine 24 24 100.0
pod 1 2 50.0
total 124 126 98.4


line stmt bran cond sub pod time code
1             package Test::Pcuke::Gherkin::Lexer;
2              
3 2     2   108328 use warnings;
  2         4  
  2         61  
4 2     2   9 use strict;
  2         5  
  2         62  
5 2     2   1911 use utf8;
  2         21  
  2         17  
6              
7 2     2   1322 use Test::Pcuke::Gherkin::I18n;
  2         6  
  2         4376  
8             =head1 NAME
9              
10             Test::Pcuke::Gherkin::Lexer - roll your own cucumber
11              
12             =head1 VERSION
13              
14             Version 0.000001
15              
16             =cut
17              
18             our $VERSION = '0.000001';
19              
20              
21             =head1 SYNOPSIS
22              
23             TODO SYNOPSIS
24              
25             use Test::Pcuke::Gherkin::Lexer;
26             ...
27             my $lexens = Test::Pcuke::Gherkin::Lexer->scan( $input );
28             ...
29              
30             =cut
31              
32             # TODO i18n!!!
33             my $REGEXP = {
34             empty => [ qr{^\s*$}ix ],
35             pragma => [ qr{^\s*#(?:\s*(\w+\s*:\s*\w+))+}i ],
36             comment => [ qr{^\s*#}i ],
37             tag => [ qr{@\w+}ix ],
38             feature => [ qr{^\s*(feature: .*?)\s*$}ix ],
39             any => [ qr{^\s*(.*?)\s*$}ix ],
40             background => [ qr{^\s*(background:.*?)\s*$}ix ],
41             scenario => [ qr{^\s*(scenario:.*?)\s*$}ix ],
42             outline => [ qr{^\s*(scenario outline:.*?)\s*$}i, qr{^\s*(scenario template:.*?)\s*$}ix ],
43             given => [ qr{^\s*(\*)\s*(.*?)\s*$}i, qr{^\s*(given)\s*(.*?)\s*$}ix ],
44             when => [ qr{^\s*(\*)\s*(.*?)\s*$}i, qr{^\s*(when)\s*(.*?)\s*$}ix ],
45             then => [ qr{^\s*(\*)\s*(.*?)\s*$}i, qr{^\s*(then)\s*(.*?)\s*$}ix ],
46             and => [ qr{^\s*(\*)\s*(.*?)\s*$}i, qr{^\s*(and)\s*(.*?)\s*$}ix ],
47             but => [ qr{^\s*(\*)\s*(.*?)\s*$}i, qr{^\s*(but)\s*(.*?)\s*$}ix ],
48             examples => [ qr{^\s*(examples:.*?)\s*$}i, qr{^\s*(scenarios:.*?)\s*$}ix ],
49             trow => [ qr{^ \s* \| (.*?) \| \s* $}ix ],
50             text_quote => [ qr{^ \s* (""") \s* $}ix ],
51             };
52              
53             my @LEVELS = qw{root};
54              
55             my $TOKENIZERS = {
56             root => [
57             {
58             regexps => ['empty'],
59             mktoken => _skip()
60             },
61            
62             {
63             regexps => ['pragma'],
64             mktoken => _mktoken_split('PRAG', qr{\s*:\s*}),
65             },
66            
67             {
68             regexps => ['comment'],
69             mktoken => _skip(),
70             },
71            
72             {
73             regexps => ['tag'],
74             mktoken => _mktoken_perchunk('TAG'),
75             },
76            
77             {
78             regexps => ['feature'],
79             mktoken => _mktoken_join('FEAT'),
80             level => 'feature',
81             },
82            
83             {
84             regexps => ['any'],
85             mktoken => _mktoken_join('ERR'),
86             },
87             ],
88            
89             feature => [
90             {
91             regexps => ['empty', 'comment'],
92             mktoken => _skip()
93             },
94            
95             {
96             regexps => ['tag'],
97             mktoken => _mktoken_perchunk('TAG'),
98             },
99            
100             {
101             regexps => ['background'],
102             mktoken => _mktoken_join('BGR'),
103             level => 'steps',
104             },
105            
106             {
107             regexps => ['scenario'],
108             mktoken => _mktoken_join('SCEN'),
109             level => 'steps',
110             },
111            
112             {
113             regexps => ['outline'],
114             mktoken => _mktoken_join('OUTL'),
115             level => ['outline', 'steps'],
116             },
117            
118             {
119             regexps => ['any'],
120             mktoken => _mktoken_join('NARR'),
121             },
122             ],
123            
124             steps => [
125             {
126             regexps => ['empty', 'comment'],
127             mktoken => _skip()
128             },
129            
130             {
131             regexps => [qw{given when then and but}],
132             mktoken => sub {
133             my ($self, $line, $type, $title) = @_;
134             ['STEP', uc $type, $title ]
135             },
136             },
137            
138             {
139             regexps => ['text_quote'],
140             mktoken => _skip(),
141             level => 'text',
142             },
143            
144             {
145             regexps => ['trow'],
146             mktoken => sub {
147             my ($self, $line, @chunks) = @_;
148             my @cols = map { s/^\s*|\s*$//g; $_ } split /\s*\|\s*/, $chunks[0];
149             ['TROW', @cols];
150             }
151             },
152            
153             {
154             regexps => ['any'],
155             mktoken => _mktoken_uplevel(),
156             },
157             ],
158            
159             text => [
160             {
161             regexps => ['text_quote'],
162             mktoken => _skip(), # maybe _mktoken_skip_uplevel() ?
163             level => 'steps', # 'cause it is not very beautiful
164             },
165             {
166             regexps => ['any'],
167             mktoken => _mktoken_join('TEXT'),
168             },
169             ],
170            
171             outline => [
172             {
173             regexps => ['empty', 'comment'],
174             mktoken => _skip()
175             },
176            
177             {
178             regexps => ['examples'],
179             mktoken => _mktoken_join('SCENS'),
180             level => 'scenarios',
181             },
182            
183             {
184             regexps => ['any'],
185             mktoken => _mktoken_uplevel(),
186             },
187             ],
188            
189             scenarios => [
190             {
191             regexps => ['empty', 'comment'],
192             mktoken => _skip()
193             },
194            
195             {
196             regexps => ['trow'],
197             mktoken => _mktoken_split('TROW', qr{\s*\|\s*}),
198             },
199            
200             {
201             regexps => ['any'],
202             mktoken => _mktoken_uplevel(),
203             },
204             ],
205             };
206              
207             =head1 METHODS
208              
209             =head2 scan $input
210              
211             =cut
212              
213             sub scan {
214 17     17 1 104 my ($self, $input) = @_;
215 17         18 my $result;
216 17         97 my @lines = split /[\r\n]+/, $input;
217            
218 17         35 foreach my $line ( @lines ) {
219 64         87 push @$result, @{ $self->_scan_line($line) };
  64         144  
220             }
221            
222 17         91 return $result;
223             }
224              
225             sub _scan_line {
226 66     66   108 my ($self, $line) = @_;
227 66         63 my @chunks;
228             my @result;
229            
230 66         78 foreach my $tokenizer ( @{$TOKENIZERS->{ $self->_get_level } } ) {
  66         133  
231 197         443 push @result, $self->_tokenize($tokenizer, $line);
232 197 100       464 last if @result;
233             }
234            
235 66         113 @result = grep { $_->[0] ne 'SKIP' } @result;
  69         212  
236            
237 66         108 $self->_process_pragmas( grep { $_->[0] eq 'PRAG' } @result);
  57         182  
238            
239 66         218 return \@result;
240             }
241              
242             sub _tokenize {
243 197     197   306 my ($self, $conf, $line) = @_;
244 197         220 my @tokens;
245             my @chunks;
246            
247 197         207 foreach my $re_name ( @{$conf->{regexps}} ) {
  197         418  
248 274         321 foreach my $re ( @{ $REGEXP->{$re_name} } ) {
  274         504  
249 343         2444 @chunks = ($line =~ /$re/ig );
250 343 100       885 if ( @chunks ) {
251 66         226 push @tokens, $conf->{mktoken}->($self, $line, @chunks);
252            
253 66 100       163 if ( $conf->{level} ) {
254 15 100       45 $conf->{level} = [$conf->{level}] unless ref $conf->{level};
255 15         19 $self->_append_levels( @{ $conf->{level} } )
  15         43  
256             }
257 66         87 last;
258             }
259             }
260 274 100       1048 last if @tokens;
261             }
262 197         461 return @tokens;
263             }
264              
265             sub _process_pragmas {
266 66     66   121 my $self = shift;
267 66         154 foreach (@_) {
268 5         9 my ($prag, $name, $value) = @{$_};
  5         11  
269 5 100       27 $self->_set_language($value) if $name eq 'language';
270             }
271             }
272              
273             sub _set_language {
274 3     3   7 my ($self, $language) = @_;
275 3         41 $REGEXP = Test::Pcuke::Gherkin::I18n->patterns( $language );
276             }
277              
278 12     12   29 sub _skip { return sub {['SKIP']} }
  16     16   107  
279              
280             sub _mktoken_join {
281 16     16   26 my ($label) = @_;
282             return sub {
283 22     22   60 my ($self, $line, @chunks) = @_;
284 22         83 [$label, join(' ', @chunks) ]
285             }
286 16         86 }
287              
288             sub _mktoken_uplevel {
289             return sub {
290 2     2   7 my ($self, $line, @chunks) = @_;
291 2         8 $self->_level_up;
292 2         3 return @ { $self->_scan_line($line) };
  2         10  
293             }
294 6     6   48 }
295              
296             sub _mktoken_perchunk {
297 4     4   7 my ($label) = @_;
298             return sub {
299 6     6   12 my ($self, $line, @chunks) = @_;
300 6         11 map {[$label,$_]} @chunks;
  9         27  
301             }
302 4         31 }
303              
304             sub _mktoken_split {
305 4     4   9 my ($label, $re) = @_;
306             return sub {
307 10     10   30 my ($self, $line, @chunks) = @_;
308 10         15 map {[$label, map { s/^\s*|\s*$//g; $_ } split /$re/i, $_]} @chunks;
  10         60  
  25         128  
  25         82  
309             }
310 4         33 }
311              
312             sub reset {
313 17     17 0 234 my ($self) = @_;
314 17         43 $self->_set_levels( 'root' );
315             }
316              
317             sub _set_levels {
318 17     17   24 my $self = shift;
319 17         50 @LEVELS = @_;
320             }
321              
322             sub _append_levels {
323 20     20   62 my ($self, @levels) = @_;
324 20         44 push @LEVELS, @levels;
325             }
326              
327             sub _level_up {
328 2     2   5 my ($self) = @_;
329 2 50       9 if ( @LEVELS > 1 ) {
330 2         4 pop @LEVELS;
331             }
332             else {
333             # confess can't go nowhere
334             }
335             }
336              
337 66     66   172 sub _get_level { $LEVELS[-1] }
338              
339             =head1 AUTHOR
340              
341             Andrei V. Toutoukine, C<< >>
342              
343             =head1 BUGS
344              
345             Please report any bugs or feature requests to C, or through
346             the web interface at L. I will be notified, and then you'll
347             automatically be notified of progress on your bug as I make changes.
348              
349              
350              
351              
352             =head1 SUPPORT
353              
354             You can find documentation for this module with the perldoc command.
355              
356             perldoc Test::Pcuke::Gherkin::Lexer
357              
358              
359             You can also look for information at:
360              
361             =over 4
362              
363             =item * RT: CPAN's request tracker
364              
365             L
366              
367             =item * AnnoCPAN: Annotated CPAN documentation
368              
369             L
370              
371             =item * CPAN Ratings
372              
373             L
374              
375             =item * Search CPAN
376              
377             L
378              
379             =back
380              
381              
382             =head1 ACKNOWLEDGEMENTS
383              
384              
385             =head1 LICENSE AND COPYRIGHT
386              
387             Copyright 2011 Andrei V. Toutoukine.
388              
389             This program is released under the following license: artistic
390              
391              
392             =cut
393              
394             1; # End of Test::Pcuke::Gherkin::Lexer