vendor/symfony/yaml/Parser.php line 86

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