vendor/symfony/yaml/Parser.php line 565

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