"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/rockettheme/toolbox/Compat/src/Yaml/Parser.php" (1 Sep 2020, 33082 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Parser.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /*
    4  * This file is part of the Symfony package.
    5  *
    6  * (c) Fabien Potencier <fabien@symfony.com>
    7  *
    8  * For the full copyright and license information, please view the LICENSE
    9  * file that was distributed with this source code.
   10  */
   11 
   12 namespace RocketTheme\Toolbox\Compat\Yaml;
   13 
   14 use RocketTheme\Toolbox\Compat\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  */
   21 class 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 }