Version 1
[yaffs-website] / vendor / symfony / yaml / Parser.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Yaml;
13
14 use Symfony\Component\Yaml\Exception\ParseException;
15
16 /**
17  * Parser parses YAML strings to convert them to PHP arrays.
18  *
19  * @author Fabien Potencier <fabien@symfony.com>
20  */
21 class Parser
22 {
23     const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
24     // BC - wrongly named
25     const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
26
27     private $offset = 0;
28     private $totalNumberOfLines;
29     private $lines = array();
30     private $currentLineNb = -1;
31     private $currentLine = '';
32     private $refs = array();
33     private $skippedLineNumbers = array();
34     private $locallySkippedLineNumbers = array();
35
36     /**
37      * Constructor.
38      *
39      * @param int      $offset             The offset of YAML document (used for line numbers in error messages)
40      * @param int|null $totalNumberOfLines The overall number of lines being parsed
41      * @param int[]    $skippedLineNumbers Number of comment lines that have been skipped by the parser
42      */
43     public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
44     {
45         $this->offset = $offset;
46         $this->totalNumberOfLines = $totalNumberOfLines;
47         $this->skippedLineNumbers = $skippedLineNumbers;
48     }
49
50     /**
51      * Parses a YAML string to a PHP value.
52      *
53      * @param string $value                  A YAML string
54      * @param bool   $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
55      * @param bool   $objectSupport          true if object support is enabled, false otherwise
56      * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
57      *
58      * @return mixed A PHP value
59      *
60      * @throws ParseException If the YAML is not valid
61      */
62     public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
63     {
64         if (false === preg_match('//u', $value)) {
65             throw new ParseException('The YAML value does not appear to be valid UTF-8.');
66         }
67         $this->currentLineNb = -1;
68         $this->currentLine = '';
69         $value = $this->cleanup($value);
70         $this->lines = explode("\n", $value);
71
72         if (null === $this->totalNumberOfLines) {
73             $this->totalNumberOfLines = count($this->lines);
74         }
75
76         if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
77             $mbEncoding = mb_internal_encoding();
78             mb_internal_encoding('UTF-8');
79         }
80
81         $data = array();
82         $context = null;
83         $allowOverwrite = false;
84         while ($this->moveToNextLine()) {
85             if ($this->isCurrentLineEmpty()) {
86                 continue;
87             }
88
89             // tab?
90             if ("\t" === $this->currentLine[0]) {
91                 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
92             }
93
94             $isRef = $mergeNode = false;
95             if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
96                 if ($context && 'mapping' == $context) {
97                     throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
98                 }
99                 $context = 'sequence';
100
101                 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
102                     $isRef = $matches['ref'];
103                     $values['value'] = $matches['value'];
104                 }
105
106                 // array
107                 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
108                     $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
109                 } else {
110                     if (isset($values['leadspaces'])
111                         && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
112                     ) {
113                         // this is a compact notation element, add to next block and parse
114                         $block = $values['value'];
115                         if ($this->isNextLineIndented()) {
116                             $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
117                         }
118
119                         $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
120                     } else {
121                         $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
122                     }
123                 }
124                 if ($isRef) {
125                     $this->refs[$isRef] = end($data);
126                 }
127             } elseif (
128                 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
129                 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
130             ) {
131                 if ($context && 'sequence' == $context) {
132                     throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
133                 }
134                 $context = 'mapping';
135
136                 // force correct settings
137                 Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
138                 try {
139                     $key = Inline::parseScalar($values['key']);
140                 } catch (ParseException $e) {
141                     $e->setParsedLine($this->getRealCurrentLineNb() + 1);
142                     $e->setSnippet($this->currentLine);
143
144                     throw $e;
145                 }
146
147                 // Convert float keys to strings, to avoid being converted to integers by PHP
148                 if (is_float($key)) {
149                     $key = (string) $key;
150                 }
151
152                 if ('<<' === $key) {
153                     $mergeNode = true;
154                     $allowOverwrite = true;
155                     if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
156                         $refName = substr($values['value'], 1);
157                         if (!array_key_exists($refName, $this->refs)) {
158                             throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
159                         }
160
161                         $refValue = $this->refs[$refName];
162
163                         if (!is_array($refValue)) {
164                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
165                         }
166
167                         $data += $refValue; // array union
168                     } else {
169                         if (isset($values['value']) && $values['value'] !== '') {
170                             $value = $values['value'];
171                         } else {
172                             $value = $this->getNextEmbedBlock();
173                         }
174                         $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
175
176                         if (!is_array($parsed)) {
177                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
178                         }
179
180                         if (isset($parsed[0])) {
181                             // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
182                             // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
183                             // in the sequence override keys specified in later mapping nodes.
184                             foreach ($parsed as $parsedItem) {
185                                 if (!is_array($parsedItem)) {
186                                     throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
187                                 }
188
189                                 $data += $parsedItem; // array union
190                             }
191                         } else {
192                             // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
193                             // current mapping, unless the key already exists in it.
194                             $data += $parsed; // array union
195                         }
196                     }
197                 } elseif (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
198                     $isRef = $matches['ref'];
199                     $values['value'] = $matches['value'];
200                 }
201
202                 if ($mergeNode) {
203                     // Merge keys
204                 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
205                     // hash
206                     // if next line is less indented or equal, then it means that the current value is null
207                     if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
208                         // Spec: Keys MUST be unique; first one wins.
209                         // But overwriting is allowed when a merge node is used in current block.
210                         if ($allowOverwrite || !isset($data[$key])) {
211                             $data[$key] = null;
212                         }
213                     } else {
214                         $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
215                         // Spec: Keys MUST be unique; first one wins.
216                         // But overwriting is allowed when a merge node is used in current block.
217                         if ($allowOverwrite || !isset($data[$key])) {
218                             $data[$key] = $value;
219                         }
220                     }
221                 } else {
222                     $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
223                     // Spec: Keys MUST be unique; first one wins.
224                     // But overwriting is allowed when a merge node is used in current block.
225                     if ($allowOverwrite || !isset($data[$key])) {
226                         $data[$key] = $value;
227                     }
228                 }
229                 if ($isRef) {
230                     $this->refs[$isRef] = $data[$key];
231                 }
232             } else {
233                 // multiple documents are not supported
234                 if ('---' === $this->currentLine) {
235                     throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
236                 }
237
238                 // 1-liner optionally followed by newline(s)
239                 if (is_string($value) && $this->lines[0] === trim($value)) {
240                     try {
241                         $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
242                     } catch (ParseException $e) {
243                         $e->setParsedLine($this->getRealCurrentLineNb() + 1);
244                         $e->setSnippet($this->currentLine);
245
246                         throw $e;
247                     }
248
249                     if (isset($mbEncoding)) {
250                         mb_internal_encoding($mbEncoding);
251                     }
252
253                     return $value;
254                 }
255
256                 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
257             }
258         }
259
260         if (isset($mbEncoding)) {
261             mb_internal_encoding($mbEncoding);
262         }
263
264         if ($objectForMap && !is_object($data) && 'mapping' === $context) {
265             $object = new \stdClass();
266
267             foreach ($data as $key => $value) {
268                 $object->$key = $value;
269             }
270
271             $data = $object;
272         }
273
274         return empty($data) ? null : $data;
275     }
276
277     private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
278     {
279         $skippedLineNumbers = $this->skippedLineNumbers;
280
281         foreach ($this->locallySkippedLineNumbers as $lineNumber) {
282             if ($lineNumber < $offset) {
283                 continue;
284             }
285
286             $skippedLineNumbers[] = $lineNumber;
287         }
288
289         $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
290         $parser->refs = &$this->refs;
291
292         return $parser->parse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
293     }
294
295     /**
296      * Returns the current line number (takes the offset into account).
297      *
298      * @return int The current line number
299      */
300     private function getRealCurrentLineNb()
301     {
302         $realCurrentLineNumber = $this->currentLineNb + $this->offset;
303
304         foreach ($this->skippedLineNumbers as $skippedLineNumber) {
305             if ($skippedLineNumber > $realCurrentLineNumber) {
306                 break;
307             }
308
309             ++$realCurrentLineNumber;
310         }
311
312         return $realCurrentLineNumber;
313     }
314
315     /**
316      * Returns the current line indentation.
317      *
318      * @return int The current line indentation
319      */
320     private function getCurrentLineIndentation()
321     {
322         return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
323     }
324
325     /**
326      * Returns the next embed block of YAML.
327      *
328      * @param int  $indentation The indent level at which the block is to be read, or null for default
329      * @param bool $inSequence  True if the enclosing data structure is a sequence
330      *
331      * @return string A YAML string
332      *
333      * @throws ParseException When indentation problem are detected
334      */
335     private function getNextEmbedBlock($indentation = null, $inSequence = false)
336     {
337         $oldLineIndentation = $this->getCurrentLineIndentation();
338         $blockScalarIndentations = array();
339
340         if ($this->isBlockScalarHeader()) {
341             $blockScalarIndentations[] = $this->getCurrentLineIndentation();
342         }
343
344         if (!$this->moveToNextLine()) {
345             return;
346         }
347
348         if (null === $indentation) {
349             $newIndent = $this->getCurrentLineIndentation();
350
351             $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
352
353             if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
354                 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
355             }
356         } else {
357             $newIndent = $indentation;
358         }
359
360         $data = array();
361         if ($this->getCurrentLineIndentation() >= $newIndent) {
362             $data[] = substr($this->currentLine, $newIndent);
363         } else {
364             $this->moveToPreviousLine();
365
366             return;
367         }
368
369         if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
370             // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
371             // and therefore no nested list or mapping
372             $this->moveToPreviousLine();
373
374             return;
375         }
376
377         $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
378
379         if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
380             $blockScalarIndentations[] = $this->getCurrentLineIndentation();
381         }
382
383         $previousLineIndentation = $this->getCurrentLineIndentation();
384
385         while ($this->moveToNextLine()) {
386             $indent = $this->getCurrentLineIndentation();
387
388             // terminate all block scalars that are more indented than the current line
389             if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
390                 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
391                     if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
392                         unset($blockScalarIndentations[$key]);
393                     }
394                 }
395             }
396
397             if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
398                 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
399             }
400
401             $previousLineIndentation = $indent;
402
403             if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
404                 $this->moveToPreviousLine();
405                 break;
406             }
407
408             if ($this->isCurrentLineBlank()) {
409                 $data[] = substr($this->currentLine, $newIndent);
410                 continue;
411             }
412
413             // we ignore "comment" lines only when we are not inside a scalar block
414             if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
415                 // remember ignored comment lines (they are used later in nested
416                 // parser calls to determine real line numbers)
417                 //
418                 // CAUTION: beware to not populate the global property here as it
419                 // will otherwise influence the getRealCurrentLineNb() call here
420                 // for consecutive comment lines and subsequent embedded blocks
421                 $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
422
423                 continue;
424             }
425
426             if ($indent >= $newIndent) {
427                 $data[] = substr($this->currentLine, $newIndent);
428             } elseif (0 == $indent) {
429                 $this->moveToPreviousLine();
430
431                 break;
432             } else {
433                 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
434             }
435         }
436
437         return implode("\n", $data);
438     }
439
440     /**
441      * Moves the parser to the next line.
442      *
443      * @return bool
444      */
445     private function moveToNextLine()
446     {
447         if ($this->currentLineNb >= count($this->lines) - 1) {
448             return false;
449         }
450
451         $this->currentLine = $this->lines[++$this->currentLineNb];
452
453         return true;
454     }
455
456     /**
457      * Moves the parser to the previous line.
458      *
459      * @return bool
460      */
461     private function moveToPreviousLine()
462     {
463         if ($this->currentLineNb < 1) {
464             return false;
465         }
466
467         $this->currentLine = $this->lines[--$this->currentLineNb];
468
469         return true;
470     }
471
472     /**
473      * Parses a YAML value.
474      *
475      * @param string $value                  A YAML value
476      * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
477      * @param bool   $objectSupport          True if object support is enabled, false otherwise
478      * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
479      * @param string $context                The parser context (either sequence or mapping)
480      *
481      * @return mixed A PHP value
482      *
483      * @throws ParseException When reference does not exist
484      */
485     private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
486     {
487         if (0 === strpos($value, '*')) {
488             if (false !== $pos = strpos($value, '#')) {
489                 $value = substr($value, 1, $pos - 2);
490             } else {
491                 $value = substr($value, 1);
492             }
493
494             if (!array_key_exists($value, $this->refs)) {
495                 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
496             }
497
498             return $this->refs[$value];
499         }
500
501         if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
502             $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
503
504             return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
505         }
506
507         try {
508             $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
509
510             if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
511                 @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
512
513                 // to be thrown in 3.0
514                 // throw new ParseException('A colon cannot be used in an unquoted mapping value.');
515             }
516
517             return $parsedValue;
518         } catch (ParseException $e) {
519             $e->setParsedLine($this->getRealCurrentLineNb() + 1);
520             $e->setSnippet($this->currentLine);
521
522             throw $e;
523         }
524     }
525
526     /**
527      * Parses a block scalar.
528      *
529      * @param string $style       The style indicator that was used to begin this block scalar (| or >)
530      * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
531      * @param int    $indentation The indentation indicator that was used to begin this block scalar
532      *
533      * @return string The text value
534      */
535     private function parseBlockScalar($style, $chomping = '', $indentation = 0)
536     {
537         $notEOF = $this->moveToNextLine();
538         if (!$notEOF) {
539             return '';
540         }
541
542         $isCurrentLineBlank = $this->isCurrentLineBlank();
543         $blockLines = array();
544
545         // leading blank lines are consumed before determining indentation
546         while ($notEOF && $isCurrentLineBlank) {
547             // newline only if not EOF
548             if ($notEOF = $this->moveToNextLine()) {
549                 $blockLines[] = '';
550                 $isCurrentLineBlank = $this->isCurrentLineBlank();
551             }
552         }
553
554         // determine indentation if not specified
555         if (0 === $indentation) {
556             if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
557                 $indentation = strlen($matches[0]);
558             }
559         }
560
561         if ($indentation > 0) {
562             $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
563
564             while (
565                 $notEOF && (
566                     $isCurrentLineBlank ||
567                     self::preg_match($pattern, $this->currentLine, $matches)
568                 )
569             ) {
570                 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
571                     $blockLines[] = substr($this->currentLine, $indentation);
572                 } elseif ($isCurrentLineBlank) {
573                     $blockLines[] = '';
574                 } else {
575                     $blockLines[] = $matches[1];
576                 }
577
578                 // newline only if not EOF
579                 if ($notEOF = $this->moveToNextLine()) {
580                     $isCurrentLineBlank = $this->isCurrentLineBlank();
581                 }
582             }
583         } elseif ($notEOF) {
584             $blockLines[] = '';
585         }
586
587         if ($notEOF) {
588             $blockLines[] = '';
589             $this->moveToPreviousLine();
590         } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
591             $blockLines[] = '';
592         }
593
594         // folded style
595         if ('>' === $style) {
596             $text = '';
597             $previousLineIndented = false;
598             $previousLineBlank = false;
599
600             for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
601                 if ('' === $blockLines[$i]) {
602                     $text .= "\n";
603                     $previousLineIndented = false;
604                     $previousLineBlank = true;
605                 } elseif (' ' === $blockLines[$i][0]) {
606                     $text .= "\n".$blockLines[$i];
607                     $previousLineIndented = true;
608                     $previousLineBlank = false;
609                 } elseif ($previousLineIndented) {
610                     $text .= "\n".$blockLines[$i];
611                     $previousLineIndented = false;
612                     $previousLineBlank = false;
613                 } elseif ($previousLineBlank || 0 === $i) {
614                     $text .= $blockLines[$i];
615                     $previousLineIndented = false;
616                     $previousLineBlank = false;
617                 } else {
618                     $text .= ' '.$blockLines[$i];
619                     $previousLineIndented = false;
620                     $previousLineBlank = false;
621                 }
622             }
623         } else {
624             $text = implode("\n", $blockLines);
625         }
626
627         // deal with trailing newlines
628         if ('' === $chomping) {
629             $text = preg_replace('/\n+$/', "\n", $text);
630         } elseif ('-' === $chomping) {
631             $text = preg_replace('/\n+$/', '', $text);
632         }
633
634         return $text;
635     }
636
637     /**
638      * Returns true if the next line is indented.
639      *
640      * @return bool Returns true if the next line is indented, false otherwise
641      */
642     private function isNextLineIndented()
643     {
644         $currentIndentation = $this->getCurrentLineIndentation();
645         $EOF = !$this->moveToNextLine();
646
647         while (!$EOF && $this->isCurrentLineEmpty()) {
648             $EOF = !$this->moveToNextLine();
649         }
650
651         if ($EOF) {
652             return false;
653         }
654
655         $ret = $this->getCurrentLineIndentation() > $currentIndentation;
656
657         $this->moveToPreviousLine();
658
659         return $ret;
660     }
661
662     /**
663      * Returns true if the current line is blank or if it is a comment line.
664      *
665      * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
666      */
667     private function isCurrentLineEmpty()
668     {
669         return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
670     }
671
672     /**
673      * Returns true if the current line is blank.
674      *
675      * @return bool Returns true if the current line is blank, false otherwise
676      */
677     private function isCurrentLineBlank()
678     {
679         return '' == trim($this->currentLine, ' ');
680     }
681
682     /**
683      * Returns true if the current line is a comment line.
684      *
685      * @return bool Returns true if the current line is a comment line, false otherwise
686      */
687     private function isCurrentLineComment()
688     {
689         //checking explicitly the first char of the trim is faster than loops or strpos
690         $ltrimmedLine = ltrim($this->currentLine, ' ');
691
692         return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
693     }
694
695     private function isCurrentLineLastLineInDocument()
696     {
697         return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
698     }
699
700     /**
701      * Cleanups a YAML string to be parsed.
702      *
703      * @param string $value The input YAML string
704      *
705      * @return string A cleaned up YAML string
706      */
707     private function cleanup($value)
708     {
709         $value = str_replace(array("\r\n", "\r"), "\n", $value);
710
711         // strip YAML header
712         $count = 0;
713         $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
714         $this->offset += $count;
715
716         // remove leading comments
717         $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
718         if ($count == 1) {
719             // items have been removed, update the offset
720             $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
721             $value = $trimmedValue;
722         }
723
724         // remove start of the document marker (---)
725         $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
726         if ($count == 1) {
727             // items have been removed, update the offset
728             $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
729             $value = $trimmedValue;
730
731             // remove end of the document marker (...)
732             $value = preg_replace('#\.\.\.\s*$#', '', $value);
733         }
734
735         return $value;
736     }
737
738     /**
739      * Returns true if the next line starts unindented collection.
740      *
741      * @return bool Returns true if the next line starts unindented collection, false otherwise
742      */
743     private function isNextLineUnIndentedCollection()
744     {
745         $currentIndentation = $this->getCurrentLineIndentation();
746         $notEOF = $this->moveToNextLine();
747
748         while ($notEOF && $this->isCurrentLineEmpty()) {
749             $notEOF = $this->moveToNextLine();
750         }
751
752         if (false === $notEOF) {
753             return false;
754         }
755
756         $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
757
758         $this->moveToPreviousLine();
759
760         return $ret;
761     }
762
763     /**
764      * Returns true if the string is un-indented collection item.
765      *
766      * @return bool Returns true if the string is un-indented collection item, false otherwise
767      */
768     private function isStringUnIndentedCollectionItem()
769     {
770         return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
771     }
772
773     /**
774      * Tests whether or not the current line is the header of a block scalar.
775      *
776      * @return bool
777      */
778     private function isBlockScalarHeader()
779     {
780         return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
781     }
782
783     /**
784      * A local wrapper for `preg_match` which will throw a ParseException if there
785      * is an internal error in the PCRE engine.
786      *
787      * This avoids us needing to check for "false" every time PCRE is used
788      * in the YAML engine
789      *
790      * @throws ParseException on a PCRE internal error
791      *
792      * @see preg_last_error()
793      *
794      * @internal
795      */
796     public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
797     {
798         if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
799             switch (preg_last_error()) {
800                 case PREG_INTERNAL_ERROR:
801                     $error = 'Internal PCRE error.';
802                     break;
803                 case PREG_BACKTRACK_LIMIT_ERROR:
804                     $error = 'pcre.backtrack_limit reached.';
805                     break;
806                 case PREG_RECURSION_LIMIT_ERROR:
807                     $error = 'pcre.recursion_limit reached.';
808                     break;
809                 case PREG_BAD_UTF8_ERROR:
810                     $error = 'Malformed UTF-8 data.';
811                     break;
812                 case PREG_BAD_UTF8_OFFSET_ERROR:
813                     $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
814                     break;
815                 default:
816                     $error = 'Error.';
817             }
818
819             throw new ParseException($error);
820         }
821
822         return $ret;
823     }
824 }