File Coverage

blib/lib/Gherkin/TokenMatcher.pm
Criterion Covered Total %
statement 112 146 76.7
branch 31 46 67.3
condition 8 20 40.0
subroutine 27 29 93.1
pod 4 18 22.2
total 182 259 70.2


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