vendor/symfony/yaml/Parser.php line 459

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