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