File Coverage

blib/lib/Gherkin/TokenMatcher.pm
Criterion Covered Total %
statement 118 153 77.1
branch 32 48 66.6
condition 8 20 40.0
subroutine 27 29 93.1
pod 4 18 22.2
total 189 268 70.5


line stmt bran cond sub pod time code
1             package Gherkin::TokenMatcher;
2             $Gherkin::TokenMatcher::VERSION = '39.0.0';
3 1     1   9 use strict;
  1         2  
  1         51  
4 1     1   7 use warnings;
  1         2  
  1         98  
5              
6 1     1   7 use List::Util qw(any first reduce);
  1         3  
  1         331  
7              
8             our $LANGUAGE_RE = qr/^\s*#\s*language\s*:\s*([a-zA-Z\-_]+)\s*$/o;
9              
10 1         16 use Class::XSAccessor accessors => [
11             qw/dialect _default_dialect_name _indent_to_remove _active_doc_string_separator _keyword_types _sorted_step_keywords /,
12 1     1   9 ];
  1         3  
13              
14 1     1   591 use Cucumber::Messages;
  1         3  
  1         27  
15 1     1   693 use Gherkin::Dialect;
  1         4  
  1         2081  
16              
17              
18             sub new {
19 3     3 1 11 my ( $class, $options ) = @_;
20 3   33     39 $options->{'dialect'} ||= Gherkin::Dialect->new( { dialect => 'en' } );
21 3         56 my $self = bless $options, $class;
22 3         14 $self->_default_dialect_name( $self->dialect_name );
23 3         14 $self->reset();
24 3         67 return $self;
25             }
26              
27             sub _add_keyword_type_mappings {
28 24     24   55 my ($keyword_types, $keywords, $type) = @_;
29              
30 24         39 for my $keyword (@{$keywords}) {
  24         43  
31 60 100       134 if (not exists $keyword_types->{$keyword}) {
32 36         87 $keyword_types->{$keyword} = [];
33             }
34 60         160 push @{$keyword_types->{$keyword}}, $type;
  60         154  
35             }
36             }
37              
38 39     39 1 244 sub dialect_name { $_[0]->dialect->dialect }
39             sub change_dialect {
40 6     6 1 9 my $self = shift;
41 6         29 $self->dialect->change_dialect(@_);
42              
43 6         11 my $keyword_types = {};
44 6         27 _add_keyword_type_mappings($keyword_types, $self->dialect->Given,
45             Cucumber::Messages::Step::KEYWORDTYPE_CONTEXT);
46 6         28 _add_keyword_type_mappings($keyword_types, $self->dialect->When,
47             Cucumber::Messages::Step::KEYWORDTYPE_ACTION);
48 6         25 _add_keyword_type_mappings($keyword_types, $self->dialect->Then,
49             Cucumber::Messages::Step::KEYWORDTYPE_OUTCOME);
50             _add_keyword_type_mappings($keyword_types,
51 6         12 [ @{ $self->dialect->And }, @{ $self->dialect->But } ],
  6         22  
  6         20  
52             Cucumber::Messages::Step::KEYWORDTYPE_CONJUNCTION);
53 6         35 $self->_keyword_types( $keyword_types );
54             $self->_sorted_step_keywords(
55             [ sort {
56 144         255 length $b <=> length $a # longest keyword first (See #400)
57             } map {
58 6         16 @{ $self->dialect->$_ }
  30         46  
  30         85  
59             } qw/Given When Then And But/ ]
60             );
61             }
62              
63             sub reset {
64 6     6 1 11 my $self = shift;
65 6         40 $self->change_dialect( $self->_default_dialect_name );
66 6         25 $self->_indent_to_remove(0);
67 6         23 $self->_active_doc_string_separator(undef);
68              
69             }
70              
71             sub match_FeatureLine {
72 3     3 0 7 my ( $self, $token ) = @_;
73 3         21 $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
74             }
75              
76             sub match_RuleLine {
77 9     9 0 19 my ( $self, $token ) = @_;
78 9         35 $self->_match_title_line( $token,
79             RuleLine => $self->dialect->Rule );
80             }
81              
82             sub match_ScenarioLine {
83 15     15 0 36 my ( $self, $token ) = @_;
84 15 100       62 $self->_match_title_line(
85             $token,
86             ScenarioLine => $self->dialect->Scenario )
87             or $self->_match_title_line(
88             $token,
89             ScenarioLine => $self->dialect->ScenarioOutline );
90             }
91              
92             sub match_BackgroundLine {
93 3     3 0 8 my ( $self, $token ) = @_;
94 3         35 $self->_match_title_line( $token,
95             BackgroundLine => $self->dialect->Background );
96             }
97              
98             sub match_ExamplesLine {
99 6     6 0 14 my ( $self, $token ) = @_;
100 6         36 $self->_match_title_line( $token,
101             ExamplesLine => $self->dialect->Examples );
102             }
103              
104             sub match_Language {
105 3     3 0 9 my ( $self, $token ) = @_;
106 3 50 33     25 if ( $token->line and $token->line->get_line_text =~ $LANGUAGE_RE ) {
107 0         0 my $dialect_name = $1;
108 0         0 $self->_set_token_matched( $token,
109             Language => { text => $dialect_name } );
110 0         0 local $@;
111 0         0 eval { $self->change_dialect( $dialect_name, $token->location ) };
  0         0  
112 0         0 return (1, $@);
113             } else {
114 3         26 return;
115             }
116             }
117              
118             sub match_TagLine {
119 39     39 0 73 my ( $self, $token ) = @_;
120 39 50 33     161 return unless $token->line and $token->line->startswith('@');
121              
122 0         0 my ($tags, $err) = $token->line->tags;
123 0         0 $self->_set_token_matched( $token,
124             TagLine => { items => $tags } );
125 0         0 return (1, $err);
126             }
127              
128             sub _match_title_line {
129 45     45   98 my ( $self, $token, $token_type, $keywords ) = @_;
130 45 50       134 return unless $token->line;
131              
132 45         90 for my $keyword (@{$keywords}) {
  45         109  
133 75 100       200 if ( $token->line->startswith_title_keyword($keyword) ) {
134 12         50 my $title =
135             $token->line->get_rest_trimmed( length( $keyword . ': ' ) );
136 12         86 $self->_set_token_matched( $token, $token_type,
137             { text => $title, keyword => $keyword } );
138 12         90 return 1;
139             }
140             }
141              
142 33         162 return;
143             }
144              
145             sub _set_token_matched {
146 36     36   82 my ( $self, $token, $matched_type, $options ) = @_;
147 36   50     215 $options->{'items'} ||= [];
148 36         151 $token->matched_type($matched_type);
149              
150 36 100       92 if ( defined $options->{'text'} ) {
151 21         53 chomp( $options->{'text'} );
152 21         98 $token->matched_text( $options->{'text'} );
153             }
154              
155             $token->matched_keyword( $options->{'keyword'} )
156 36 100       118 if defined $options->{'keyword'};
157             $token->matched_keyword_type( $options->{'keyword_type'} )
158 36 100       107 if defined $options->{'keyword_type'};
159              
160 36 100       81 if ( defined $options->{'indent'} ) {
161 12         31 $token->matched_indent( $options->{'indent'} );
162             } else {
163 24 100       114 $token->matched_indent( $token->line ? $token->line->indent : 0 );
164             }
165              
166             $token->matched_items( $options->{'items'} )
167 36 50       116 if defined $options->{'items'};
168              
169 36         104 $token->location->{'column'} = $token->matched_indent + 1;
170 36         100 $token->matched_gherkin_dialect( $self->dialect_name );
171             }
172              
173             sub match_EOF {
174 36     36 0 91 my ( $self, $token ) = @_;
175 36 100       126 return unless $token->is_eof;
176 3         13 $self->_set_token_matched( $token, 'EOF' );
177 3         12 return 1;
178             }
179              
180             sub match_Empty {
181 24     24 0 51 my ( $self, $token ) = @_;
182 24 100 66     112 return unless $token->line and $token->line->is_empty;
183 12         63 $self->_set_token_matched( $token, Empty => { indent => 0 } );
184 12         56 return 1;
185             }
186              
187             sub match_Comment {
188 21     21 0 47 my ( $self, $token ) = @_;
189 21 50 33     83 return unless $token->line and $token->line->startswith('#');
190              
191 0         0 my $comment_text = $token->line->line_text;
192 0         0 $comment_text =~ s/\r\n$//; # Why?
193              
194 0         0 $self->_set_token_matched( $token,
195             Comment => { text => $comment_text, indent => 0 } );
196 0         0 return 1;
197             }
198              
199             sub match_Other {
200 0     0 0 0 my ( $self, $token ) = @_;
201 0 0       0 return unless $token->line;
202              
203             # take the entire line, except removing DocString indents
204 0         0 my $text = $token->line->get_line_text( $self->_indent_to_remove );
205 0         0 $self->_set_token_matched( $token,
206             Other => { indent => 0, text => $self->_unescaped_docstring($text) } );
207 0         0 return 1;
208             }
209              
210             sub _unescaped_docstring {
211 0     0   0 my ( $self, $text ) = @_;
212 0 0       0 if ( $self->_active_doc_string_separator ) {
213 0         0 $text =~ s!\\"\\"\\"!"""!;
214 0         0 $text =~ s!\\`\\`\\`!```!;
215 0         0 return $text;
216             } else {
217 0         0 return $text;
218             }
219             }
220              
221             sub match_StepLine {
222 24     24 0 52 my ( $self, $token ) = @_;
223 24         42 my @keywords = @{ $self->_sorted_step_keywords };
  24         114  
224 24         56 my $line = $token->line;
225              
226 24         53 for my $keyword (@keywords) {
227 159 100       379 if ( $line->startswith($keyword) ) {
228 9         27 my $title = $line->get_rest_trimmed( length($keyword) );
229             my $keyword_type =
230 9         66 (scalar @{$self->_keyword_types->{$keyword}} > 1)
231             ? Cucumber::Messages::Step::KEYWORDTYPE_UNKNOWN
232 9 50       18 : $self->_keyword_types->{$keyword}->[0];
233 9         67 $self->_set_token_matched(
234             $token,
235             StepLine => {
236             text => $title,
237             keyword => $keyword,
238             keyword_type => $keyword_type,
239             } );
240 9         70 return 1;
241             }
242             }
243 15         58 return;
244             }
245              
246             sub match_DocStringSeparator {
247 15     15 0 31 my ( $self, $token ) = @_;
248 15 50       39 if ($token->is_eof) {
249 0         0 return 0;
250             }
251 15 50       53 if ( !$self->_active_doc_string_separator ) {
252 15   33     42 return $self->_match_DocStringSeparator( $token, '"""', 1 )
253             || $self->_match_DocStringSeparator( $token, '```', 1 );
254             } else {
255 0         0 return $self->_match_DocStringSeparator( $token,
256             $self->_active_doc_string_separator, 0 );
257             }
258             }
259              
260             sub _match_DocStringSeparator {
261 30     30   69 my ( $self, $token, $separator, $is_open ) = @_;
262 30 50       82 return unless $token->line->startswith($separator);
263              
264 0         0 my $content_type;
265 0 0       0 if ($is_open) {
266 0         0 $content_type = $token->line->get_rest_trimmed( length($separator) );
267 0         0 $self->_active_doc_string_separator($separator);
268 0         0 $self->_indent_to_remove( $token->line->indent );
269             } else {
270 0         0 $self->_active_doc_string_separator(undef);
271 0         0 $self->_indent_to_remove(0);
272             }
273              
274 0         0 $self->_set_token_matched( $token,
275             DocStringSeparator => { text => $content_type, keyword => $separator } );
276             }
277              
278             sub match_TableRow {
279 15     15 0 31 my ( $self, $token ) = @_;
280 15 50       52 return unless $token->line->startswith('|');
281              
282 0           $self->_set_token_matched( $token,
283             TableRow => { items => $token->line->table_cells } );
284             }
285              
286             1;
287              
288              
289             __END__