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 12namespace Symfony\Component\Yaml; 13 14use Symfony\Component\Yaml\Exception\ParseException; 15 16/** 17 * Parser parses YAML strings to convert them to PHP arrays. 18 * 19 * @author Fabien Potencier <fabien@symfony.com> 20 */ 21class Parser 22{ 23 const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?'; 24 // BC - wrongly named 25 const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN; 26 27 private $offset = 0; 28 private $totalNumberOfLines; 29 private $lines = array(); 30 private $currentLineNb = -1; 31 private $currentLine = ''; 32 private $refs = array(); 33 private $skippedLineNumbers = array(); 34 private $locallySkippedLineNumbers = array(); 35 36 /** 37 * @param int $offset The offset of YAML document (used for line numbers in error messages) 38 * @param int|null $totalNumberOfLines The overall number of lines being parsed 39 * @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser 40 */ 41 public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array()) 42 { 43 $this->offset = $offset; 44 $this->totalNumberOfLines = $totalNumberOfLines; 45 $this->skippedLineNumbers = $skippedLineNumbers; 46 } 47 48 /** 49 * Parses a YAML string to a PHP value. 50 * 51 * @param string $value A YAML string 52 * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise 53 * @param bool $objectSupport True if object support is enabled, false otherwise 54 * @param bool $objectForMap True if maps should return a stdClass instead of array() 55 * 56 * @return mixed A PHP value 57 * 58 * @throws ParseException If the YAML is not valid 59 */ 60 public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) 61 { 62 if (false === preg_match('//u', $value)) { 63 throw new ParseException('The YAML value does not appear to be valid UTF-8.'); 64 } 65 66 $this->refs = array(); 67 68 $mbEncoding = null; 69 $e = null; 70 $data = null; 71 72 if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { 73 $mbEncoding = mb_internal_encoding(); 74 mb_internal_encoding('UTF-8'); 75 } 76 77 try { 78 $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap); 79 } catch (\Exception $e) { 80 } catch (\Throwable $e) { 81 } 82 83 if (null !== $mbEncoding) { 84 mb_internal_encoding($mbEncoding); 85 } 86 87 $this->lines = array(); 88 $this->currentLine = ''; 89 $this->refs = array(); 90 $this->skippedLineNumbers = array(); 91 $this->locallySkippedLineNumbers = array(); 92 93 if (null !== $e) { 94 throw $e; 95 } 96 97 return $data; 98 } 99 100 private function doParse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) 101 { 102 $this->currentLineNb = -1; 103 $this->currentLine = ''; 104 $value = $this->cleanup($value); 105 $this->lines = explode("\n", $value); 106 $this->locallySkippedLineNumbers = array(); 107 108 if (null === $this->totalNumberOfLines) { 109 $this->totalNumberOfLines = \count($this->lines); 110 } 111 112 $data = array(); 113 $context = null; 114 $allowOverwrite = false; 115 116 while ($this->moveToNextLine()) { 117 if ($this->isCurrentLineEmpty()) { 118 continue; 119 } 120 121 // tab? 122 if ("\t" === $this->currentLine[0]) { 123 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); 124 } 125 126 $isRef = $mergeNode = false; 127 if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) { 128 if ($context && 'mapping' == $context) { 129 throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine); 130 } 131 $context = 'sequence'; 132 133 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) { 134 $isRef = $matches['ref']; 135 $values['value'] = $matches['value']; 136 } 137 138 // array 139 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { 140 $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap); 141 } else { 142 if (isset($values['leadspaces']) 143 && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches) 144 ) { 145 // this is a compact notation element, add to next block and parse 146 $block = $values['value']; 147 if ($this->isNextLineIndented()) { 148 $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1); 149 } 150 151 $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap); 152 } else { 153 $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); 154 } 155 } 156 if ($isRef) { 157 $this->refs[$isRef] = end($data); 158 } 159 } elseif ( 160 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values) 161 && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], array('"', "'"))) 162 ) { 163 if ($context && 'sequence' == $context) { 164 throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine); 165 } 166 $context = 'mapping'; 167 168 // force correct settings 169 Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); 170 try { 171 $key = Inline::parseScalar($values['key']); 172 } catch (ParseException $e) { 173 $e->setParsedLine($this->getRealCurrentLineNb() + 1); 174 $e->setSnippet($this->currentLine); 175 176 throw $e; 177 } 178 179 // Convert float keys to strings, to avoid being converted to integers by PHP 180 if (\is_float($key)) { 181 $key = (string) $key; 182 } 183 184 if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) { 185 $mergeNode = true; 186 $allowOverwrite = true; 187 if (isset($values['value']) && 0 === strpos($values['value'], '*')) { 188 $refName = substr($values['value'], 1); 189 if (!array_key_exists($refName, $this->refs)) { 190 throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine); 191 } 192 193 $refValue = $this->refs[$refName]; 194 195 if (!\is_array($refValue)) { 196 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); 197 } 198 199 $data += $refValue; // array union 200 } else { 201 if (isset($values['value']) && '' !== $values['value']) { 202 $value = $values['value']; 203 } else { 204 $value = $this->getNextEmbedBlock(); 205 } 206 $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap); 207 208 if (!\is_array($parsed)) { 209 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); 210 } 211 212 if (isset($parsed[0])) { 213 // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes 214 // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier 215 // in the sequence override keys specified in later mapping nodes. 216 foreach ($parsed as $parsedItem) { 217 if (!\is_array($parsedItem)) { 218 throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); 219 } 220 221 $data += $parsedItem; // array union 222 } 223 } else { 224 // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the 225 // current mapping, unless the key already exists in it. 226 $data += $parsed; // array union 227 } 228 } 229 } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) { 230 $isRef = $matches['ref']; 231 $values['value'] = $matches['value']; 232 } 233 234 if ($mergeNode) { 235 // Merge keys 236 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#') || '<<' === $key) { 237 // hash 238 // if next line is less indented or equal, then it means that the current value is null 239 if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { 240 // Spec: Keys MUST be unique; first one wins. 241 // But overwriting is allowed when a merge node is used in current block. 242 if ($allowOverwrite || !isset($data[$key])) { 243 $data[$key] = null; 244 } 245 } else { 246 $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap); 247 248 if ('<<' === $key) { 249 $this->refs[$refMatches['ref']] = $value; 250 $data += $value; 251 } elseif ($allowOverwrite || !isset($data[$key])) { 252 // Spec: Keys MUST be unique; first one wins. 253 // But overwriting is allowed when a merge node is used in current block. 254 $data[$key] = $value; 255 } 256 } 257 } else { 258 $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); 259 // Spec: Keys MUST be unique; first one wins. 260 // But overwriting is allowed when a merge node is used in current block. 261 if ($allowOverwrite || !isset($data[$key])) { 262 $data[$key] = $value; 263 } 264 } 265 if ($isRef) { 266 $this->refs[$isRef] = $data[$key]; 267 } 268 } else { 269 // multiple documents are not supported 270 if ('---' === $this->currentLine) { 271 throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine); 272 } 273 274 // 1-liner optionally followed by newline(s) 275 if (\is_string($value) && $this->lines[0] === trim($value)) { 276 try { 277 $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); 278 } catch (ParseException $e) { 279 $e->setParsedLine($this->getRealCurrentLineNb() + 1); 280 $e->setSnippet($this->currentLine); 281 282 throw $e; 283 } 284 285 return $value; 286 } 287 288 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine); 289 } 290 } 291 292 if ($objectForMap && !\is_object($data) && 'mapping' === $context) { 293 $object = new \stdClass(); 294 295 foreach ($data as $key => $value) { 296 $object->$key = $value; 297 } 298 299 $data = $object; 300 } 301 302 return empty($data) ? null : $data; 303 } 304 305 private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap) 306 { 307 $skippedLineNumbers = $this->skippedLineNumbers; 308 309 foreach ($this->locallySkippedLineNumbers as $lineNumber) { 310 if ($lineNumber < $offset) { 311 continue; 312 } 313 314 $skippedLineNumbers[] = $lineNumber; 315 } 316 317 $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers); 318 $parser->refs = &$this->refs; 319 320 return $parser->doParse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap); 321 } 322 323 /** 324 * Returns the current line number (takes the offset into account). 325 * 326 * @return int The current line number 327 */ 328 private function getRealCurrentLineNb() 329 { 330 $realCurrentLineNumber = $this->currentLineNb + $this->offset; 331 332 foreach ($this->skippedLineNumbers as $skippedLineNumber) { 333 if ($skippedLineNumber > $realCurrentLineNumber) { 334 break; 335 } 336 337 ++$realCurrentLineNumber; 338 } 339 340 return $realCurrentLineNumber; 341 } 342 343 /** 344 * Returns the current line indentation. 345 * 346 * @return int The current line indentation 347 */ 348 private function getCurrentLineIndentation() 349 { 350 return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' ')); 351 } 352 353 /** 354 * Returns the next embed block of YAML. 355 * 356 * @param int $indentation The indent level at which the block is to be read, or null for default 357 * @param bool $inSequence True if the enclosing data structure is a sequence 358 * 359 * @return string A YAML string 360 * 361 * @throws ParseException When indentation problem are detected 362 */ 363 private function getNextEmbedBlock($indentation = null, $inSequence = false) 364 { 365 $oldLineIndentation = $this->getCurrentLineIndentation(); 366 $blockScalarIndentations = array(); 367 368 if ($this->isBlockScalarHeader()) { 369 $blockScalarIndentations[] = $this->getCurrentLineIndentation(); 370 } 371 372 if (!$this->moveToNextLine()) { 373 return; 374 } 375 376 if (null === $indentation) { 377 $newIndent = $this->getCurrentLineIndentation(); 378 379 $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem(); 380 381 if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { 382 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); 383 } 384 } else { 385 $newIndent = $indentation; 386 } 387 388 $data = array(); 389 if ($this->getCurrentLineIndentation() >= $newIndent) { 390 $data[] = substr($this->currentLine, $newIndent); 391 } else { 392 $this->moveToPreviousLine(); 393 394 return; 395 } 396 397 if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) { 398 // the previous line contained a dash but no item content, this line is a sequence item with the same indentation 399 // and therefore no nested list or mapping 400 $this->moveToPreviousLine(); 401 402 return; 403 } 404 405 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); 406 407 if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) { 408 $blockScalarIndentations[] = $this->getCurrentLineIndentation(); 409 } 410 411 $previousLineIndentation = $this->getCurrentLineIndentation(); 412 413 while ($this->moveToNextLine()) { 414 $indent = $this->getCurrentLineIndentation(); 415 416 // terminate all block scalars that are more indented than the current line 417 if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && '' !== trim($this->currentLine)) { 418 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) { 419 if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) { 420 unset($blockScalarIndentations[$key]); 421 } 422 } 423 } 424 425 if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) { 426 $blockScalarIndentations[] = $this->getCurrentLineIndentation(); 427 } 428 429 $previousLineIndentation = $indent; 430 431 if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) { 432 $this->moveToPreviousLine(); 433 break; 434 } 435 436 if ($this->isCurrentLineBlank()) { 437 $data[] = substr($this->currentLine, $newIndent); 438 continue; 439 } 440 441 // we ignore "comment" lines only when we are not inside a scalar block 442 if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) { 443 // remember ignored comment lines (they are used later in nested 444 // parser calls to determine real line numbers) 445 // 446 // CAUTION: beware to not populate the global property here as it 447 // will otherwise influence the getRealCurrentLineNb() call here 448 // for consecutive comment lines and subsequent embedded blocks 449 $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb(); 450 451 continue; 452 } 453 454 if ($indent >= $newIndent) { 455 $data[] = substr($this->currentLine, $newIndent); 456 } elseif (0 == $indent) { 457 $this->moveToPreviousLine(); 458 459 break; 460 } else { 461 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); 462 } 463 } 464 465 return implode("\n", $data); 466 } 467 468 /** 469 * Moves the parser to the next line. 470 * 471 * @return bool 472 */ 473 private function moveToNextLine() 474 { 475 if ($this->currentLineNb >= \count($this->lines) - 1) { 476 return false; 477 } 478 479 $this->currentLine = $this->lines[++$this->currentLineNb]; 480 481 return true; 482 } 483 484 /** 485 * Moves the parser to the previous line. 486 * 487 * @return bool 488 */ 489 private function moveToPreviousLine() 490 { 491 if ($this->currentLineNb < 1) { 492 return false; 493 } 494 495 $this->currentLine = $this->lines[--$this->currentLineNb]; 496 497 return true; 498 } 499 500 /** 501 * Parses a YAML value. 502 * 503 * @param string $value A YAML value 504 * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise 505 * @param bool $objectSupport True if object support is enabled, false otherwise 506 * @param bool $objectForMap True if maps should return a stdClass instead of array() 507 * @param string $context The parser context (either sequence or mapping) 508 * 509 * @return mixed A PHP value 510 * 511 * @throws ParseException When reference does not exist 512 */ 513 private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context) 514 { 515 if (0 === strpos($value, '*')) { 516 if (false !== $pos = strpos($value, '#')) { 517 $value = substr($value, 1, $pos - 2); 518 } else { 519 $value = substr($value, 1); 520 } 521 522 if (!array_key_exists($value, $this->refs)) { 523 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine); 524 } 525 526 return $this->refs[$value]; 527 } 528 529 if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { 530 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; 531 532 return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers)); 533 } 534 535 try { 536 $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); 537 538 if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { 539 @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED); 540 541 // to be thrown in 3.0 542 // throw new ParseException('A colon cannot be used in an unquoted mapping value.'); 543 } 544 545 return $parsedValue; 546 } catch (ParseException $e) { 547 $e->setParsedLine($this->getRealCurrentLineNb() + 1); 548 $e->setSnippet($this->currentLine); 549 550 throw $e; 551 } 552 } 553 554 /** 555 * Parses a block scalar. 556 * 557 * @param string $style The style indicator that was used to begin this block scalar (| or >) 558 * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -) 559 * @param int $indentation The indentation indicator that was used to begin this block scalar 560 * 561 * @return string The text value 562 */ 563 private function parseBlockScalar($style, $chomping = '', $indentation = 0) 564 { 565 $notEOF = $this->moveToNextLine(); 566 if (!$notEOF) { 567 return ''; 568 } 569 570 $isCurrentLineBlank = $this->isCurrentLineBlank(); 571 $blockLines = array(); 572 573 // leading blank lines are consumed before determining indentation 574 while ($notEOF && $isCurrentLineBlank) { 575 // newline only if not EOF 576 if ($notEOF = $this->moveToNextLine()) { 577 $blockLines[] = ''; 578 $isCurrentLineBlank = $this->isCurrentLineBlank(); 579 } 580 } 581 582 // determine indentation if not specified 583 if (0 === $indentation) { 584 if (self::preg_match('/^ +/', $this->currentLine, $matches)) { 585 $indentation = \strlen($matches[0]); 586 } 587 } 588 589 if ($indentation > 0) { 590 $pattern = sprintf('/^ {%d}(.*)$/', $indentation); 591 592 while ( 593 $notEOF && ( 594 $isCurrentLineBlank || 595 self::preg_match($pattern, $this->currentLine, $matches) 596 ) 597 ) { 598 if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) { 599 $blockLines[] = substr($this->currentLine, $indentation); 600 } elseif ($isCurrentLineBlank) { 601 $blockLines[] = ''; 602 } else { 603 $blockLines[] = $matches[1]; 604 } 605 606 // newline only if not EOF 607 if ($notEOF = $this->moveToNextLine()) { 608 $isCurrentLineBlank = $this->isCurrentLineBlank(); 609 } 610 } 611 } elseif ($notEOF) { 612 $blockLines[] = ''; 613 } 614 615 if ($notEOF) { 616 $blockLines[] = ''; 617 $this->moveToPreviousLine(); 618 } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) { 619 $blockLines[] = ''; 620 } 621 622 // folded style 623 if ('>' === $style) { 624 $text = ''; 625 $previousLineIndented = false; 626 $previousLineBlank = false; 627 628 for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) { 629 if ('' === $blockLines[$i]) { 630 $text .= "\n"; 631 $previousLineIndented = false; 632 $previousLineBlank = true; 633 } elseif (' ' === $blockLines[$i][0]) { 634 $text .= "\n".$blockLines[$i]; 635 $previousLineIndented = true; 636 $previousLineBlank = false; 637 } elseif ($previousLineIndented) { 638 $text .= "\n".$blockLines[$i]; 639 $previousLineIndented = false; 640 $previousLineBlank = false; 641 } elseif ($previousLineBlank || 0 === $i) { 642 $text .= $blockLines[$i]; 643 $previousLineIndented = false; 644 $previousLineBlank = false; 645 } else { 646 $text .= ' '.$blockLines[$i]; 647 $previousLineIndented = false; 648 $previousLineBlank = false; 649 } 650 } 651 } else { 652 $text = implode("\n", $blockLines); 653 } 654 655 // deal with trailing newlines 656 if ('' === $chomping) { 657 $text = preg_replace('/\n+$/', "\n", $text); 658 } elseif ('-' === $chomping) { 659 $text = preg_replace('/\n+$/', '', $text); 660 } 661 662 return $text; 663 } 664 665 /** 666 * Returns true if the next line is indented. 667 * 668 * @return bool Returns true if the next line is indented, false otherwise 669 */ 670 private function isNextLineIndented() 671 { 672 $currentIndentation = $this->getCurrentLineIndentation(); 673 $EOF = !$this->moveToNextLine(); 674 675 while (!$EOF && $this->isCurrentLineEmpty()) { 676 $EOF = !$this->moveToNextLine(); 677 } 678 679 if ($EOF) { 680 return false; 681 } 682 683 $ret = $this->getCurrentLineIndentation() > $currentIndentation; 684 685 $this->moveToPreviousLine(); 686 687 return $ret; 688 } 689 690 /** 691 * Returns true if the current line is blank or if it is a comment line. 692 * 693 * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise 694 */ 695 private function isCurrentLineEmpty() 696 { 697 return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); 698 } 699 700 /** 701 * Returns true if the current line is blank. 702 * 703 * @return bool Returns true if the current line is blank, false otherwise 704 */ 705 private function isCurrentLineBlank() 706 { 707 return '' == trim($this->currentLine, ' '); 708 } 709 710 /** 711 * Returns true if the current line is a comment line. 712 * 713 * @return bool Returns true if the current line is a comment line, false otherwise 714 */ 715 private function isCurrentLineComment() 716 { 717 //checking explicitly the first char of the trim is faster than loops or strpos 718 $ltrimmedLine = ltrim($this->currentLine, ' '); 719 720 return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0]; 721 } 722 723 private function isCurrentLineLastLineInDocument() 724 { 725 return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1); 726 } 727 728 /** 729 * Cleanups a YAML string to be parsed. 730 * 731 * @param string $value The input YAML string 732 * 733 * @return string A cleaned up YAML string 734 */ 735 private function cleanup($value) 736 { 737 $value = str_replace(array("\r\n", "\r"), "\n", $value); 738 739 // strip YAML header 740 $count = 0; 741 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); 742 $this->offset += $count; 743 744 // remove leading comments 745 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); 746 if (1 == $count) { 747 // items have been removed, update the offset 748 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); 749 $value = $trimmedValue; 750 } 751 752 // remove start of the document marker (---) 753 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); 754 if (1 == $count) { 755 // items have been removed, update the offset 756 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); 757 $value = $trimmedValue; 758 759 // remove end of the document marker (...) 760 $value = preg_replace('#\.\.\.\s*$#', '', $value); 761 } 762 763 return $value; 764 } 765 766 /** 767 * Returns true if the next line starts unindented collection. 768 * 769 * @return bool Returns true if the next line starts unindented collection, false otherwise 770 */ 771 private function isNextLineUnIndentedCollection() 772 { 773 $currentIndentation = $this->getCurrentLineIndentation(); 774 $notEOF = $this->moveToNextLine(); 775 776 while ($notEOF && $this->isCurrentLineEmpty()) { 777 $notEOF = $this->moveToNextLine(); 778 } 779 780 if (false === $notEOF) { 781 return false; 782 } 783 784 $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem(); 785 786 $this->moveToPreviousLine(); 787 788 return $ret; 789 } 790 791 /** 792 * Returns true if the string is un-indented collection item. 793 * 794 * @return bool Returns true if the string is un-indented collection item, false otherwise 795 */ 796 private function isStringUnIndentedCollectionItem() 797 { 798 return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- '); 799 } 800 801 /** 802 * Tests whether or not the current line is the header of a block scalar. 803 * 804 * @return bool 805 */ 806 private function isBlockScalarHeader() 807 { 808 return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine); 809 } 810 811 /** 812 * A local wrapper for `preg_match` which will throw a ParseException if there 813 * is an internal error in the PCRE engine. 814 * 815 * This avoids us needing to check for "false" every time PCRE is used 816 * in the YAML engine 817 * 818 * @throws ParseException on a PCRE internal error 819 * 820 * @see preg_last_error() 821 * 822 * @internal 823 */ 824 public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) 825 { 826 if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) { 827 switch (preg_last_error()) { 828 case PREG_INTERNAL_ERROR: 829 $error = 'Internal PCRE error.'; 830 break; 831 case PREG_BACKTRACK_LIMIT_ERROR: 832 $error = 'pcre.backtrack_limit reached.'; 833 break; 834 case PREG_RECURSION_LIMIT_ERROR: 835 $error = 'pcre.recursion_limit reached.'; 836 break; 837 case PREG_BAD_UTF8_ERROR: 838 $error = 'Malformed UTF-8 data.'; 839 break; 840 case PREG_BAD_UTF8_OFFSET_ERROR: 841 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; 842 break; 843 default: 844 $error = 'Error.'; 845 } 846 847 throw new ParseException($error); 848 } 849 850 return $ret; 851 } 852} 853