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