PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.85
WindPress – Tailwind CSS integration for WordPress v3.2.85
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
windpress / vendor / symfony / yaml / Parser.php

Parser.php in WindPress – Tailwind CSS integration for WordPress 3.2.85, at vendor/symfony/yaml/Parser.php

1,076 lines 50.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 namespace WindPressDeps\Symfony\Component\Yaml;
12
13 use WindPressDeps\Symfony\Component\Yaml\Exception\ParseException;
14 use WindPressDeps\Symfony\Component\Yaml\Tag\TaggedValue;
15 /**
16 * Parser parses YAML strings to convert them to PHP arrays.
17 *
18 * @author Fabien Potencier <fabien@symfony.com>
19 *
20 * @final
21 */
22 class Parser
23 {
24 public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
25 public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
26 public const REFERENCE_PATTERN = '#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u';
27 private $filename;
28 private $offset = 0;
29 private $numberOfParsedLines = 0;
30 private $totalNumberOfLines;
31 private $lines = [];
32 private $currentLineNb = -1;
33 private $currentLine = '';
34 private $refs = [];
35 private $skippedLineNumbers = [];
36 private $locallySkippedLineNumbers = [];
37 private $refsBeingParsed = [];
38 /**
39 * Parses a YAML file into a PHP value.
40 *
41 * @param string $filename The path to the YAML file to be parsed
42 * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
43 *
44 * @return mixed
45 *
46 * @throws ParseException If the file could not be read or the YAML is not valid
47 */
48 public function parseFile(string $filename, int $flags = 0)
49 {
50 if (!is_file($filename)) {
51 throw new ParseException(sprintf('File "%s" does not exist.', $filename));
52 }
53 if (!is_readable($filename)) {
54 throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
55 }
56 $this->filename = $filename;
57 try {
58 return $this->parse(file_get_contents($filename), $flags);
59 } finally {
60 $this->filename = null;
61 }
62 }
63 /**
64 * Parses a YAML string to a PHP value.
65 *
66 * @param string $value A YAML string
67 * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
68 *
69 * @return mixed
70 *
71 * @throws ParseException If the YAML is not valid
72 */
73 public function parse(string $value, int $flags = 0)
74 {
75 if (\false === preg_match('//u', $value)) {
76 throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
77 }
78 $this->refs = [];
79 $mbEncoding = null;
80 if (2 & (int) \ini_get('mbstring.func_overload')) {
81 $mbEncoding = mb_internal_encoding();
82 mb_internal_encoding('UTF-8');
83 }
84 try {
85 $data = $this->doParse($value, $flags);
86 } finally {
87 if (null !== $mbEncoding) {
88 mb_internal_encoding($mbEncoding);
89 }
90 $this->refsBeingParsed = [];
91 $this->offset = 0;
92 $this->lines = [];
93 $this->currentLine = '';
94 $this->numberOfParsedLines = 0;
95 $this->refs = [];
96 $this->skippedLineNumbers = [];
97 $this->locallySkippedLineNumbers = [];
98 $this->totalNumberOfLines = null;
99 }
100 return $data;
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 if (null === $this->totalNumberOfLines) {
111 $this->totalNumberOfLines = $this->numberOfParsedLines;
112 }
113 if (!$this->moveToNextLine()) {
114 return null;
115 }
116 $data = [];
117 $context = null;
118 $allowOverwrite = \false;
119 while ($this->isCurrentLineEmpty()) {
120 if (!$this->moveToNextLine()) {
121 return null;
122 }
123 }
124 // Resolves the tag and returns if end of the document
125 if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, \false)) && !$this->moveToNextLine()) {
126 return new TaggedValue($tag, '');
127 }
128 do {
129 if ($this->isCurrentLineEmpty()) {
130 continue;
131 }
132 // tab?
133 if ("\t" === $this->currentLine[0]) {
134 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
135 }
136 Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
137 $isRef = $mergeNode = \false;
138 if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
139 if ($context && 'mapping' == $context) {
140 throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
141 }
142 $context = 'sequence';
143 if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
144 $isRef = $matches['ref'];
145 $this->refsBeingParsed[] = $isRef;
146 $values['value'] = $matches['value'];
147 }
148 if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
149 throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
150 }
151 // array
152 if (isset($values['value']) && 0 === strpos(ltrim($values['value'], ' '), '-')) {
153 // Inline first child
154 $currentLineNumber = $this->getRealCurrentLineNb();
155 $sequenceIndentation = \strlen($values['leadspaces']) + 1;
156 $sequenceYaml = substr($this->currentLine, $sequenceIndentation);
157 $sequenceYaml .= "\n" . $this->getNextEmbedBlock($sequenceIndentation, \true);
158 $data[] = $this->parseBlock($currentLineNumber, rtrim($sequenceYaml), $flags);
159 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
160 $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, \true) ?? '', $flags);
161 } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
162 $data[] = new TaggedValue($subTag, $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, \true), $flags));
163 } else if (isset($values['leadspaces']) && ('!' === $values['value'][0] || self::preg_match('#^(?P<key>' . Inline::REGEX_QUOTED_STRING . '|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches))) {
164 $block = $values['value'];
165 if ($this->isNextLineIndented() || isset($matches['value']) && '>-' === $matches['value']) {
166 $block .= "\n" . $this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
167 }
168 $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
169 } else {
170 $data[] = $this->parseValue($values['value'], $flags, $context);
171 }
172 if ($isRef) {
173 $this->refs[$isRef] = end($data);
174 array_pop($this->refsBeingParsed);
175 }
176 } elseif (self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:' . Inline::REGEX_QUOTED_STRING . '|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(( |\t)++(?P<value>.+))?$#u', rtrim($this->currentLine), $values) && (\false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))) {
177 if ($context && 'sequence' == $context) {
178 throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
179 }
180 $context = 'mapping';
181 try {
182 $key = Inline::parseScalar($values['key']);
183 } catch (ParseException $e) {
184 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
185 $e->setSnippet($this->currentLine);
186 throw $e;
187 }
188 if (!\is_string($key) && !\is_int($key)) {
189 throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string') . ' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
190 }
191 // Convert float keys to strings, to avoid being converted to integers by PHP
192 if (\is_float($key)) {
193 $key = (string) $key;
194 }
195 if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
196 $mergeNode = \true;
197 $allowOverwrite = \true;
198 if (isset($values['value'][0]) && '*' === $values['value'][0]) {
199 $refName = substr(rtrim($values['value']), 1);
200 if (!\array_key_exists($refName, $this->refs)) {
201 if (\false !== $pos = array_search($refName, $this->refsBeingParsed, \true)) {
202 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);
203 }
204 throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
205 }
206 $refValue = $this->refs[$refName];
207 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
208 $refValue = (array) $refValue;
209 }
210 if (!\is_array($refValue)) {
211 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
212 }
213 $data += $refValue;
214 // array union
215 } else {
216 if (isset($values['value']) && '' !== $values['value']) {
217 $value = $values['value'];
218 } else {
219 $value = $this->getNextEmbedBlock();
220 }
221 $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
222 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
223 $parsed = (array) $parsed;
224 }
225 if (!\is_array($parsed)) {
226 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
227 }
228 if (isset($parsed[0])) {
229 // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
230 // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
231 // in the sequence override keys specified in later mapping nodes.
232 foreach ($parsed as $parsedItem) {
233 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
234 $parsedItem = (array) $parsedItem;
235 }
236 if (!\is_array($parsedItem)) {
237 throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
238 }
239 $data += $parsedItem;
240 // array union
241 }
242 } else {
243 // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
244 // current mapping, unless the key already exists in it.
245 $data += $parsed;
246 // 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'] || 0 === strpos($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 (0 === $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 (0 === $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 (\false !== strpos($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 && '\\' === substr($line, -1)) {
395 $value .= ltrim(substr($line, 0, -1));
396 } elseif ('' !== $trimmedLine) {
397 $value .= $trimmedLine;
398 }
399 if ('' === $trimmedLine) {
400 $previousLineWasNewline = \true;
401 $previousLineWasTerminatedWithBackslash = \false;
402 } elseif ('\\' === substr($line, -1)) {
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 $e) {
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 $offset, string $yaml, int $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 /**
465 * Returns the current line indentation.
466 */
467 private function getCurrentLineIndentation(): int
468 {
469 if (' ' !== ($this->currentLine[0] ?? '')) {
470 return 0;
471 }
472 return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
473 }
474 /**
475 * Returns the next embed block of YAML.
476 *
477 * @param int|null $indentation The indent level at which the block is to be read, or null for default
478 * @param bool $inSequence True if the enclosing data structure is a sequence
479 *
480 * @throws ParseException When indentation problem are detected
481 */
482 private function getNextEmbedBlock(?int $indentation = null, bool $inSequence = \false): string
483 {
484 $oldLineIndentation = $this->getCurrentLineIndentation();
485 if (!$this->moveToNextLine()) {
486 return '';
487 }
488 if (null === $indentation) {
489 $newIndent = null;
490 $movements = 0;
491 do {
492 $EOF = \false;
493 // empty and comment-like lines do not influence the indentation depth
494 if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
495 $EOF = !$this->moveToNextLine();
496 if (!$EOF) {
497 ++$movements;
498 }
499 } else {
500 $newIndent = $this->getCurrentLineIndentation();
501 }
502 } while (!$EOF && null === $newIndent);
503 for ($i = 0; $i < $movements; ++$i) {
504 $this->moveToPreviousLine();
505 }
506 $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
507 if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
508 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
509 }
510 } else {
511 $newIndent = $indentation;
512 }
513 $data = [];
514 if ($this->getCurrentLineIndentation() >= $newIndent) {
515 $data[] = substr($this->currentLine, $newIndent ?? 0);
516 } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
517 $data[] = $this->currentLine;
518 } else {
519 $this->moveToPreviousLine();
520 return '';
521 }
522 if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
523 // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
524 // and therefore no nested list or mapping
525 $this->moveToPreviousLine();
526 return '';
527 }
528 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
529 $isItComment = $this->isCurrentLineComment();
530 while ($this->moveToNextLine()) {
531 if ($isItComment && !$isItUnindentedCollection) {
532 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
533 $isItComment = $this->isCurrentLineComment();
534 }
535 $indent = $this->getCurrentLineIndentation();
536 if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
537 $this->moveToPreviousLine();
538 break;
539 }
540 if ($this->isCurrentLineBlank()) {
541 $data[] = substr($this->currentLine, $newIndent ?? 0);
542 continue;
543 }
544 if ($indent >= $newIndent) {
545 $data[] = substr($this->currentLine, $newIndent ?? 0);
546 } elseif ($this->isCurrentLineComment()) {
547 $data[] = $this->currentLine;
548 } elseif (0 == $indent) {
549 $this->moveToPreviousLine();
550 break;
551 } else {
552 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
553 }
554 }
555 return implode("\n", $data);
556 }
557 private function hasMoreLines(): bool
558 {
559 return \count($this->lines) - 1 > $this->currentLineNb;
560 }
561 /**
562 * Moves the parser to the next line.
563 */
564 private function moveToNextLine(): bool
565 {
566 if ($this->currentLineNb >= $this->numberOfParsedLines - 1) {
567 return \false;
568 }
569 $this->currentLine = $this->lines[++$this->currentLineNb];
570 return \true;
571 }
572 /**
573 * Moves the parser to the previous line.
574 */
575 private function moveToPreviousLine(): bool
576 {
577 if ($this->currentLineNb < 1) {
578 return \false;
579 }
580 $this->currentLine = $this->lines[--$this->currentLineNb];
581 return \true;
582 }
583 /**
584 * Parses a YAML value.
585 *
586 * @param string $value A YAML value
587 * @param int $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
588 * @param string $context The parser context (either sequence or mapping)
589 *
590 * @return mixed
591 *
592 * @throws ParseException When reference does not exist
593 */
594 private function parseValue(string $value, int $flags, string $context)
595 {
596 if (0 === strpos($value, '*')) {
597 if (\false !== $pos = strpos($value, '#')) {
598 $value = substr($value, 1, $pos - 2);
599 } else {
600 $value = substr($value, 1);
601 }
602 if (!\array_key_exists($value, $this->refs)) {
603 if (\false !== $pos = array_search($value, $this->refsBeingParsed, \true)) {
604 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);
605 }
606 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
607 }
608 return $this->refs[$value];
609 }
610 if (\in_array($value[0], ['!', '|', '>'], \true) && self::preg_match('/^(?:' . self::TAG_PATTERN . ' +)?' . self::BLOCK_SCALAR_HEADER_PATTERN . '$/', $value, $matches)) {
611 $modifiers = $matches['modifiers'] ?? '';
612 $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers));
613 if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
614 if ('!!binary' === $matches['tag']) {
615 return Inline::evaluateBinaryScalar($data);
616 }
617 return new TaggedValue(substr($matches['tag'], 1), $data);
618 }
619 return $data;
620 }
621 try {
622 if ('' !== $value && '{' === $value[0]) {
623 $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
624 return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs);
625 } elseif ('' !== $value && '[' === $value[0]) {
626 $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
627 return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs);
628 }
629 switch ($value[0] ?? '') {
630 case '"':
631 case "'":
632 $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
633 $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs);
634 if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) {
635 throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor)));
636 }
637 return $parsedValue;
638 default:
639 $lines = [];
640 while ($this->moveToNextLine()) {
641 // unquoted strings end before the first unindented line
642 if (0 === $this->getCurrentLineIndentation()) {
643 $this->moveToPreviousLine();
644 break;
645 }
646 $lines[] = trim($this->currentLine);
647 }
648 for ($i = 0, $linesCount = \count($lines), $previousLineBlank = \false; $i < $linesCount; ++$i) {
649 if ('' === $lines[$i]) {
650 $value .= "\n";
651 $previousLineBlank = \true;
652 } elseif ($previousLineBlank) {
653 $value .= $lines[$i];
654 $previousLineBlank = \false;
655 } else {
656 $value .= ' ' . $lines[$i];
657 $previousLineBlank = \false;
658 }
659 }
660 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
661 $parsedValue = Inline::parse($value, $flags, $this->refs);
662 if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && \false !== strpos($parsedValue, ': ')) {
663 throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
664 }
665 return $parsedValue;
666 }
667 } catch (ParseException $e) {
668 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
669 $e->setSnippet($this->currentLine);
670 throw $e;
671 }
672 }
673 /**
674 * Parses a block scalar.
675 *
676 * @param string $style The style indicator that was used to begin this block scalar (| or >)
677 * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
678 * @param int $indentation The indentation indicator that was used to begin this block scalar
679 */
680 private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string
681 {
682 $notEOF = $this->moveToNextLine();
683 if (!$notEOF) {
684 return '';
685 }
686 $isCurrentLineBlank = $this->isCurrentLineBlank();
687 $blockLines = [];
688 // leading blank lines are consumed before determining indentation
689 while ($notEOF && $isCurrentLineBlank) {
690 // newline only if not EOF
691 if ($notEOF = $this->moveToNextLine()) {
692 $blockLines[] = '';
693 $isCurrentLineBlank = $this->isCurrentLineBlank();
694 }
695 }
696 // determine indentation if not specified
697 if (0 === $indentation) {
698 $currentLineLength = \strlen($this->currentLine);
699 for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) {
700 ++$indentation;
701 }
702 }
703 if ($indentation > 0) {
704 $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
705 while ($notEOF && ($isCurrentLineBlank || self::preg_match($pattern, $this->currentLine, $matches))) {
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 || 0 === $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 for ($i = 0; $i < $movements; ++$i) {
781 $this->moveToPreviousLine();
782 }
783 return \false;
784 }
785 $ret = $this->getCurrentLineIndentation() > $currentIndentation;
786 for ($i = 0; $i < $movements; ++$i) {
787 $this->moveToPreviousLine();
788 }
789 return $ret;
790 }
791 /**
792 * Returns true if the current line is blank or if it is a comment line.
793 */
794 private function isCurrentLineEmpty(): bool
795 {
796 return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
797 }
798 /**
799 * Returns true if the current line is blank.
800 */
801 private function isCurrentLineBlank(): bool
802 {
803 return '' === $this->currentLine || '' === trim($this->currentLine, ' ');
804 }
805 /**
806 * Returns true if the current line is a comment line.
807 */
808 private function isCurrentLineComment(): bool
809 {
810 // checking explicitly the first char of the trim is faster than loops or strpos
811 $ltrimmedLine = '' !== $this->currentLine && ' ' === $this->currentLine[0] ? ltrim($this->currentLine, ' ') : $this->currentLine;
812 return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
813 }
814 private function isCurrentLineLastLineInDocument(): bool
815 {
816 return $this->offset + $this->currentLineNb >= $this->totalNumberOfLines - 1;
817 }
818 /**
819 * Cleanups a YAML string to be parsed.
820 *
821 * @param string $value The input YAML string
822 */
823 private function cleanup(string $value): string
824 {
825 $value = str_replace(["\r\n", "\r"], "\n", $value);
826 // strip YAML header
827 $count = 0;
828 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
829 $this->offset += $count;
830 // remove leading comments
831 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
832 if (1 === $count) {
833 // items have been removed, update the offset
834 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
835 $value = $trimmedValue;
836 }
837 // remove start of the document marker (---)
838 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
839 if (1 === $count) {
840 // items have been removed, update the offset
841 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
842 $value = $trimmedValue;
843 // remove end of the document marker (...)
844 $value = preg_replace('#\.\.\.\s*$#', '', $value);
845 }
846 return $value;
847 }
848 /**
849 * Returns true if the next line starts unindented collection.
850 */
851 private function isNextLineUnIndentedCollection(): bool
852 {
853 $currentIndentation = $this->getCurrentLineIndentation();
854 $movements = 0;
855 do {
856 $EOF = !$this->moveToNextLine();
857 if (!$EOF) {
858 ++$movements;
859 }
860 } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
861 if ($EOF) {
862 return \false;
863 }
864 $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
865 for ($i = 0; $i < $movements; ++$i) {
866 $this->moveToPreviousLine();
867 }
868 return $ret;
869 }
870 /**
871 * Returns true if the string is un-indented collection item.
872 */
873 private function isStringUnIndentedCollectionItem(): bool
874 {
875 return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
876 }
877 /**
878 * A local wrapper for "preg_match" which will throw a ParseException if there
879 * is an internal error in the PCRE engine.
880 *
881 * This avoids us needing to check for "false" every time PCRE is used
882 * in the YAML engine
883 *
884 * @throws ParseException on a PCRE internal error
885 *
886 * @see preg_last_error()
887 *
888 * @internal
889 */
890 public static function preg_match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
891 {
892 if (\false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
893 switch (preg_last_error()) {
894 case \PREG_INTERNAL_ERROR:
895 $error = 'Internal PCRE error.';
896 break;
897 case \PREG_BACKTRACK_LIMIT_ERROR:
898 $error = 'pcre.backtrack_limit reached.';
899 break;
900 case \PREG_RECURSION_LIMIT_ERROR:
901 $error = 'pcre.recursion_limit reached.';
902 break;
903 case \PREG_BAD_UTF8_ERROR:
904 $error = 'Malformed UTF-8 data.';
905 break;
906 case \PREG_BAD_UTF8_OFFSET_ERROR:
907 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
908 break;
909 default:
910 $error = 'Error.';
911 }
912 throw new ParseException($error);
913 }
914 return $ret;
915 }
916 /**
917 * Trim the tag on top of the value.
918 *
919 * Prevent values such as "!foo {quz: bar}" to be considered as
920 * a mapping block.
921 */
922 private function trimTag(string $value): string
923 {
924 if ('!' === $value[0]) {
925 return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
926 }
927 return $value;
928 }
929 private function getLineTag(string $value, int $flags, bool $nextLineCheck = \true): ?string
930 {
931 if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^' . self::TAG_PATTERN . ' *( +#.*)?$/', $value, $matches)) {
932 return null;
933 }
934 if ($nextLineCheck && !$this->isNextLineIndented()) {
935 return null;
936 }
937 $tag = substr($matches['tag'], 1);
938 // Built-in tags
939 if ($tag && '!' === $tag[0]) {
940 throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
941 }
942 if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
943 return $tag;
944 }
945 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);
946 }
947 private function lexInlineQuotedString(int &$cursor = 0): string
948 {
949 $quotation = $this->currentLine[$cursor];
950 $value = $quotation;
951 ++$cursor;
952 $previousLineWasNewline = \true;
953 $previousLineWasTerminatedWithBackslash = \false;
954 $lineNumber = 0;
955 do {
956 if (++$lineNumber > 1) {
957 $cursor += strspn($this->currentLine, ' ', $cursor);
958 }
959 if ($this->isCurrentLineBlank()) {
960 $value .= "\n";
961 } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
962 $value .= ' ';
963 }
964 for (; \strlen($this->currentLine) > $cursor; ++$cursor) {
965 switch ($this->currentLine[$cursor]) {
966 case '\\':
967 if ("'" === $quotation) {
968 $value .= '\\';
969 } elseif (isset($this->currentLine[++$cursor])) {
970 $value .= '\\' . $this->currentLine[$cursor];
971 }
972 break;
973 case $quotation:
974 ++$cursor;
975 if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) {
976 $value .= "''";
977 break;
978 }
979 return $value . $quotation;
980 default:
981 $value .= $this->currentLine[$cursor];
982 }
983 }
984 if ($this->isCurrentLineBlank()) {
985 $previousLineWasNewline = \true;
986 $previousLineWasTerminatedWithBackslash = \false;
987 } elseif ('\\' === $this->currentLine[-1]) {
988 $previousLineWasNewline = \false;
989 $previousLineWasTerminatedWithBackslash = \true;
990 } else {
991 $previousLineWasNewline = \false;
992 $previousLineWasTerminatedWithBackslash = \false;
993 }
994 if ($this->hasMoreLines()) {
995 $cursor = 0;
996 }
997 } while ($this->moveToNextLine());
998 throw new ParseException('Malformed inline YAML string.');
999 }
1000 private function lexUnquotedString(int &$cursor): string
1001 {
1002 $offset = $cursor;
1003 $cursor += strcspn($this->currentLine, '[]{},: ', $cursor);
1004 if ($cursor === $offset) {
1005 throw new ParseException('Malformed unquoted YAML string.');
1006 }
1007 return substr($this->currentLine, $offset, $cursor - $offset);
1008 }
1009 private function lexInlineMapping(int &$cursor = 0): string
1010 {
1011 return $this->lexInlineStructure($cursor, '}');
1012 }
1013 private function lexInlineSequence(int &$cursor = 0): string
1014 {
1015 return $this->lexInlineStructure($cursor, ']');
1016 }
1017 private function lexInlineStructure(int &$cursor, string $closingTag): string
1018 {
1019 $value = $this->currentLine[$cursor];
1020 ++$cursor;
1021 do {
1022 $this->consumeWhitespaces($cursor);
1023 while (isset($this->currentLine[$cursor])) {
1024 switch ($this->currentLine[$cursor]) {
1025 case '"':
1026 case "'":
1027 $value .= $this->lexInlineQuotedString($cursor);
1028 break;
1029 case ':':
1030 case ',':
1031 $value .= $this->currentLine[$cursor];
1032 ++$cursor;
1033 break;
1034 case '{':
1035 $value .= $this->lexInlineMapping($cursor);
1036 break;
1037 case '[':
1038 $value .= $this->lexInlineSequence($cursor);
1039 break;
1040 case $closingTag:
1041 $value .= $this->currentLine[$cursor];
1042 ++$cursor;
1043 return $value;
1044 case '#':
1045 break 2;
1046 default:
1047 $value .= $this->lexUnquotedString($cursor);
1048 }
1049 if ($this->consumeWhitespaces($cursor)) {
1050 $value .= ' ';
1051 }
1052 }
1053 if ($this->hasMoreLines()) {
1054 $cursor = 0;
1055 }
1056 } while ($this->moveToNextLine());
1057 throw new ParseException('Malformed inline YAML string.');
1058 }
1059 private function consumeWhitespaces(int &$cursor): bool
1060 {
1061 $whitespacesConsumed = 0;
1062 do {
1063 $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor);
1064 $whitespacesConsumed += $whitespaceOnlyTokenLength;
1065 $cursor += $whitespaceOnlyTokenLength;
1066 if (isset($this->currentLine[$cursor])) {
1067 return 0 < $whitespacesConsumed;
1068 }
1069 if ($this->hasMoreLines()) {
1070 $cursor = 0;
1071 }
1072 } while ($this->moveToNextLine());
1073 return 0 < $whitespacesConsumed;
1074 }
1075 }
1076