"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/symfony/yaml/Parser.php" (1 Sep 2020, 42220 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Parser.php" see the Fossies "Dox" file reference documentation.

    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 use Symfony\Component\Yaml\Tag\TaggedValue;
   16 
   17 /**
   18  * Parser parses YAML strings to convert them to PHP arrays.
   19  *
   20  * @author Fabien Potencier <fabien@symfony.com>
   21  *
   22  * @final
   23  */
   24 class Parser
   25 {
   26     const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
   27     const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
   28 
   29     private $filename;
   30     private $offset = 0;
   31     private $totalNumberOfLines;
   32     private $lines = [];
   33     private $currentLineNb = -1;
   34     private $currentLine = '';
   35     private $refs = [];
   36     private $skippedLineNumbers = [];
   37     private $locallySkippedLineNumbers = [];
   38     private $refsBeingParsed = [];
   39 
   40     /**
   41      * Parses a YAML file into a PHP value.
   42      *
   43      * @param string $filename The path to the YAML file to be parsed
   44      * @param int    $flags    A bit field of PARSE_* constants to customize the YAML parser behavior
   45      *
   46      * @return mixed The YAML converted to a PHP value
   47      *
   48      * @throws ParseException If the file could not be read or the YAML is not valid
   49      */
   50     public function parseFile(string $filename, int $flags = 0)
   51     {
   52         if (!is_file($filename)) {
   53             throw new ParseException(sprintf('File "%s" does not exist.', $filename));
   54         }
   55 
   56         if (!is_readable($filename)) {
   57             throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
   58         }
   59 
   60         $this->filename = $filename;
   61 
   62         try {
   63             return $this->parse(file_get_contents($filename), $flags);
   64         } finally {
   65             $this->filename = null;
   66         }
   67     }
   68 
   69     /**
   70      * Parses a YAML string to a PHP value.
   71      *
   72      * @param string $value A YAML string
   73      * @param int    $flags A bit field of PARSE_* constants to customize the YAML parser behavior
   74      *
   75      * @return mixed A PHP value
   76      *
   77      * @throws ParseException If the YAML is not valid
   78      */
   79     public function parse(string $value, int $flags = 0)
   80     {
   81         if (false === preg_match('//u', $value)) {
   82             throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
   83         }
   84 
   85         $this->refs = [];
   86 
   87         $mbEncoding = null;
   88         $data = null;
   89 
   90         if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
   91             $mbEncoding = mb_internal_encoding();
   92             mb_internal_encoding('UTF-8');
   93         }
   94 
   95         try {
   96             $data = $this->doParse($value, $flags);
   97         } finally {
   98             if (null !== $mbEncoding) {
   99                 mb_internal_encoding($mbEncoding);
  100             }
  101             $this->lines = [];
  102             $this->currentLine = '';
  103             $this->refs = [];
  104             $this->skippedLineNumbers = [];
  105             $this->locallySkippedLineNumbers = [];
  106         }
  107 
  108         return $data;
  109     }
  110 
  111     /**
  112      * @internal
  113      *
  114      * @return int
  115      */
  116     public function getLastLineNumberBeforeDeprecation(): int
  117     {
  118         return $this->getRealCurrentLineNb();
  119     }
  120 
  121     private function doParse(string $value, int $flags)
  122     {
  123         $this->currentLineNb = -1;
  124         $this->currentLine = '';
  125         $value = $this->cleanup($value);
  126         $this->lines = explode("\n", $value);
  127         $this->locallySkippedLineNumbers = [];
  128 
  129         if (null === $this->totalNumberOfLines) {
  130             $this->totalNumberOfLines = \count($this->lines);
  131         }
  132 
  133         if (!$this->moveToNextLine()) {
  134             return null;
  135         }
  136 
  137         $data = [];
  138         $context = null;
  139         $allowOverwrite = false;
  140 
  141         while ($this->isCurrentLineEmpty()) {
  142             if (!$this->moveToNextLine()) {
  143                 return null;
  144             }
  145         }
  146 
  147         // Resolves the tag and returns if end of the document
  148         if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
  149             return new TaggedValue($tag, '');
  150         }
  151 
  152         do {
  153             if ($this->isCurrentLineEmpty()) {
  154                 continue;
  155             }
  156 
  157             // tab?
  158             if ("\t" === $this->currentLine[0]) {
  159                 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  160             }
  161 
  162             Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
  163 
  164             $isRef = $mergeNode = false;
  165             if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
  166                 if ($context && 'mapping' == $context) {
  167                     throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  168                 }
  169                 $context = 'sequence';
  170 
  171                 if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
  172                     $isRef = $matches['ref'];
  173                     $this->refsBeingParsed[] = $isRef;
  174                     $values['value'] = $matches['value'];
  175                 }
  176 
  177                 if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
  178                     throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  179                 }
  180 
  181                 // array
  182                 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
  183                     $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true) ?? '', $flags);
  184                 } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
  185                     $data[] = new TaggedValue(
  186                         $subTag,
  187                         $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
  188                     );
  189                 } else {
  190                     if (isset($values['leadspaces'])
  191                         && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
  192                     ) {
  193                         // this is a compact notation element, add to next block and parse
  194                         $block = $values['value'];
  195                         if ($this->isNextLineIndented()) {
  196                             $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
  197                         }
  198 
  199                         $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
  200                     } else {
  201                         $data[] = $this->parseValue($values['value'], $flags, $context);
  202                     }
  203                 }
  204                 if ($isRef) {
  205                     $this->refs[$isRef] = end($data);
  206                     array_pop($this->refsBeingParsed);
  207                 }
  208             } elseif (
  209                 self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(\s++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
  210                 && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))
  211             ) {
  212                 if ($context && 'sequence' == $context) {
  213                     throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine, $this->filename);
  214                 }
  215                 $context = 'mapping';
  216 
  217                 try {
  218                     $key = Inline::parseScalar($values['key']);
  219                 } catch (ParseException $e) {
  220                     $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  221                     $e->setSnippet($this->currentLine);
  222 
  223                     throw $e;
  224                 }
  225 
  226                 if (!\is_string($key) && !\is_int($key)) {
  227                     throw new ParseException(sprintf('%s keys are not supported. Quote your evaluable mapping keys instead.', is_numeric($key) ? 'Numeric' : 'Non-string'), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  228                 }
  229 
  230                 // Convert float keys to strings, to avoid being converted to integers by PHP
  231                 if (\is_float($key)) {
  232                     $key = (string) $key;
  233                 }
  234 
  235                 if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
  236                     $mergeNode = true;
  237                     $allowOverwrite = true;
  238                     if (isset($values['value'][0]) && '*' === $values['value'][0]) {
  239                         $refName = substr(rtrim($values['value']), 1);
  240                         if (!\array_key_exists($refName, $this->refs)) {
  241                             if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) {
  242                                 throw new ParseException(sprintf('Circular reference [%s, %s] detected for reference "%s".', implode(', ', \array_slice($this->refsBeingParsed, $pos)), $refName, $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
  243                             }
  244 
  245                             throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  246                         }
  247 
  248                         $refValue = $this->refs[$refName];
  249 
  250                         if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
  251                             $refValue = (array) $refValue;
  252                         }
  253 
  254                         if (!\is_array($refValue)) {
  255                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  256                         }
  257 
  258                         $data += $refValue; // array union
  259                     } else {
  260                         if (isset($values['value']) && '' !== $values['value']) {
  261                             $value = $values['value'];
  262                         } else {
  263                             $value = $this->getNextEmbedBlock();
  264                         }
  265                         $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
  266 
  267                         if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
  268                             $parsed = (array) $parsed;
  269                         }
  270 
  271                         if (!\is_array($parsed)) {
  272                             throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  273                         }
  274 
  275                         if (isset($parsed[0])) {
  276                             // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
  277                             // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
  278                             // in the sequence override keys specified in later mapping nodes.
  279                             foreach ($parsed as $parsedItem) {
  280                                 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
  281                                     $parsedItem = (array) $parsedItem;
  282                                 }
  283 
  284                                 if (!\is_array($parsedItem)) {
  285                                     throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
  286                                 }
  287 
  288                                 $data += $parsedItem; // array union
  289                             }
  290                         } else {
  291                             // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
  292                             // current mapping, unless the key already exists in it.
  293                             $data += $parsed; // array union
  294                         }
  295                     }
  296                 } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u', $values['value'], $matches)) {
  297                     $isRef = $matches['ref'];
  298                     $this->refsBeingParsed[] = $isRef;
  299                     $values['value'] = $matches['value'];
  300                 }
  301 
  302                 $subTag = null;
  303                 if ($mergeNode) {
  304                     // Merge keys
  305                 } elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
  306                     // hash
  307                     // if next line is less indented or equal, then it means that the current value is null
  308                     if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
  309                         // Spec: Keys MUST be unique; first one wins.
  310                         // But overwriting is allowed when a merge node is used in current block.
  311                         if ($allowOverwrite || !isset($data[$key])) {
  312                             if (null !== $subTag) {
  313                                 $data[$key] = new TaggedValue($subTag, '');
  314                             } else {
  315                                 $data[$key] = null;
  316                             }
  317                         } else {
  318                             throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  319                         }
  320                     } else {
  321                         // remember the parsed line number here in case we need it to provide some contexts in error messages below
  322                         $realCurrentLineNbKey = $this->getRealCurrentLineNb();
  323                         $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
  324                         if ('<<' === $key) {
  325                             $this->refs[$refMatches['ref']] = $value;
  326 
  327                             if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
  328                                 $value = (array) $value;
  329                             }
  330 
  331                             $data += $value;
  332                         } elseif ($allowOverwrite || !isset($data[$key])) {
  333                             // Spec: Keys MUST be unique; first one wins.
  334                             // But overwriting is allowed when a merge node is used in current block.
  335                             if (null !== $subTag) {
  336                                 $data[$key] = new TaggedValue($subTag, $value);
  337                             } else {
  338                                 $data[$key] = $value;
  339                             }
  340                         } else {
  341                             throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $realCurrentLineNbKey + 1, $this->currentLine);
  342                         }
  343                     }
  344                 } else {
  345                     $value = $this->parseValue(rtrim($values['value']), $flags, $context);
  346                     // Spec: Keys MUST be unique; first one wins.
  347                     // But overwriting is allowed when a merge node is used in current block.
  348                     if ($allowOverwrite || !isset($data[$key])) {
  349                         $data[$key] = $value;
  350                     } else {
  351                         throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  352                     }
  353                 }
  354                 if ($isRef) {
  355                     $this->refs[$isRef] = $data[$key];
  356                     array_pop($this->refsBeingParsed);
  357                 }
  358             } else {
  359                 // multiple documents are not supported
  360                 if ('---' === $this->currentLine) {
  361                     throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
  362                 }
  363 
  364                 if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) {
  365                     throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  366                 }
  367 
  368                 // 1-liner optionally followed by newline(s)
  369                 if (\is_string($value) && $this->lines[0] === trim($value)) {
  370                     try {
  371                         $value = Inline::parse($this->lines[0], $flags, $this->refs);
  372                     } catch (ParseException $e) {
  373                         $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  374                         $e->setSnippet($this->currentLine);
  375 
  376                         throw $e;
  377                     }
  378 
  379                     return $value;
  380                 }
  381 
  382                 // try to parse the value as a multi-line string as a last resort
  383                 if (0 === $this->currentLineNb) {
  384                     $previousLineWasNewline = false;
  385                     $previousLineWasTerminatedWithBackslash = false;
  386                     $value = '';
  387 
  388                     foreach ($this->lines as $line) {
  389                         // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
  390                         if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) {
  391                             throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  392                         }
  393                         if ('' === trim($line)) {
  394                             $value .= "\n";
  395                         } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
  396                             $value .= ' ';
  397                         }
  398 
  399                         if ('' !== trim($line) && '\\' === substr($line, -1)) {
  400                             $value .= ltrim(substr($line, 0, -1));
  401                         } elseif ('' !== trim($line)) {
  402                             $value .= trim($line);
  403                         }
  404 
  405                         if ('' === trim($line)) {
  406                             $previousLineWasNewline = true;
  407                             $previousLineWasTerminatedWithBackslash = false;
  408                         } elseif ('\\' === substr($line, -1)) {
  409                             $previousLineWasNewline = false;
  410                             $previousLineWasTerminatedWithBackslash = true;
  411                         } else {
  412                             $previousLineWasNewline = false;
  413                             $previousLineWasTerminatedWithBackslash = false;
  414                         }
  415                     }
  416 
  417                     try {
  418                         return Inline::parse(trim($value));
  419                     } catch (ParseException $e) {
  420                         // fall-through to the ParseException thrown below
  421                     }
  422                 }
  423 
  424                 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  425             }
  426         } while ($this->moveToNextLine());
  427 
  428         if (null !== $tag) {
  429             $data = new TaggedValue($tag, $data);
  430         }
  431 
  432         if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !\is_object($data) && 'mapping' === $context) {
  433             $object = new \stdClass();
  434 
  435             foreach ($data as $key => $value) {
  436                 $object->$key = $value;
  437             }
  438 
  439             $data = $object;
  440         }
  441 
  442         return empty($data) ? null : $data;
  443     }
  444 
  445     private function parseBlock(int $offset, string $yaml, int $flags)
  446     {
  447         $skippedLineNumbers = $this->skippedLineNumbers;
  448 
  449         foreach ($this->locallySkippedLineNumbers as $lineNumber) {
  450             if ($lineNumber < $offset) {
  451                 continue;
  452             }
  453 
  454             $skippedLineNumbers[] = $lineNumber;
  455         }
  456 
  457         $parser = new self();
  458         $parser->offset = $offset;
  459         $parser->totalNumberOfLines = $this->totalNumberOfLines;
  460         $parser->skippedLineNumbers = $skippedLineNumbers;
  461         $parser->refs = &$this->refs;
  462         $parser->refsBeingParsed = $this->refsBeingParsed;
  463 
  464         return $parser->doParse($yaml, $flags);
  465     }
  466 
  467     /**
  468      * Returns the current line number (takes the offset into account).
  469      *
  470      * @internal
  471      *
  472      * @return int The current line number
  473      */
  474     public function getRealCurrentLineNb(): int
  475     {
  476         $realCurrentLineNumber = $this->currentLineNb + $this->offset;
  477 
  478         foreach ($this->skippedLineNumbers as $skippedLineNumber) {
  479             if ($skippedLineNumber > $realCurrentLineNumber) {
  480                 break;
  481             }
  482 
  483             ++$realCurrentLineNumber;
  484         }
  485 
  486         return $realCurrentLineNumber;
  487     }
  488 
  489     /**
  490      * Returns the current line indentation.
  491      *
  492      * @return int The current line indentation
  493      */
  494     private function getCurrentLineIndentation(): int
  495     {
  496         return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
  497     }
  498 
  499     /**
  500      * Returns the next embed block of YAML.
  501      *
  502      * @param int|null $indentation The indent level at which the block is to be read, or null for default
  503      * @param bool     $inSequence  True if the enclosing data structure is a sequence
  504      *
  505      * @return string A YAML string
  506      *
  507      * @throws ParseException When indentation problem are detected
  508      */
  509     private function getNextEmbedBlock(int $indentation = null, bool $inSequence = false): ?string
  510     {
  511         $oldLineIndentation = $this->getCurrentLineIndentation();
  512 
  513         if (!$this->moveToNextLine()) {
  514             return null;
  515         }
  516 
  517         if (null === $indentation) {
  518             $newIndent = null;
  519             $movements = 0;
  520 
  521             do {
  522                 $EOF = false;
  523 
  524                 // empty and comment-like lines do not influence the indentation depth
  525                 if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  526                     $EOF = !$this->moveToNextLine();
  527 
  528                     if (!$EOF) {
  529                         ++$movements;
  530                     }
  531                 } else {
  532                     $newIndent = $this->getCurrentLineIndentation();
  533                 }
  534             } while (!$EOF && null === $newIndent);
  535 
  536             for ($i = 0; $i < $movements; ++$i) {
  537                 $this->moveToPreviousLine();
  538             }
  539 
  540             $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
  541 
  542             if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
  543                 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  544             }
  545         } else {
  546             $newIndent = $indentation;
  547         }
  548 
  549         $data = [];
  550         if ($this->getCurrentLineIndentation() >= $newIndent) {
  551             $data[] = substr($this->currentLine, $newIndent);
  552         } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  553             $data[] = $this->currentLine;
  554         } else {
  555             $this->moveToPreviousLine();
  556 
  557             return null;
  558         }
  559 
  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();
  564 
  565             return null;
  566         }
  567 
  568         $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
  569 
  570         while ($this->moveToNextLine()) {
  571             $indent = $this->getCurrentLineIndentation();
  572 
  573             if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
  574                 $this->moveToPreviousLine();
  575                 break;
  576             }
  577 
  578             if ($this->isCurrentLineBlank()) {
  579                 $data[] = substr($this->currentLine, $newIndent);
  580                 continue;
  581             }
  582 
  583             if ($indent >= $newIndent) {
  584                 $data[] = substr($this->currentLine, $newIndent);
  585             } elseif ($this->isCurrentLineComment()) {
  586                 $data[] = $this->currentLine;
  587             } elseif (0 == $indent) {
  588                 $this->moveToPreviousLine();
  589 
  590                 break;
  591             } else {
  592                 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  593             }
  594         }
  595 
  596         return implode("\n", $data);
  597     }
  598 
  599     /**
  600      * Moves the parser to the next line.
  601      *
  602      * @return bool
  603      */
  604     private function moveToNextLine(): bool
  605     {
  606         if ($this->currentLineNb >= \count($this->lines) - 1) {
  607             return false;
  608         }
  609 
  610         $this->currentLine = $this->lines[++$this->currentLineNb];
  611 
  612         return true;
  613     }
  614 
  615     /**
  616      * Moves the parser to the previous line.
  617      *
  618      * @return bool
  619      */
  620     private function moveToPreviousLine(): bool
  621     {
  622         if ($this->currentLineNb < 1) {
  623             return false;
  624         }
  625 
  626         $this->currentLine = $this->lines[--$this->currentLineNb];
  627 
  628         return true;
  629     }
  630 
  631     /**
  632      * Parses a YAML value.
  633      *
  634      * @param string $value   A YAML value
  635      * @param int    $flags   A bit field of PARSE_* constants to customize the YAML parser behavior
  636      * @param string $context The parser context (either sequence or mapping)
  637      *
  638      * @return mixed A PHP value
  639      *
  640      * @throws ParseException When reference does not exist
  641      */
  642     private function parseValue(string $value, int $flags, string $context)
  643     {
  644         if (0 === strpos($value, '*')) {
  645             if (false !== $pos = strpos($value, '#')) {
  646                 $value = substr($value, 1, $pos - 2);
  647             } else {
  648                 $value = substr($value, 1);
  649             }
  650 
  651             if (!\array_key_exists($value, $this->refs)) {
  652                 if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) {
  653                     throw new ParseException(sprintf('Circular reference [%s, %s] detected for reference "%s".', implode(', ', \array_slice($this->refsBeingParsed, $pos)), $value, $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
  654                 }
  655 
  656                 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
  657             }
  658 
  659             return $this->refs[$value];
  660         }
  661 
  662         if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
  663             $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
  664 
  665             $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
  666 
  667             if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
  668                 if ('!!binary' === $matches['tag']) {
  669                     return Inline::evaluateBinaryScalar($data);
  670                 }
  671 
  672                 return new TaggedValue(substr($matches['tag'], 1), $data);
  673             }
  674 
  675             return $data;
  676         }
  677 
  678         try {
  679             $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
  680 
  681             // do not take following lines into account when the current line is a quoted single line value
  682             if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) {
  683                 return Inline::parse($value, $flags, $this->refs);
  684             }
  685 
  686             $lines = [];
  687 
  688             while ($this->moveToNextLine()) {
  689                 // unquoted strings end before the first unindented line
  690                 if (null === $quotation && 0 === $this->getCurrentLineIndentation()) {
  691                     $this->moveToPreviousLine();
  692 
  693                     break;
  694                 }
  695 
  696                 $lines[] = trim($this->currentLine);
  697 
  698                 // quoted string values end with a line that is terminated with the quotation character
  699                 if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
  700                     break;
  701                 }
  702             }
  703 
  704             for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
  705                 if ('' === $lines[$i]) {
  706                     $value .= "\n";
  707                     $previousLineBlank = true;
  708                 } elseif ($previousLineBlank) {
  709                     $value .= $lines[$i];
  710                     $previousLineBlank = false;
  711                 } else {
  712                     $value .= ' '.$lines[$i];
  713                     $previousLineBlank = false;
  714                 }
  715             }
  716 
  717             Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
  718 
  719             $parsedValue = Inline::parse($value, $flags, $this->refs);
  720 
  721             if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
  722                 throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
  723             }
  724 
  725             return $parsedValue;
  726         } catch (ParseException $e) {
  727             $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  728             $e->setSnippet($this->currentLine);
  729 
  730             throw $e;
  731         }
  732     }
  733 
  734     /**
  735      * Parses a block scalar.
  736      *
  737      * @param string $style       The style indicator that was used to begin this block scalar (| or >)
  738      * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
  739      * @param int    $indentation The indentation indicator that was used to begin this block scalar
  740      *
  741      * @return string The text value
  742      */
  743     private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string
  744     {
  745         $notEOF = $this->moveToNextLine();
  746         if (!$notEOF) {
  747             return '';
  748         }
  749 
  750         $isCurrentLineBlank = $this->isCurrentLineBlank();
  751         $blockLines = [];
  752 
  753         // leading blank lines are consumed before determining indentation
  754         while ($notEOF && $isCurrentLineBlank) {
  755             // newline only if not EOF
  756             if ($notEOF = $this->moveToNextLine()) {
  757                 $blockLines[] = '';
  758                 $isCurrentLineBlank = $this->isCurrentLineBlank();
  759             }
  760         }
  761 
  762         // determine indentation if not specified
  763         if (0 === $indentation) {
  764             $currentLineLength = \strlen($this->currentLine);
  765 
  766             for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) {
  767                 ++$indentation;
  768             }
  769         }
  770 
  771         if ($indentation > 0) {
  772             $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
  773 
  774             while (
  775                 $notEOF && (
  776                     $isCurrentLineBlank ||
  777                     self::preg_match($pattern, $this->currentLine, $matches)
  778                 )
  779             ) {
  780                 if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
  781                     $blockLines[] = substr($this->currentLine, $indentation);
  782                 } elseif ($isCurrentLineBlank) {
  783                     $blockLines[] = '';
  784                 } else {
  785                     $blockLines[] = $matches[1];
  786                 }
  787 
  788                 // newline only if not EOF
  789                 if ($notEOF = $this->moveToNextLine()) {
  790                     $isCurrentLineBlank = $this->isCurrentLineBlank();
  791                 }
  792             }
  793         } elseif ($notEOF) {
  794             $blockLines[] = '';
  795         }
  796 
  797         if ($notEOF) {
  798             $blockLines[] = '';
  799             $this->moveToPreviousLine();
  800         } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
  801             $blockLines[] = '';
  802         }
  803 
  804         // folded style
  805         if ('>' === $style) {
  806             $text = '';
  807             $previousLineIndented = false;
  808             $previousLineBlank = false;
  809 
  810             for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
  811                 if ('' === $blockLines[$i]) {
  812                     $text .= "\n";
  813                     $previousLineIndented = false;
  814                     $previousLineBlank = true;
  815                 } elseif (' ' === $blockLines[$i][0]) {
  816                     $text .= "\n".$blockLines[$i];
  817                     $previousLineIndented = true;
  818                     $previousLineBlank = false;
  819                 } elseif ($previousLineIndented) {
  820                     $text .= "\n".$blockLines[$i];
  821                     $previousLineIndented = false;
  822                     $previousLineBlank = false;
  823                 } elseif ($previousLineBlank || 0 === $i) {
  824                     $text .= $blockLines[$i];
  825                     $previousLineIndented = false;
  826                     $previousLineBlank = false;
  827                 } else {
  828                     $text .= ' '.$blockLines[$i];
  829                     $previousLineIndented = false;
  830                     $previousLineBlank = false;
  831                 }
  832             }
  833         } else {
  834             $text = implode("\n", $blockLines);
  835         }
  836 
  837         // deal with trailing newlines
  838         if ('' === $chomping) {
  839             $text = preg_replace('/\n+$/', "\n", $text);
  840         } elseif ('-' === $chomping) {
  841             $text = preg_replace('/\n+$/', '', $text);
  842         }
  843 
  844         return $text;
  845     }
  846 
  847     /**
  848      * Returns true if the next line is indented.
  849      *
  850      * @return bool Returns true if the next line is indented, false otherwise
  851      */
  852     private function isNextLineIndented(): bool
  853     {
  854         $currentIndentation = $this->getCurrentLineIndentation();
  855         $movements = 0;
  856 
  857         do {
  858             $EOF = !$this->moveToNextLine();
  859 
  860             if (!$EOF) {
  861                 ++$movements;
  862             }
  863         } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  864 
  865         if ($EOF) {
  866             return false;
  867         }
  868 
  869         $ret = $this->getCurrentLineIndentation() > $currentIndentation;
  870 
  871         for ($i = 0; $i < $movements; ++$i) {
  872             $this->moveToPreviousLine();
  873         }
  874 
  875         return $ret;
  876     }
  877 
  878     /**
  879      * Returns true if the current line is blank or if it is a comment line.
  880      *
  881      * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
  882      */
  883     private function isCurrentLineEmpty(): bool
  884     {
  885         return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
  886     }
  887 
  888     /**
  889      * Returns true if the current line is blank.
  890      *
  891      * @return bool Returns true if the current line is blank, false otherwise
  892      */
  893     private function isCurrentLineBlank(): bool
  894     {
  895         return '' == trim($this->currentLine, ' ');
  896     }
  897 
  898     /**
  899      * Returns true if the current line is a comment line.
  900      *
  901      * @return bool Returns true if the current line is a comment line, false otherwise
  902      */
  903     private function isCurrentLineComment(): bool
  904     {
  905         //checking explicitly the first char of the trim is faster than loops or strpos
  906         $ltrimmedLine = ltrim($this->currentLine, ' ');
  907 
  908         return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
  909     }
  910 
  911     private function isCurrentLineLastLineInDocument(): bool
  912     {
  913         return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
  914     }
  915 
  916     /**
  917      * Cleanups a YAML string to be parsed.
  918      *
  919      * @param string $value The input YAML string
  920      *
  921      * @return string A cleaned up YAML string
  922      */
  923     private function cleanup(string $value): string
  924     {
  925         $value = str_replace(["\r\n", "\r"], "\n", $value);
  926 
  927         // strip YAML header
  928         $count = 0;
  929         $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
  930         $this->offset += $count;
  931 
  932         // remove leading comments
  933         $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
  934         if (1 === $count) {
  935             // items have been removed, update the offset
  936             $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
  937             $value = $trimmedValue;
  938         }
  939 
  940         // remove start of the document marker (---)
  941         $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
  942         if (1 === $count) {
  943             // items have been removed, update the offset
  944             $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
  945             $value = $trimmedValue;
  946 
  947             // remove end of the document marker (...)
  948             $value = preg_replace('#\.\.\.\s*$#', '', $value);
  949         }
  950 
  951         return $value;
  952     }
  953 
  954     /**
  955      * Returns true if the next line starts unindented collection.
  956      *
  957      * @return bool Returns true if the next line starts unindented collection, false otherwise
  958      */
  959     private function isNextLineUnIndentedCollection(): bool
  960     {
  961         $currentIndentation = $this->getCurrentLineIndentation();
  962         $movements = 0;
  963 
  964         do {
  965             $EOF = !$this->moveToNextLine();
  966 
  967             if (!$EOF) {
  968                 ++$movements;
  969             }
  970         } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  971 
  972         if ($EOF) {
  973             return false;
  974         }
  975 
  976         $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
  977 
  978         for ($i = 0; $i < $movements; ++$i) {
  979             $this->moveToPreviousLine();
  980         }
  981 
  982         return $ret;
  983     }
  984 
  985     /**
  986      * Returns true if the string is un-indented collection item.
  987      *
  988      * @return bool Returns true if the string is un-indented collection item, false otherwise
  989      */
  990     private function isStringUnIndentedCollectionItem(): bool
  991     {
  992         return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
  993     }
  994 
  995     /**
  996      * A local wrapper for "preg_match" which will throw a ParseException if there
  997      * is an internal error in the PCRE engine.
  998      *
  999      * This avoids us needing to check for "false" every time PCRE is used
 1000      * in the YAML engine
 1001      *
 1002      * @throws ParseException on a PCRE internal error
 1003      *
 1004      * @see preg_last_error()
 1005      *
 1006      * @internal
 1007      */
 1008     public static function preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int
 1009     {
 1010         if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
 1011             switch (preg_last_error()) {
 1012                 case PREG_INTERNAL_ERROR:
 1013                     $error = 'Internal PCRE error.';
 1014                     break;
 1015                 case PREG_BACKTRACK_LIMIT_ERROR:
 1016                     $error = 'pcre.backtrack_limit reached.';
 1017                     break;
 1018                 case PREG_RECURSION_LIMIT_ERROR:
 1019                     $error = 'pcre.recursion_limit reached.';
 1020                     break;
 1021                 case PREG_BAD_UTF8_ERROR:
 1022                     $error = 'Malformed UTF-8 data.';
 1023                     break;
 1024                 case PREG_BAD_UTF8_OFFSET_ERROR:
 1025                     $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
 1026                     break;
 1027                 default:
 1028                     $error = 'Error.';
 1029             }
 1030 
 1031             throw new ParseException($error);
 1032         }
 1033 
 1034         return $ret;
 1035     }
 1036 
 1037     /**
 1038      * Trim the tag on top of the value.
 1039      *
 1040      * Prevent values such as "!foo {quz: bar}" to be considered as
 1041      * a mapping block.
 1042      */
 1043     private function trimTag(string $value): string
 1044     {
 1045         if ('!' === $value[0]) {
 1046             return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
 1047         }
 1048 
 1049         return $value;
 1050     }
 1051 
 1052     private function getLineTag(string $value, int $flags, bool $nextLineCheck = true): ?string
 1053     {
 1054         if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
 1055             return null;
 1056         }
 1057 
 1058         if ($nextLineCheck && !$this->isNextLineIndented()) {
 1059             return null;
 1060         }
 1061 
 1062         $tag = substr($matches['tag'], 1);
 1063 
 1064         // Built-in tags
 1065         if ($tag && '!' === $tag[0]) {
 1066             throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
 1067         }
 1068 
 1069         if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
 1070             return $tag;
 1071         }
 1072 
 1073         throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
 1074     }
 1075 }