4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Drush\Internal\Config\Yaml;
14 use Drush\Internal\Config\Yaml\Exception\ParseException;
15 use Drush\Internal\Config\Yaml\Tag\TaggedValue;
18 * Parser parses YAML strings to convert them to PHP arrays.
20 * @author Fabien Potencier <fabien@symfony.com>
24 const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
25 const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
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();
36 public function __construct()
38 if (func_num_args() > 0) {
39 @trigger_error(sprintf('The constructor arguments $offset, $totalNumberOfLines, $skippedLineNumbers of %s are deprecated and will be removed in 4.0', self::class), E_USER_DEPRECATED);
41 $this->offset = func_get_arg(0);
42 if (func_num_args() > 1) {
43 $this->totalNumberOfLines = func_get_arg(1);
45 if (func_num_args() > 2) {
46 $this->skippedLineNumbers = func_get_arg(2);
52 * Parses a YAML string to a PHP value.
54 * @param string $value A YAML string
55 * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
57 * @return mixed A PHP value
59 * @throws ParseException If the YAML is not valid
61 public function parse($value, $flags = 0)
63 if (is_bool($flags)) {
64 @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
67 $flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
73 if (func_num_args() >= 3) {
74 @trigger_error('Passing a boolean flag to toggle object support is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.', E_USER_DEPRECATED);
76 if (func_get_arg(2)) {
77 $flags |= Yaml::PARSE_OBJECT;
81 if (func_num_args() >= 4) {
82 @trigger_error('Passing a boolean flag to toggle object for map support is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.', E_USER_DEPRECATED);
84 if (func_get_arg(3)) {
85 $flags |= Yaml::PARSE_OBJECT_FOR_MAP;
89 if (false === preg_match('//u', $value)) {
90 throw new ParseException('The YAML value does not appear to be valid UTF-8.');
93 $this->refs = array();
99 if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
100 $mbEncoding = mb_internal_encoding();
101 mb_internal_encoding('UTF-8');
105 $data = $this->doParse($value, $flags);
106 } catch (\Exception $e) {
107 } catch (\Throwable $e) {
110 if (null !== $mbEncoding) {
111 mb_internal_encoding($mbEncoding);
114 $this->lines = array();
115 $this->currentLine = '';
116 $this->refs = array();
117 $this->skippedLineNumbers = array();
118 $this->locallySkippedLineNumbers = array();
127 private function doParse($value, $flags)
129 $this->currentLineNb = -1;
130 $this->currentLine = '';
131 $value = $this->cleanup($value);
132 $this->lines = explode("\n", $value);
133 $this->locallySkippedLineNumbers = array();
135 if (null === $this->totalNumberOfLines) {
136 $this->totalNumberOfLines = count($this->lines);
139 if (!$this->moveToNextLine()) {
145 $allowOverwrite = false;
147 while ($this->isCurrentLineEmpty()) {
148 if (!$this->moveToNextLine()) {
153 // Resolves the tag and returns if end of the document
154 if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
155 return new TaggedValue($tag, '');
159 if ($this->isCurrentLineEmpty()) {
164 if ("\t" === $this->currentLine[0]) {
165 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
168 $isRef = $mergeNode = false;
169 if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
170 if ($context && 'mapping' == $context) {
171 throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
173 $context = 'sequence';
175 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
176 $isRef = $matches['ref'];
177 $values['value'] = $matches['value'];
180 if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
181 @trigger_error(sprintf('Starting an unquoted string with a question mark followed by a space is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
185 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
186 $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags);
187 } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
188 $data[] = new TaggedValue(
190 $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
193 if (isset($values['leadspaces'])
194 && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
196 // this is a compact notation element, add to next block and parse
197 $block = $values['value'];
198 if ($this->isNextLineIndented()) {
199 $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
202 $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
204 $data[] = $this->parseValue($values['value'], $flags, $context);
208 $this->refs[$isRef] = end($data);
211 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?(?:![^\s]++\s++)?[^ \'"\[\{!].*?) *\:(\s++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
212 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
214 if ($context && 'sequence' == $context) {
215 throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
217 $context = 'mapping';
219 // force correct settings
220 Inline::parse(null, $flags, $this->refs);
222 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
224 $evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS & $flags);
226 // constants in key will be evaluated anyway
227 if (isset($values['key'][0]) && '!' === $values['key'][0] && Yaml::PARSE_CONSTANT & $flags) {
231 $key = Inline::parseScalar($values['key'], 0, null, $i, $evaluateKey);
232 } catch (ParseException $e) {
233 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
234 $e->setSnippet($this->currentLine);
239 if (!(Yaml::PARSE_KEYS_AS_STRINGS & $flags) && !is_string($key) && !is_int($key)) {
240 $keyType = is_numeric($key) ? 'numeric key' : 'non-string key';
241 @trigger_error(sprintf('Implicit casting of %s to string is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Quote your evaluable mapping keys instead on line %d.', $keyType, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
244 // Convert float keys to strings, to avoid being converted to integers by PHP
245 if (is_float($key)) {
246 $key = (string) $key;
249 if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
251 $allowOverwrite = true;
252 if (isset($values['value'][0]) && '*' === $values['value'][0]) {
253 $refName = substr(rtrim($values['value']), 1);
254 if (!array_key_exists($refName, $this->refs)) {
255 throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
258 $refValue = $this->refs[$refName];
260 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
261 $refValue = (array) $refValue;
264 if (!is_array($refValue)) {
265 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
268 $data += $refValue; // array union
270 if (isset($values['value']) && '' !== $values['value']) {
271 $value = $values['value'];
273 $value = $this->getNextEmbedBlock();
275 $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
277 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
278 $parsed = (array) $parsed;
281 if (!is_array($parsed)) {
282 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
285 if (isset($parsed[0])) {
286 // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
287 // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
288 // in the sequence override keys specified in later mapping nodes.
289 foreach ($parsed as $parsedItem) {
290 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
291 $parsedItem = (array) $parsedItem;
294 if (!is_array($parsedItem)) {
295 throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
298 $data += $parsedItem; // array union
301 // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
302 // current mapping, unless the key already exists in it.
303 $data += $parsed; // array union
306 } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u', $values['value'], $matches)) {
307 $isRef = $matches['ref'];
308 $values['value'] = $matches['value'];
314 } elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
316 // if next line is less indented or equal, then it means that the current value is null
317 if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
318 // Spec: Keys MUST be unique; first one wins.
319 // But overwriting is allowed when a merge node is used in current block.
320 if ($allowOverwrite || !isset($data[$key])) {
321 if (null !== $subTag) {
322 $data[$key] = new TaggedValue($subTag, '');
327 @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
330 // remember the parsed line number here in case we need it to provide some contexts in error messages below
331 $realCurrentLineNbKey = $this->getRealCurrentLineNb();
332 $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
334 $this->refs[$refMatches['ref']] = $value;
336 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
337 $value = (array) $value;
341 } elseif ($allowOverwrite || !isset($data[$key])) {
342 // Spec: Keys MUST be unique; first one wins.
343 // But overwriting is allowed when a merge node is used in current block.
344 if (null !== $subTag) {
345 $data[$key] = new TaggedValue($subTag, $value);
347 $data[$key] = $value;
350 @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $key, $realCurrentLineNbKey + 1), E_USER_DEPRECATED);
354 $value = $this->parseValue(rtrim($values['value']), $flags, $context);
355 // Spec: Keys MUST be unique; first one wins.
356 // But overwriting is allowed when a merge node is used in current block.
357 if ($allowOverwrite || !isset($data[$key])) {
358 $data[$key] = $value;
360 @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
364 $this->refs[$isRef] = $data[$key];
367 // multiple documents are not supported
368 if ('---' === $this->currentLine) {
369 throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
372 if (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1]) {
373 @trigger_error(sprintf('Starting an unquoted string with a question mark followed by a space is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
376 // 1-liner optionally followed by newline(s)
377 if (is_string($value) && $this->lines[0] === trim($value)) {
379 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
380 $value = Inline::parse($this->lines[0], $flags, $this->refs);
381 } catch (ParseException $e) {
382 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
383 $e->setSnippet($this->currentLine);
391 // try to parse the value as a multi-line string as a last resort
392 if (0 === $this->currentLineNb) {
394 $previousLineWasNewline = false;
395 $previousLineWasTerminatedWithBackslash = false;
398 foreach ($this->lines as $line) {
400 if (isset($line[0]) && ('"' === $line[0] || "'" === $line[0])) {
403 $parsedLine = Inline::parse($line, $flags, $this->refs);
406 if (!is_string($parsedLine)) {
411 if ('' === trim($parsedLine)) {
413 } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
417 if ('' !== trim($parsedLine) && '\\' === substr($parsedLine, -1)) {
418 $value .= ltrim(substr($parsedLine, 0, -1));
419 } elseif ('' !== trim($parsedLine)) {
420 $value .= trim($parsedLine);
423 if ('' === trim($parsedLine)) {
424 $previousLineWasNewline = true;
425 $previousLineWasTerminatedWithBackslash = false;
426 } elseif ('\\' === substr($parsedLine, -1)) {
427 $previousLineWasNewline = false;
428 $previousLineWasTerminatedWithBackslash = true;
430 $previousLineWasNewline = false;
431 $previousLineWasTerminatedWithBackslash = false;
433 } catch (ParseException $e) {
440 return Inline::parse(trim($value));
444 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
446 } while ($this->moveToNextLine());
449 $data = new TaggedValue($tag, $data);
452 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !is_object($data) && 'mapping' === $context) {
453 $object = new \stdClass();
455 foreach ($data as $key => $value) {
456 $object->$key = $value;
462 return empty($data) ? null : $data;
465 private function parseBlock($offset, $yaml, $flags)
467 $skippedLineNumbers = $this->skippedLineNumbers;
469 foreach ($this->locallySkippedLineNumbers as $lineNumber) {
470 if ($lineNumber < $offset) {
474 $skippedLineNumbers[] = $lineNumber;
477 $parser = new self();
478 $parser->offset = $offset;
479 $parser->totalNumberOfLines = $this->totalNumberOfLines;
480 $parser->skippedLineNumbers = $skippedLineNumbers;
481 $parser->refs = &$this->refs;
483 return $parser->doParse($yaml, $flags);
487 * Returns the current line number (takes the offset into account).
489 * @return int The current line number
491 private function getRealCurrentLineNb()
493 $realCurrentLineNumber = $this->currentLineNb + $this->offset;
495 foreach ($this->skippedLineNumbers as $skippedLineNumber) {
496 if ($skippedLineNumber > $realCurrentLineNumber) {
500 ++$realCurrentLineNumber;
503 return $realCurrentLineNumber;
507 * Returns the current line indentation.
509 * @return int The current line indentation
511 private function getCurrentLineIndentation()
513 return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
517 * Returns the next embed block of YAML.
519 * @param int $indentation The indent level at which the block is to be read, or null for default
520 * @param bool $inSequence True if the enclosing data structure is a sequence
522 * @return string A YAML string
524 * @throws ParseException When indentation problem are detected
526 private function getNextEmbedBlock($indentation = null, $inSequence = false)
528 $oldLineIndentation = $this->getCurrentLineIndentation();
529 $blockScalarIndentations = array();
531 if ($this->isBlockScalarHeader()) {
532 $blockScalarIndentations[] = $oldLineIndentation;
535 if (!$this->moveToNextLine()) {
539 if (null === $indentation) {
540 $newIndent = $this->getCurrentLineIndentation();
542 $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
544 if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
545 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
548 $newIndent = $indentation;
552 if ($this->getCurrentLineIndentation() >= $newIndent) {
553 $data[] = substr($this->currentLine, $newIndent);
555 $this->moveToPreviousLine();
560 if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
561 // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
562 // and therefore no nested list or mapping
563 $this->moveToPreviousLine();
568 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
570 if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
571 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
574 $previousLineIndentation = $this->getCurrentLineIndentation();
576 while ($this->moveToNextLine()) {
577 $indent = $this->getCurrentLineIndentation();
579 // terminate all block scalars that are more indented than the current line
580 if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && '' !== trim($this->currentLine)) {
581 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
582 if ($blockScalarIndentation >= $indent) {
583 unset($blockScalarIndentations[$key]);
588 if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
589 $blockScalarIndentations[] = $indent;
592 $previousLineIndentation = $indent;
594 if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
595 $this->moveToPreviousLine();
599 if ($this->isCurrentLineBlank()) {
600 $data[] = substr($this->currentLine, $newIndent);
604 if ($indent >= $newIndent) {
605 $data[] = substr($this->currentLine, $newIndent);
606 } elseif ($this->isCurrentLineComment()) {
607 $data[] = $this->currentLine;
608 } elseif (0 == $indent) {
609 $this->moveToPreviousLine();
613 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
617 return implode("\n", $data);
621 * Moves the parser to the next line.
625 private function moveToNextLine()
627 if ($this->currentLineNb >= count($this->lines) - 1) {
631 $this->currentLine = $this->lines[++$this->currentLineNb];
637 * Moves the parser to the previous line.
641 private function moveToPreviousLine()
643 if ($this->currentLineNb < 1) {
647 $this->currentLine = $this->lines[--$this->currentLineNb];
653 * Parses a YAML value.
655 * @param string $value A YAML value
656 * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
657 * @param string $context The parser context (either sequence or mapping)
659 * @return mixed A PHP value
661 * @throws ParseException When reference does not exist
663 private function parseValue($value, $flags, $context)
665 if (0 === strpos($value, '*')) {
666 if (false !== $pos = strpos($value, '#')) {
667 $value = substr($value, 1, $pos - 2);
669 $value = substr($value, 1);
672 if (!array_key_exists($value, $this->refs)) {
673 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
676 return $this->refs[$value];
679 if (self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
680 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
682 $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
684 if ('' !== $matches['tag']) {
685 if ('!!binary' === $matches['tag']) {
686 return Inline::evaluateBinaryScalar($data);
687 } elseif ('!' !== $matches['tag']) {
688 @trigger_error(sprintf('Using the custom tag "%s" for the value "%s" is deprecated since version 3.3. It will be replaced by an instance of %s in 4.0 on line %d.', $matches['tag'], $data, TaggedValue::class, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
696 $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
698 // do not take following lines into account when the current line is a quoted single line value
699 if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) {
700 return Inline::parse($value, $flags, $this->refs);
705 while ($this->moveToNextLine()) {
706 // unquoted strings end before the first unindented line
707 if (null === $quotation && 0 === $this->getCurrentLineIndentation()) {
708 $this->moveToPreviousLine();
713 $lines[] = trim($this->currentLine);
715 // quoted string values end with a line that is terminated with the quotation character
716 if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
721 for ($i = 0, $linesCount = count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
722 if ('' === $lines[$i]) {
724 $previousLineBlank = true;
725 } elseif ($previousLineBlank) {
726 $value .= $lines[$i];
727 $previousLineBlank = false;
729 $value .= ' '.$lines[$i];
730 $previousLineBlank = false;
734 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
735 $parsedValue = Inline::parse($value, $flags, $this->refs);
737 if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
738 throw new ParseException('A colon cannot be used in an unquoted mapping value.');
742 } catch (ParseException $e) {
743 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
744 $e->setSnippet($this->currentLine);
751 * Parses a block scalar.
753 * @param string $style The style indicator that was used to begin this block scalar (| or >)
754 * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
755 * @param int $indentation The indentation indicator that was used to begin this block scalar
757 * @return string The text value
759 private function parseBlockScalar($style, $chomping = '', $indentation = 0)
761 $notEOF = $this->moveToNextLine();
766 $isCurrentLineBlank = $this->isCurrentLineBlank();
767 $blockLines = array();
769 // leading blank lines are consumed before determining indentation
770 while ($notEOF && $isCurrentLineBlank) {
771 // newline only if not EOF
772 if ($notEOF = $this->moveToNextLine()) {
774 $isCurrentLineBlank = $this->isCurrentLineBlank();
778 // determine indentation if not specified
779 if (0 === $indentation) {
780 if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
781 $indentation = strlen($matches[0]);
785 if ($indentation > 0) {
786 $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
790 $isCurrentLineBlank ||
791 self::preg_match($pattern, $this->currentLine, $matches)
794 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
795 $blockLines[] = substr($this->currentLine, $indentation);
796 } elseif ($isCurrentLineBlank) {
799 $blockLines[] = $matches[1];
802 // newline only if not EOF
803 if ($notEOF = $this->moveToNextLine()) {
804 $isCurrentLineBlank = $this->isCurrentLineBlank();
813 $this->moveToPreviousLine();
814 } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
819 if ('>' === $style) {
821 $previousLineIndented = false;
822 $previousLineBlank = false;
824 for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
825 if ('' === $blockLines[$i]) {
827 $previousLineIndented = false;
828 $previousLineBlank = true;
829 } elseif (' ' === $blockLines[$i][0]) {
830 $text .= "\n".$blockLines[$i];
831 $previousLineIndented = true;
832 $previousLineBlank = false;
833 } elseif ($previousLineIndented) {
834 $text .= "\n".$blockLines[$i];
835 $previousLineIndented = false;
836 $previousLineBlank = false;
837 } elseif ($previousLineBlank || 0 === $i) {
838 $text .= $blockLines[$i];
839 $previousLineIndented = false;
840 $previousLineBlank = false;
842 $text .= ' '.$blockLines[$i];
843 $previousLineIndented = false;
844 $previousLineBlank = false;
848 $text = implode("\n", $blockLines);
851 // deal with trailing newlines
852 if ('' === $chomping) {
853 $text = preg_replace('/\n+$/', "\n", $text);
854 } elseif ('-' === $chomping) {
855 $text = preg_replace('/\n+$/', '', $text);
862 * Returns true if the next line is indented.
864 * @return bool Returns true if the next line is indented, false otherwise
866 private function isNextLineIndented()
868 $currentIndentation = $this->getCurrentLineIndentation();
869 $EOF = !$this->moveToNextLine();
871 while (!$EOF && $this->isCurrentLineEmpty()) {
872 $EOF = !$this->moveToNextLine();
879 $ret = $this->getCurrentLineIndentation() > $currentIndentation;
881 $this->moveToPreviousLine();
887 * Returns true if the current line is blank or if it is a comment line.
889 * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
891 private function isCurrentLineEmpty()
893 return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
897 * Returns true if the current line is blank.
899 * @return bool Returns true if the current line is blank, false otherwise
901 private function isCurrentLineBlank()
903 return '' == trim($this->currentLine, ' ');
907 * Returns true if the current line is a comment line.
909 * @return bool Returns true if the current line is a comment line, false otherwise
911 private function isCurrentLineComment()
913 //checking explicitly the first char of the trim is faster than loops or strpos
914 $ltrimmedLine = ltrim($this->currentLine, ' ');
916 return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
919 private function isCurrentLineLastLineInDocument()
921 return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
925 * Cleanups a YAML string to be parsed.
927 * @param string $value The input YAML string
929 * @return string A cleaned up YAML string
931 private function cleanup($value)
933 $value = str_replace(array("\r\n", "\r"), "\n", $value);
937 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
938 $this->offset += $count;
940 // remove leading comments
941 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
943 // items have been removed, update the offset
944 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
945 $value = $trimmedValue;
948 // remove start of the document marker (---)
949 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
951 // items have been removed, update the offset
952 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
953 $value = $trimmedValue;
955 // remove end of the document marker (...)
956 $value = preg_replace('#\.\.\.\s*$#', '', $value);
963 * Returns true if the next line starts unindented collection.
965 * @return bool Returns true if the next line starts unindented collection, false otherwise
967 private function isNextLineUnIndentedCollection()
969 $currentIndentation = $this->getCurrentLineIndentation();
970 $notEOF = $this->moveToNextLine();
972 while ($notEOF && $this->isCurrentLineEmpty()) {
973 $notEOF = $this->moveToNextLine();
976 if (false === $notEOF) {
980 $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
982 $this->moveToPreviousLine();
988 * Returns true if the string is un-indented collection item.
990 * @return bool Returns true if the string is un-indented collection item, false otherwise
992 private function isStringUnIndentedCollectionItem()
994 return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
998 * Tests whether or not the current line is the header of a block scalar.
1002 private function isBlockScalarHeader()
1004 return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
1008 * A local wrapper for `preg_match` which will throw a ParseException if there
1009 * is an internal error in the PCRE engine.
1011 * This avoids us needing to check for "false" every time PCRE is used
1012 * in the YAML engine
1014 * @throws ParseException on a PCRE internal error
1016 * @see preg_last_error()
1020 public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
1022 if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
1023 switch (preg_last_error()) {
1024 case PREG_INTERNAL_ERROR:
1025 $error = 'Internal PCRE error.';
1027 case PREG_BACKTRACK_LIMIT_ERROR:
1028 $error = 'pcre.backtrack_limit reached.';
1030 case PREG_RECURSION_LIMIT_ERROR:
1031 $error = 'pcre.recursion_limit reached.';
1033 case PREG_BAD_UTF8_ERROR:
1034 $error = 'Malformed UTF-8 data.';
1036 case PREG_BAD_UTF8_OFFSET_ERROR:
1037 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
1043 throw new ParseException($error);
1050 * Trim the tag on top of the value.
1052 * Prevent values such as `!foo {quz: bar}` to be considered as
1055 private function trimTag($value)
1057 if ('!' === $value[0]) {
1058 return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
1064 private function getLineTag($value, $flags, $nextLineCheck = true)
1066 if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
1070 if ($nextLineCheck && !$this->isNextLineIndented()) {
1074 $tag = substr($matches['tag'], 1);
1077 if ($tag && '!' === $tag[0]) {
1078 throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag));
1081 if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
1085 throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']));