Security update for permissions_by_term
[yaffs-website] / vendor / behat / gherkin / src / Behat / Gherkin / Lexer.php
1 <?php
2
3 /*
4  * This file is part of the Behat Gherkin.
5  * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 namespace Behat\Gherkin;
12
13 use Behat\Gherkin\Exception\LexerException;
14 use Behat\Gherkin\Keywords\KeywordsInterface;
15
16 /**
17  * Gherkin lexer.
18  *
19  * @author Konstantin Kudryashov <ever.zet@gmail.com>
20  */
21 class Lexer
22 {
23     private $language;
24     private $lines;
25     private $linesCount;
26     private $line;
27     private $trimmedLine;
28     private $lineNumber;
29     private $eos;
30     private $keywords;
31     private $keywordsCache = array();
32     private $stepKeywordTypesCache = array();
33     private $deferredObjects = array();
34     private $deferredObjectsCount = 0;
35     private $stashedToken;
36     private $inPyString = false;
37     private $pyStringSwallow = 0;
38     private $featureStarted = false;
39     private $allowMultilineArguments = false;
40     private $allowSteps = false;
41
42     /**
43      * Initializes lexer.
44      *
45      * @param KeywordsInterface $keywords Keywords holder
46      */
47     public function __construct(KeywordsInterface $keywords)
48     {
49         $this->keywords = $keywords;
50     }
51
52     /**
53      * Sets lexer input.
54      *
55      * @param string $input    Input string
56      * @param string $language Language name
57      *
58      * @throws Exception\LexerException
59      */
60     public function analyse($input, $language = 'en')
61     {
62         // try to detect unsupported encoding
63         if ('UTF-8' !== mb_detect_encoding($input, 'UTF-8', true)) {
64             throw new LexerException('Feature file is not in UTF8 encoding');
65         }
66
67         $input = strtr($input, array("\r\n" => "\n", "\r" => "\n"));
68
69         $this->lines = explode("\n", $input);
70         $this->linesCount = count($this->lines);
71         $this->line = $this->lines[0];
72         $this->lineNumber = 1;
73         $this->trimmedLine = null;
74         $this->eos = false;
75
76         $this->deferredObjects = array();
77         $this->deferredObjectsCount = 0;
78         $this->stashedToken = null;
79         $this->inPyString = false;
80         $this->pyStringSwallow = 0;
81
82         $this->featureStarted = false;
83         $this->allowMultilineArguments = false;
84         $this->allowSteps = false;
85
86         $this->keywords->setLanguage($this->language = $language);
87         $this->keywordsCache = array();
88         $this->stepKeywordTypesCache = array();
89     }
90
91     /**
92      * Returns current lexer language.
93      *
94      * @return string
95      */
96     public function getLanguage()
97     {
98         return $this->language;
99     }
100
101     /**
102      * Returns next token or previously stashed one.
103      *
104      * @return array
105      */
106     public function getAdvancedToken()
107     {
108         return $this->getStashedToken() ?: $this->getNextToken();
109     }
110
111     /**
112      * Defers token.
113      *
114      * @param array $token Token to defer
115      */
116     public function deferToken(array $token)
117     {
118         $token['deferred'] = true;
119         $this->deferredObjects[] = $token;
120         ++$this->deferredObjectsCount;
121     }
122
123     /**
124      * Predicts for number of tokens.
125      *
126      * @return array
127      */
128     public function predictToken()
129     {
130         if (null === $this->stashedToken) {
131             $this->stashedToken = $this->getNextToken();
132         }
133
134         return $this->stashedToken;
135     }
136
137     /**
138      * Constructs token with specified parameters.
139      *
140      * @param string $type  Token type
141      * @param string $value Token value
142      *
143      * @return array
144      */
145     public function takeToken($type, $value = null)
146     {
147         return array(
148             'type'     => $type,
149             'line'     => $this->lineNumber,
150             'value'    => $value ?: null,
151             'deferred' => false
152         );
153     }
154
155     /**
156      * Consumes line from input & increments line counter.
157      */
158     protected function consumeLine()
159     {
160         ++$this->lineNumber;
161
162         if (($this->lineNumber - 1) === $this->linesCount) {
163             $this->eos = true;
164
165             return;
166         }
167
168         $this->line = $this->lines[$this->lineNumber - 1];
169         $this->trimmedLine = null;
170     }
171
172     /**
173      * Returns trimmed version of line.
174      *
175      * @return string
176      */
177     protected function getTrimmedLine()
178     {
179         return null !== $this->trimmedLine ? $this->trimmedLine : $this->trimmedLine = trim($this->line);
180     }
181
182     /**
183      * Returns stashed token or null if hasn't.
184      *
185      * @return array|null
186      */
187     protected function getStashedToken()
188     {
189         $stashedToken = $this->stashedToken;
190         $this->stashedToken = null;
191
192         return $stashedToken;
193     }
194
195     /**
196      * Returns deferred token or null if hasn't.
197      *
198      * @return array|null
199      */
200     protected function getDeferredToken()
201     {
202         if (!$this->deferredObjectsCount) {
203             return null;
204         }
205
206         --$this->deferredObjectsCount;
207
208         return array_shift($this->deferredObjects);
209     }
210
211     /**
212      * Returns next token from input.
213      *
214      * @return array
215      */
216     protected function getNextToken()
217     {
218         return $this->getDeferredToken()
219             ?: $this->scanEOS()
220             ?: $this->scanLanguage()
221             ?: $this->scanComment()
222             ?: $this->scanPyStringOp()
223             ?: $this->scanPyStringContent()
224             ?: $this->scanStep()
225             ?: $this->scanScenario()
226             ?: $this->scanBackground()
227             ?: $this->scanOutline()
228             ?: $this->scanExamples()
229             ?: $this->scanFeature()
230             ?: $this->scanTags()
231             ?: $this->scanTableRow()
232             ?: $this->scanNewline()
233             ?: $this->scanText();
234     }
235
236     /**
237      * Scans for token with specified regex.
238      *
239      * @param string $regex Regular expression
240      * @param string $type  Expected token type
241      *
242      * @return null|array
243      */
244     protected function scanInput($regex, $type)
245     {
246         if (!preg_match($regex, $this->line, $matches)) {
247             return null;
248         }
249
250         $token = $this->takeToken($type, $matches[1]);
251         $this->consumeLine();
252
253         return $token;
254     }
255
256     /**
257      * Scans for token with specified keywords.
258      *
259      * @param string $keywords Keywords (splitted with |)
260      * @param string $type     Expected token type
261      *
262      * @return null|array
263      */
264     protected function scanInputForKeywords($keywords, $type)
265     {
266         if (!preg_match('/^(\s*)(' . $keywords . '):\s*(.*)/u', $this->line, $matches)) {
267             return null;
268         }
269
270         $token = $this->takeToken($type, $matches[3]);
271         $token['keyword'] = $matches[2];
272         $token['indent'] = mb_strlen($matches[1], 'utf8');
273
274         $this->consumeLine();
275
276         // turn off language searching
277         if ('Feature' === $type) {
278             $this->featureStarted = true;
279         }
280
281         // turn off PyString and Table searching
282         if ('Feature' === $type || 'Scenario' === $type || 'Outline' === $type) {
283             $this->allowMultilineArguments = false;
284         } elseif ('Examples' === $type) {
285             $this->allowMultilineArguments = true;
286         }
287
288         // turn on steps searching
289         if ('Scenario' === $type || 'Background' === $type || 'Outline' === $type) {
290             $this->allowSteps = true;
291         }
292
293         return $token;
294     }
295
296     /**
297      * Scans EOS from input & returns it if found.
298      *
299      * @return null|array
300      */
301     protected function scanEOS()
302     {
303         if (!$this->eos) {
304             return null;
305         }
306
307         return $this->takeToken('EOS');
308     }
309
310     /**
311      * Returns keywords for provided type.
312      *
313      * @param string $type Keyword type
314      *
315      * @return string
316      */
317     protected function getKeywords($type)
318     {
319         if (!isset($this->keywordsCache[$type])) {
320             $getter = 'get' . $type . 'Keywords';
321             $keywords = $this->keywords->$getter();
322
323             if ('Step' === $type) {
324                 $padded = array();
325                 foreach (explode('|', $keywords) as $keyword) {
326                     $padded[] = false !== mb_strpos($keyword, '<', 0, 'utf8')
327                         ? preg_quote(mb_substr($keyword, 0, -1, 'utf8'), '/') . '\s*'
328                         : preg_quote($keyword, '/') . '\s+';
329                 }
330
331                 $keywords = implode('|', $padded);
332             }
333
334             $this->keywordsCache[$type] = $keywords;
335         }
336
337         return $this->keywordsCache[$type];
338     }
339
340     /**
341      * Scans Feature from input & returns it if found.
342      *
343      * @return null|array
344      */
345     protected function scanFeature()
346     {
347         return $this->scanInputForKeywords($this->getKeywords('Feature'), 'Feature');
348     }
349
350     /**
351      * Scans Background from input & returns it if found.
352      *
353      * @return null|array
354      */
355     protected function scanBackground()
356     {
357         return $this->scanInputForKeywords($this->getKeywords('Background'), 'Background');
358     }
359
360     /**
361      * Scans Scenario from input & returns it if found.
362      *
363      * @return null|array
364      */
365     protected function scanScenario()
366     {
367         return $this->scanInputForKeywords($this->getKeywords('Scenario'), 'Scenario');
368     }
369
370     /**
371      * Scans Scenario Outline from input & returns it if found.
372      *
373      * @return null|array
374      */
375     protected function scanOutline()
376     {
377         return $this->scanInputForKeywords($this->getKeywords('Outline'), 'Outline');
378     }
379
380     /**
381      * Scans Scenario Outline Examples from input & returns it if found.
382      *
383      * @return null|array
384      */
385     protected function scanExamples()
386     {
387         return $this->scanInputForKeywords($this->getKeywords('Examples'), 'Examples');
388     }
389
390     /**
391      * Scans Step from input & returns it if found.
392      *
393      * @return null|array
394      */
395     protected function scanStep()
396     {
397         if (!$this->allowSteps) {
398             return null;
399         }
400
401         $keywords = $this->getKeywords('Step');
402         if (!preg_match('/^\s*(' . $keywords . ')([^\s].+)/u', $this->line, $matches)) {
403             return null;
404         }
405
406         $keyword = trim($matches[1]);
407         $token = $this->takeToken('Step', $keyword);
408         $token['keyword_type'] = $this->getStepKeywordType($keyword);
409         $token['text'] = $matches[2];
410
411         $this->consumeLine();
412         $this->allowMultilineArguments = true;
413
414         return $token;
415     }
416
417     /**
418      * Scans PyString from input & returns it if found.
419      *
420      * @return null|array
421      */
422     protected function scanPyStringOp()
423     {
424         if (!$this->allowMultilineArguments) {
425             return null;
426         }
427
428         if (false === ($pos = mb_strpos($this->line, '"""', 0, 'utf8'))) {
429             return null;
430         }
431
432         $this->inPyString = !$this->inPyString;
433         $token = $this->takeToken('PyStringOp');
434         $this->pyStringSwallow = $pos;
435
436         $this->consumeLine();
437
438         return $token;
439     }
440
441     /**
442      * Scans PyString content.
443      *
444      * @return null|array
445      */
446     protected function scanPyStringContent()
447     {
448         if (!$this->inPyString) {
449             return null;
450         }
451
452         $token = $this->scanText();
453         // swallow trailing spaces
454         $token['value'] = preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value']);
455
456         return $token;
457     }
458
459     /**
460      * Scans Table Row from input & returns it if found.
461      *
462      * @return null|array
463      */
464     protected function scanTableRow()
465     {
466         if (!$this->allowMultilineArguments) {
467             return null;
468         }
469
470         $line = $this->getTrimmedLine();
471         if (!isset($line[0]) || '|' !== $line[0] || '|' !== substr($line, -1)) {
472             return null;
473         }
474
475         $token = $this->takeToken('TableRow');
476         $line = mb_substr($line, 1, mb_strlen($line, 'utf8') - 2, 'utf8');
477         $columns = array_map(function ($column) {
478             return trim(str_replace('\\|', '|', $column));
479         }, preg_split('/(?<!\\\)\|/u', $line));
480         $token['columns'] = $columns;
481
482         $this->consumeLine();
483
484         return $token;
485     }
486
487     /**
488      * Scans Tags from input & returns it if found.
489      *
490      * @return null|array
491      */
492     protected function scanTags()
493     {
494         $line = $this->getTrimmedLine();
495         if (!isset($line[0]) || '@' !== $line[0]) {
496             return null;
497         }
498
499         $token = $this->takeToken('Tag');
500         $tags = explode('@', mb_substr($line, 1, mb_strlen($line, 'utf8') - 1, 'utf8'));
501         $tags = array_map('trim', $tags);
502         $token['tags'] = $tags;
503
504         $this->consumeLine();
505
506         return $token;
507     }
508
509     /**
510      * Scans Language specifier from input & returns it if found.
511      *
512      * @return null|array
513      */
514     protected function scanLanguage()
515     {
516         if ($this->featureStarted) {
517             return null;
518         }
519
520         if ($this->inPyString) {
521             return null;
522         }
523
524         if (0 !== mb_strpos(ltrim($this->line), '#', 0, 'utf8')) {
525             return null;
526         }
527
528         return $this->scanInput('/^\s*\#\s*language:\s*([\w_\-]+)\s*$/', 'Language');
529     }
530
531     /**
532      * Scans Comment from input & returns it if found.
533      *
534      * @return null|array
535      */
536     protected function scanComment()
537     {
538         if ($this->inPyString) {
539             return null;
540         }
541
542         $line = $this->getTrimmedLine();
543         if (0 !== mb_strpos($line, '#', 0, 'utf8')) {
544             return null;
545         }
546
547         $token = $this->takeToken('Comment', $line);
548         $this->consumeLine();
549
550         return $token;
551     }
552
553     /**
554      * Scans Newline from input & returns it if found.
555      *
556      * @return null|array
557      */
558     protected function scanNewline()
559     {
560         if ('' !== $this->getTrimmedLine()) {
561             return null;
562         }
563
564         $token = $this->takeToken('Newline', mb_strlen($this->line, 'utf8'));
565         $this->consumeLine();
566
567         return $token;
568     }
569
570     /**
571      * Scans text from input & returns it if found.
572      *
573      * @return null|array
574      */
575     protected function scanText()
576     {
577         $token = $this->takeToken('Text', $this->line);
578         $this->consumeLine();
579
580         return $token;
581     }
582
583     /**
584      * Returns step type keyword (Given, When, Then, etc.).
585      *
586      * @param string $native Step keyword in provided language
587      * @return string
588      */
589     private function getStepKeywordType($native)
590     {
591         // Consider "*" as a AND keyword so that it is normalized to the previous step type
592         if ('*' === $native) {
593             return 'And';
594         }
595
596         if (empty($this->stepKeywordTypesCache)) {
597             $this->stepKeywordTypesCache = array(
598                 'Given' => explode('|', $this->keywords->getGivenKeywords()),
599                 'When' => explode('|', $this->keywords->getWhenKeywords()),
600                 'Then' => explode('|', $this->keywords->getThenKeywords()),
601                 'And' => explode('|', $this->keywords->getAndKeywords()),
602                 'But' => explode('|', $this->keywords->getButKeywords())
603             );
604         }
605
606         foreach ($this->stepKeywordTypesCache as $type => $keywords) {
607             if (in_array($native, $keywords) || in_array($native . '<', $keywords)) {
608                 return $type;
609             }
610         }
611
612         return 'Given';
613     }
614 }