Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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