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