File Coverage

blib/lib/Gherkin/TokenMatcher.pm
Criterion Covered Total %
statement 111 141 78.7
branch 30 42 71.4
condition 3 8 37.5
subroutine 27 29 93.1
pod 4 18 22.2
total 175 238 73.5


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