PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.90
WindPress – Tailwind CSS integration for WordPress v3.2.90
3.2.90 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 All 144 releases
windpress / vendor / symfony / yaml / Parser.php

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

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