vendor/symfony/yaml/Parser.php line 580

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