Projekt

Obecné

Profil

Stáhnout (43.3 KB) Statistiky
| Větev: | Revize:
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
use Symfony\Component\Yaml\Tag\TaggedValue;
16

    
17
/**
18
 * Parser parses YAML strings to convert them to PHP arrays.
19
 *
20
 * @author Fabien Potencier <fabien@symfony.com>
21
 */
22
class Parser
23
{
24
    const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
25
    const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
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
    public function __construct()
37
    {
38
        if (func_num_args() > 0) {
39
            @trigger_error(sprintf('The constructor arguments $offset, $totalNumberOfLines, $skippedLineNumbers of %s are deprecated and will be removed in 4.0', self::class), E_USER_DEPRECATED);
40

    
41
            $this->offset = func_get_arg(0);
42
            if (func_num_args() > 1) {
43
                $this->totalNumberOfLines = func_get_arg(1);
44
            }
45
            if (func_num_args() > 2) {
46
                $this->skippedLineNumbers = func_get_arg(2);
47
            }
48
        }
49
    }
50

    
51
    /**
52
     * Parses a YAML string to a PHP value.
53
     *
54
     * @param string $value A YAML string
55
     * @param int    $flags A bit field of PARSE_* constants to customize the YAML parser behavior
56
     *
57
     * @return mixed A PHP value
58
     *
59
     * @throws ParseException If the YAML is not valid
60
     */
61
    public function parse($value, $flags = 0)
62
    {
63
        if (is_bool($flags)) {
64
            @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
65

    
66
            if ($flags) {
67
                $flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
68
            } else {
69
                $flags = 0;
70
            }
71
        }
72

    
73
        if (func_num_args() >= 3) {
74
            @trigger_error('Passing a boolean flag to toggle object support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.', E_USER_DEPRECATED);
75

    
76
            if (func_get_arg(2)) {
77
                $flags |= Yaml::PARSE_OBJECT;
78
            }
79
        }
80

    
81
        if (func_num_args() >= 4) {
82
            @trigger_error('Passing a boolean flag to toggle object for map support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.', E_USER_DEPRECATED);
83

    
84
            if (func_get_arg(3)) {
85
                $flags |= Yaml::PARSE_OBJECT_FOR_MAP;
86
            }
87
        }
88

    
89
        if (false === preg_match('//u', $value)) {
90
            throw new ParseException('The YAML value does not appear to be valid UTF-8.');
91
        }
92

    
93
        $this->refs = array();
94

    
95
        $mbEncoding = null;
96
        $e = null;
97
        $data = null;
98

    
99
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
100
            $mbEncoding = mb_internal_encoding();
101
            mb_internal_encoding('UTF-8');
102
        }
103

    
104
        try {
105
            $data = $this->doParse($value, $flags);
106
        } catch (\Exception $e) {
107
        } catch (\Throwable $e) {
108
        }
109

    
110
        if (null !== $mbEncoding) {
111
            mb_internal_encoding($mbEncoding);
112
        }
113

    
114
        $this->lines = array();
115
        $this->currentLine = '';
116
        $this->refs = array();
117
        $this->skippedLineNumbers = array();
118
        $this->locallySkippedLineNumbers = array();
119

    
120
        if (null !== $e) {
121
            throw $e;
122
        }
123

    
124
        return $data;
125
    }
126

    
127
    private function doParse($value, $flags)
128
    {
129
        $this->currentLineNb = -1;
130
        $this->currentLine = '';
131
        $value = $this->cleanup($value);
132
        $this->lines = explode("\n", $value);
133
        $this->locallySkippedLineNumbers = array();
134

    
135
        if (null === $this->totalNumberOfLines) {
136
            $this->totalNumberOfLines = count($this->lines);
137
        }
138

    
139
        if (!$this->moveToNextLine()) {
140
            return null;
141
        }
142

    
143
        $data = array();
144
        $context = null;
145
        $allowOverwrite = false;
146

    
147
        while ($this->isCurrentLineEmpty()) {
148
            if (!$this->moveToNextLine()) {
149
                return null;
150
            }
151
        }
152

    
153
        // Resolves the tag and returns if end of the document
154
        if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
155
            return new TaggedValue($tag, '');
156
        }
157

    
158
        do {
159
            if ($this->isCurrentLineEmpty()) {
160
                continue;
161
            }
162

    
163
            // tab?
164
            if ("\t" === $this->currentLine[0]) {
165
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
166
            }
167

    
168
            $isRef = $mergeNode = false;
169
            if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
170
                if ($context && 'mapping' == $context) {
171
                    throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
172
                }
173
                $context = 'sequence';
174

    
175
                if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
176
                    $isRef = $matches['ref'];
177
                    $values['value'] = $matches['value'];
178
                }
179

    
180
                if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
181
                    @trigger_error(sprintf('Starting an unquoted string with a question mark followed by a space is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
182
                }
183

    
184
                // array
185
                if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
186
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags);
187
                } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
188
                    $data[] = new TaggedValue(
189
                        $subTag,
190
                        $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
191
                    );
192
                } else {
193
                    if (isset($values['leadspaces'])
194
                        && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
195
                    ) {
196
                        // this is a compact notation element, add to next block and parse
197
                        $block = $values['value'];
198
                        if ($this->isNextLineIndented()) {
199
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
200
                        }
201

    
202
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
203
                    } else {
204
                        $data[] = $this->parseValue($values['value'], $flags, $context);
205
                    }
206
                }
207
                if ($isRef) {
208
                    $this->refs[$isRef] = end($data);
209
                }
210
            } elseif (
211
                self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?(?:![^\s]++\s++)?[^ \'"\[\{!].*?) *\:(\s++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
212
                && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
213
            ) {
214
                if ($context && 'sequence' == $context) {
215
                    throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
216
                }
217
                $context = 'mapping';
218

    
219
                // force correct settings
220
                Inline::parse(null, $flags, $this->refs);
221
                try {
222
                    Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
223
                    $i = 0;
224
                    $evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS & $flags);
225

    
226
                    // constants in key will be evaluated anyway
227
                    if (isset($values['key'][0]) && '!' === $values['key'][0] && Yaml::PARSE_CONSTANT & $flags) {
228
                        $evaluateKey = true;
229
                    }
230

    
231
                    $key = Inline::parseScalar($values['key'], 0, null, $i, $evaluateKey);
232
                } catch (ParseException $e) {
233
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
234
                    $e->setSnippet($this->currentLine);
235

    
236
                    throw $e;
237
                }
238

    
239
                if (!(Yaml::PARSE_KEYS_AS_STRINGS & $flags) && !is_string($key) && !is_int($key)) {
240
                    $keyType = is_numeric($key) ? 'numeric key' : 'non-string key';
241
                    @trigger_error(sprintf('Implicit casting of %s to string is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Quote your evaluable mapping keys instead on line %d.', $keyType, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
242
                }
243

    
244
                // Convert float keys to strings, to avoid being converted to integers by PHP
245
                if (is_float($key)) {
246
                    $key = (string) $key;
247
                }
248

    
249
                if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
250
                    $mergeNode = true;
251
                    $allowOverwrite = true;
252
                    if (isset($values['value'][0]) && '*' === $values['value'][0]) {
253
                        $refName = substr(rtrim($values['value']), 1);
254
                        if (!array_key_exists($refName, $this->refs)) {
255
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
256
                        }
257

    
258
                        $refValue = $this->refs[$refName];
259

    
260
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
261
                            $refValue = (array) $refValue;
262
                        }
263

    
264
                        if (!is_array($refValue)) {
265
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
266
                        }
267

    
268
                        $data += $refValue; // array union
269
                    } else {
270
                        if (isset($values['value']) && '' !== $values['value']) {
271
                            $value = $values['value'];
272
                        } else {
273
                            $value = $this->getNextEmbedBlock();
274
                        }
275
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
276

    
277
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
278
                            $parsed = (array) $parsed;
279
                        }
280

    
281
                        if (!is_array($parsed)) {
282
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
283
                        }
284

    
285
                        if (isset($parsed[0])) {
286
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
287
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
288
                            // in the sequence override keys specified in later mapping nodes.
289
                            foreach ($parsed as $parsedItem) {
290
                                if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
291
                                    $parsedItem = (array) $parsedItem;
292
                                }
293

    
294
                                if (!is_array($parsedItem)) {
295
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
296
                                }
297

    
298
                                $data += $parsedItem; // array union
299
                            }
300
                        } else {
301
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
302
                            // current mapping, unless the key already exists in it.
303
                            $data += $parsed; // array union
304
                        }
305
                    }
306
                } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u', $values['value'], $matches)) {
307
                    $isRef = $matches['ref'];
308
                    $values['value'] = $matches['value'];
309
                }
310

    
311
                $subTag = null;
312
                if ($mergeNode) {
313
                    // Merge keys
314
                } elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
315
                    // hash
316
                    // if next line is less indented or equal, then it means that the current value is null
317
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
318
                        // Spec: Keys MUST be unique; first one wins.
319
                        // But overwriting is allowed when a merge node is used in current block.
320
                        if ($allowOverwrite || !isset($data[$key])) {
321
                            if (null !== $subTag) {
322
                                $data[$key] = new TaggedValue($subTag, '');
323
                            } else {
324
                                $data[$key] = null;
325
                            }
326
                        } else {
327
                            @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
328
                        }
329
                    } else {
330
                        // remember the parsed line number here in case we need it to provide some contexts in error messages below
331
                        $realCurrentLineNbKey = $this->getRealCurrentLineNb();
332
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
333
                        if ('<<' === $key) {
334
                            $this->refs[$refMatches['ref']] = $value;
335

    
336
                            if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
337
                                $value = (array) $value;
338
                            }
339

    
340
                            $data += $value;
341
                        } elseif ($allowOverwrite || !isset($data[$key])) {
342
                            // Spec: Keys MUST be unique; first one wins.
343
                            // But overwriting is allowed when a merge node is used in current block.
344
                            if (null !== $subTag) {
345
                                $data[$key] = new TaggedValue($subTag, $value);
346
                            } else {
347
                                $data[$key] = $value;
348
                            }
349
                        } else {
350
                            @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $key, $realCurrentLineNbKey + 1), E_USER_DEPRECATED);
351
                        }
352
                    }
353
                } else {
354
                    $value = $this->parseValue(rtrim($values['value']), $flags, $context);
355
                    // Spec: Keys MUST be unique; first one wins.
356
                    // But overwriting is allowed when a merge node is used in current block.
357
                    if ($allowOverwrite || !isset($data[$key])) {
358
                        $data[$key] = $value;
359
                    } else {
360
                        @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
361
                    }
362
                }
363
                if ($isRef) {
364
                    $this->refs[$isRef] = $data[$key];
365
                }
366
            } else {
367
                // multiple documents are not supported
368
                if ('---' === $this->currentLine) {
369
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
370
                }
371

    
372
                if (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1]) {
373
                    @trigger_error(sprintf('Starting an unquoted string with a question mark followed by a space is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0 on line %d.', $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
374
                }
375

    
376
                // 1-liner optionally followed by newline(s)
377
                if (is_string($value) && $this->lines[0] === trim($value)) {
378
                    try {
379
                        Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
380
                        $value = Inline::parse($this->lines[0], $flags, $this->refs);
381
                    } catch (ParseException $e) {
382
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
383
                        $e->setSnippet($this->currentLine);
384

    
385
                        throw $e;
386
                    }
387

    
388
                    return $value;
389
                }
390

    
391
                // try to parse the value as a multi-line string as a last resort
392
                if (0 === $this->currentLineNb) {
393
                    $parseError = false;
394
                    $previousLineWasNewline = false;
395
                    $previousLineWasTerminatedWithBackslash = false;
396
                    $value = '';
397

    
398
                    foreach ($this->lines as $line) {
399
                        try {
400
                            if (isset($line[0]) && ('"' === $line[0] || "'" === $line[0])) {
401
                                $parsedLine = $line;
402
                            } else {
403
                                $parsedLine = Inline::parse($line, $flags, $this->refs);
404
                            }
405

    
406
                            if (!is_string($parsedLine)) {
407
                                $parseError = true;
408
                                break;
409
                            }
410

    
411
                            if ('' === trim($parsedLine)) {
412
                                $value .= "\n";
413
                            } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
414
                                $value .= ' ';
415
                            }
416

    
417
                            if ('' !== trim($parsedLine) && '\\' === substr($parsedLine, -1)) {
418
                                $value .= ltrim(substr($parsedLine, 0, -1));
419
                            } elseif ('' !== trim($parsedLine)) {
420
                                $value .= trim($parsedLine);
421
                            }
422

    
423
                            if ('' === trim($parsedLine)) {
424
                                $previousLineWasNewline = true;
425
                                $previousLineWasTerminatedWithBackslash = false;
426
                            } elseif ('\\' === substr($parsedLine, -1)) {
427
                                $previousLineWasNewline = false;
428
                                $previousLineWasTerminatedWithBackslash = true;
429
                            } else {
430
                                $previousLineWasNewline = false;
431
                                $previousLineWasTerminatedWithBackslash = false;
432
                            }
433
                        } catch (ParseException $e) {
434
                            $parseError = true;
435
                            break;
436
                        }
437
                    }
438

    
439
                    if (!$parseError) {
440
                        return Inline::parse(trim($value));
441
                    }
442
                }
443

    
444
                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
445
            }
446
        } while ($this->moveToNextLine());
447

    
448
        if (null !== $tag) {
449
            $data = new TaggedValue($tag, $data);
450
        }
451

    
452
        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !is_object($data) && 'mapping' === $context) {
453
            $object = new \stdClass();
454

    
455
            foreach ($data as $key => $value) {
456
                $object->$key = $value;
457
            }
458

    
459
            $data = $object;
460
        }
461

    
462
        return empty($data) ? null : $data;
463
    }
464

    
465
    private function parseBlock($offset, $yaml, $flags)
466
    {
467
        $skippedLineNumbers = $this->skippedLineNumbers;
468

    
469
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
470
            if ($lineNumber < $offset) {
471
                continue;
472
            }
473

    
474
            $skippedLineNumbers[] = $lineNumber;
475
        }
476

    
477
        $parser = new self();
478
        $parser->offset = $offset;
479
        $parser->totalNumberOfLines = $this->totalNumberOfLines;
480
        $parser->skippedLineNumbers = $skippedLineNumbers;
481
        $parser->refs = &$this->refs;
482

    
483
        return $parser->doParse($yaml, $flags);
484
    }
485

    
486
    /**
487
     * Returns the current line number (takes the offset into account).
488
     *
489
     * @return int The current line number
490
     */
491
    private function getRealCurrentLineNb()
492
    {
493
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
494

    
495
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
496
            if ($skippedLineNumber > $realCurrentLineNumber) {
497
                break;
498
            }
499

    
500
            ++$realCurrentLineNumber;
501
        }
502

    
503
        return $realCurrentLineNumber;
504
    }
505

    
506
    /**
507
     * Returns the current line indentation.
508
     *
509
     * @return int The current line indentation
510
     */
511
    private function getCurrentLineIndentation()
512
    {
513
        return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
514
    }
515

    
516
    /**
517
     * Returns the next embed block of YAML.
518
     *
519
     * @param int  $indentation The indent level at which the block is to be read, or null for default
520
     * @param bool $inSequence  True if the enclosing data structure is a sequence
521
     *
522
     * @return string A YAML string
523
     *
524
     * @throws ParseException When indentation problem are detected
525
     */
526
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
527
    {
528
        $oldLineIndentation = $this->getCurrentLineIndentation();
529
        $blockScalarIndentations = array();
530

    
531
        if ($this->isBlockScalarHeader()) {
532
            $blockScalarIndentations[] = $oldLineIndentation;
533
        }
534

    
535
        if (!$this->moveToNextLine()) {
536
            return;
537
        }
538

    
539
        if (null === $indentation) {
540
            $newIndent = null;
541
            $movements = 0;
542

    
543
            do {
544
                $EOF = false;
545

    
546
                // empty and comment-like lines do not influence the indentation depth
547
                if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
548
                    $EOF = !$this->moveToNextLine();
549

    
550
                    if (!$EOF) {
551
                        ++$movements;
552
                    }
553
                } else {
554
                    $newIndent = $this->getCurrentLineIndentation();
555
                }
556
            } while (!$EOF && null === $newIndent);
557

    
558
            for ($i = 0; $i < $movements; ++$i) {
559
                $this->moveToPreviousLine();
560
            }
561

    
562
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
563

    
564
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
565
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
566
            }
567
        } else {
568
            $newIndent = $indentation;
569
        }
570

    
571
        $data = array();
572
        if ($this->getCurrentLineIndentation() >= $newIndent) {
573
            $data[] = substr($this->currentLine, $newIndent);
574
        } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
575
            $data[] = $this->currentLine;
576
        } else {
577
            $this->moveToPreviousLine();
578

    
579
            return;
580
        }
581

    
582
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
583
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
584
            // and therefore no nested list or mapping
585
            $this->moveToPreviousLine();
586

    
587
            return;
588
        }
589

    
590
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
591

    
592
        if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
593
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
594
        }
595

    
596
        $previousLineIndentation = $this->getCurrentLineIndentation();
597

    
598
        while ($this->moveToNextLine()) {
599
            $indent = $this->getCurrentLineIndentation();
600

    
601
            // terminate all block scalars that are more indented than the current line
602
            if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && '' !== trim($this->currentLine)) {
603
                foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
604
                    if ($blockScalarIndentation >= $indent) {
605
                        unset($blockScalarIndentations[$key]);
606
                    }
607
                }
608
            }
609

    
610
            if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
611
                $blockScalarIndentations[] = $indent;
612
            }
613

    
614
            $previousLineIndentation = $indent;
615

    
616
            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
617
                $this->moveToPreviousLine();
618
                break;
619
            }
620

    
621
            if ($this->isCurrentLineBlank()) {
622
                $data[] = substr($this->currentLine, $newIndent);
623
                continue;
624
            }
625

    
626
            if ($indent >= $newIndent) {
627
                $data[] = substr($this->currentLine, $newIndent);
628
            } elseif ($this->isCurrentLineComment()) {
629
                $data[] = $this->currentLine;
630
            } elseif (0 == $indent) {
631
                $this->moveToPreviousLine();
632

    
633
                break;
634
            } else {
635
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
636
            }
637
        }
638

    
639
        return implode("\n", $data);
640
    }
641

    
642
    /**
643
     * Moves the parser to the next line.
644
     *
645
     * @return bool
646
     */
647
    private function moveToNextLine()
648
    {
649
        if ($this->currentLineNb >= count($this->lines) - 1) {
650
            return false;
651
        }
652

    
653
        $this->currentLine = $this->lines[++$this->currentLineNb];
654

    
655
        return true;
656
    }
657

    
658
    /**
659
     * Moves the parser to the previous line.
660
     *
661
     * @return bool
662
     */
663
    private function moveToPreviousLine()
664
    {
665
        if ($this->currentLineNb < 1) {
666
            return false;
667
        }
668

    
669
        $this->currentLine = $this->lines[--$this->currentLineNb];
670

    
671
        return true;
672
    }
673

    
674
    /**
675
     * Parses a YAML value.
676
     *
677
     * @param string $value   A YAML value
678
     * @param int    $flags   A bit field of PARSE_* constants to customize the YAML parser behavior
679
     * @param string $context The parser context (either sequence or mapping)
680
     *
681
     * @return mixed A PHP value
682
     *
683
     * @throws ParseException When reference does not exist
684
     */
685
    private function parseValue($value, $flags, $context)
686
    {
687
        if (0 === strpos($value, '*')) {
688
            if (false !== $pos = strpos($value, '#')) {
689
                $value = substr($value, 1, $pos - 2);
690
            } else {
691
                $value = substr($value, 1);
692
            }
693

    
694
            if (!array_key_exists($value, $this->refs)) {
695
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
696
            }
697

    
698
            return $this->refs[$value];
699
        }
700

    
701
        if (self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
702
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
703

    
704
            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
705

    
706
            if ('' !== $matches['tag']) {
707
                if ('!!binary' === $matches['tag']) {
708
                    return Inline::evaluateBinaryScalar($data);
709
                } elseif ('!' !== $matches['tag']) {
710
                    @trigger_error(sprintf('Using the custom tag "%s" for the value "%s" is deprecated since Symfony 3.3. It will be replaced by an instance of %s in 4.0 on line %d.', $matches['tag'], $data, TaggedValue::class, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
711
                }
712
            }
713

    
714
            return $data;
715
        }
716

    
717
        try {
718
            $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
719

    
720
            // do not take following lines into account when the current line is a quoted single line value
721
            if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) {
722
                return Inline::parse($value, $flags, $this->refs);
723
            }
724

    
725
            $lines = array();
726

    
727
            while ($this->moveToNextLine()) {
728
                // unquoted strings end before the first unindented line
729
                if (null === $quotation && 0 === $this->getCurrentLineIndentation()) {
730
                    $this->moveToPreviousLine();
731

    
732
                    break;
733
                }
734

    
735
                $lines[] = trim($this->currentLine);
736

    
737
                // quoted string values end with a line that is terminated with the quotation character
738
                if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
739
                    break;
740
                }
741
            }
742

    
743
            for ($i = 0, $linesCount = count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
744
                if ('' === $lines[$i]) {
745
                    $value .= "\n";
746
                    $previousLineBlank = true;
747
                } elseif ($previousLineBlank) {
748
                    $value .= $lines[$i];
749
                    $previousLineBlank = false;
750
                } else {
751
                    $value .= ' '.$lines[$i];
752
                    $previousLineBlank = false;
753
                }
754
            }
755

    
756
            Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
757
            $parsedValue = Inline::parse($value, $flags, $this->refs);
758

    
759
            if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
760
                throw new ParseException('A colon cannot be used in an unquoted mapping value.');
761
            }
762

    
763
            return $parsedValue;
764
        } catch (ParseException $e) {
765
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
766
            $e->setSnippet($this->currentLine);
767

    
768
            throw $e;
769
        }
770
    }
771

    
772
    /**
773
     * Parses a block scalar.
774
     *
775
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
776
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
777
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
778
     *
779
     * @return string The text value
780
     */
781
    private function parseBlockScalar($style, $chomping = '', $indentation = 0)
782
    {
783
        $notEOF = $this->moveToNextLine();
784
        if (!$notEOF) {
785
            return '';
786
        }
787

    
788
        $isCurrentLineBlank = $this->isCurrentLineBlank();
789
        $blockLines = array();
790

    
791
        // leading blank lines are consumed before determining indentation
792
        while ($notEOF && $isCurrentLineBlank) {
793
            // newline only if not EOF
794
            if ($notEOF = $this->moveToNextLine()) {
795
                $blockLines[] = '';
796
                $isCurrentLineBlank = $this->isCurrentLineBlank();
797
            }
798
        }
799

    
800
        // determine indentation if not specified
801
        if (0 === $indentation) {
802
            if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
803
                $indentation = strlen($matches[0]);
804
            }
805
        }
806

    
807
        if ($indentation > 0) {
808
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
809

    
810
            while (
811
                $notEOF && (
812
                    $isCurrentLineBlank ||
813
                    self::preg_match($pattern, $this->currentLine, $matches)
814
                )
815
            ) {
816
                if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
817
                    $blockLines[] = substr($this->currentLine, $indentation);
818
                } elseif ($isCurrentLineBlank) {
819
                    $blockLines[] = '';
820
                } else {
821
                    $blockLines[] = $matches[1];
822
                }
823

    
824
                // newline only if not EOF
825
                if ($notEOF = $this->moveToNextLine()) {
826
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
827
                }
828
            }
829
        } elseif ($notEOF) {
830
            $blockLines[] = '';
831
        }
832

    
833
        if ($notEOF) {
834
            $blockLines[] = '';
835
            $this->moveToPreviousLine();
836
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
837
            $blockLines[] = '';
838
        }
839

    
840
        // folded style
841
        if ('>' === $style) {
842
            $text = '';
843
            $previousLineIndented = false;
844
            $previousLineBlank = false;
845

    
846
            for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
847
                if ('' === $blockLines[$i]) {
848
                    $text .= "\n";
849
                    $previousLineIndented = false;
850
                    $previousLineBlank = true;
851
                } elseif (' ' === $blockLines[$i][0]) {
852
                    $text .= "\n".$blockLines[$i];
853
                    $previousLineIndented = true;
854
                    $previousLineBlank = false;
855
                } elseif ($previousLineIndented) {
856
                    $text .= "\n".$blockLines[$i];
857
                    $previousLineIndented = false;
858
                    $previousLineBlank = false;
859
                } elseif ($previousLineBlank || 0 === $i) {
860
                    $text .= $blockLines[$i];
861
                    $previousLineIndented = false;
862
                    $previousLineBlank = false;
863
                } else {
864
                    $text .= ' '.$blockLines[$i];
865
                    $previousLineIndented = false;
866
                    $previousLineBlank = false;
867
                }
868
            }
869
        } else {
870
            $text = implode("\n", $blockLines);
871
        }
872

    
873
        // deal with trailing newlines
874
        if ('' === $chomping) {
875
            $text = preg_replace('/\n+$/', "\n", $text);
876
        } elseif ('-' === $chomping) {
877
            $text = preg_replace('/\n+$/', '', $text);
878
        }
879

    
880
        return $text;
881
    }
882

    
883
    /**
884
     * Returns true if the next line is indented.
885
     *
886
     * @return bool Returns true if the next line is indented, false otherwise
887
     */
888
    private function isNextLineIndented()
889
    {
890
        $currentIndentation = $this->getCurrentLineIndentation();
891
        $movements = 0;
892

    
893
        do {
894
            $EOF = !$this->moveToNextLine();
895

    
896
            if (!$EOF) {
897
                ++$movements;
898
            }
899
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
900

    
901
        if ($EOF) {
902
            return false;
903
        }
904

    
905
        $ret = $this->getCurrentLineIndentation() > $currentIndentation;
906

    
907
        for ($i = 0; $i < $movements; ++$i) {
908
            $this->moveToPreviousLine();
909
        }
910

    
911
        return $ret;
912
    }
913

    
914
    /**
915
     * Returns true if the current line is blank or if it is a comment line.
916
     *
917
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
918
     */
919
    private function isCurrentLineEmpty()
920
    {
921
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
922
    }
923

    
924
    /**
925
     * Returns true if the current line is blank.
926
     *
927
     * @return bool Returns true if the current line is blank, false otherwise
928
     */
929
    private function isCurrentLineBlank()
930
    {
931
        return '' == trim($this->currentLine, ' ');
932
    }
933

    
934
    /**
935
     * Returns true if the current line is a comment line.
936
     *
937
     * @return bool Returns true if the current line is a comment line, false otherwise
938
     */
939
    private function isCurrentLineComment()
940
    {
941
        //checking explicitly the first char of the trim is faster than loops or strpos
942
        $ltrimmedLine = ltrim($this->currentLine, ' ');
943

    
944
        return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
945
    }
946

    
947
    private function isCurrentLineLastLineInDocument()
948
    {
949
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
950
    }
951

    
952
    /**
953
     * Cleanups a YAML string to be parsed.
954
     *
955
     * @param string $value The input YAML string
956
     *
957
     * @return string A cleaned up YAML string
958
     */
959
    private function cleanup($value)
960
    {
961
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
962

    
963
        // strip YAML header
964
        $count = 0;
965
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
966
        $this->offset += $count;
967

    
968
        // remove leading comments
969
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
970
        if (1 === $count) {
971
            // items have been removed, update the offset
972
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
973
            $value = $trimmedValue;
974
        }
975

    
976
        // remove start of the document marker (---)
977
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
978
        if (1 === $count) {
979
            // items have been removed, update the offset
980
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
981
            $value = $trimmedValue;
982

    
983
            // remove end of the document marker (...)
984
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
985
        }
986

    
987
        return $value;
988
    }
989

    
990
    /**
991
     * Returns true if the next line starts unindented collection.
992
     *
993
     * @return bool Returns true if the next line starts unindented collection, false otherwise
994
     */
995
    private function isNextLineUnIndentedCollection()
996
    {
997
        $currentIndentation = $this->getCurrentLineIndentation();
998
        $movements = 0;
999

    
1000
        do {
1001
            $EOF = !$this->moveToNextLine();
1002

    
1003
            if (!$EOF) {
1004
                ++$movements;
1005
            }
1006
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
1007

    
1008
        if ($EOF) {
1009
            return false;
1010
        }
1011

    
1012
        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
1013

    
1014
        for ($i = 0; $i < $movements; ++$i) {
1015
            $this->moveToPreviousLine();
1016
        }
1017

    
1018
        return $ret;
1019
    }
1020

    
1021
    /**
1022
     * Returns true if the string is un-indented collection item.
1023
     *
1024
     * @return bool Returns true if the string is un-indented collection item, false otherwise
1025
     */
1026
    private function isStringUnIndentedCollectionItem()
1027
    {
1028
        return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
1029
    }
1030

    
1031
    /**
1032
     * Tests whether or not the current line is the header of a block scalar.
1033
     *
1034
     * @return bool
1035
     */
1036
    private function isBlockScalarHeader()
1037
    {
1038
        return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
1039
    }
1040

    
1041
    /**
1042
     * A local wrapper for `preg_match` which will throw a ParseException if there
1043
     * is an internal error in the PCRE engine.
1044
     *
1045
     * This avoids us needing to check for "false" every time PCRE is used
1046
     * in the YAML engine
1047
     *
1048
     * @throws ParseException on a PCRE internal error
1049
     *
1050
     * @see preg_last_error()
1051
     *
1052
     * @internal
1053
     */
1054
    public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
1055
    {
1056
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
1057
            switch (preg_last_error()) {
1058
                case PREG_INTERNAL_ERROR:
1059
                    $error = 'Internal PCRE error.';
1060
                    break;
1061
                case PREG_BACKTRACK_LIMIT_ERROR:
1062
                    $error = 'pcre.backtrack_limit reached.';
1063
                    break;
1064
                case PREG_RECURSION_LIMIT_ERROR:
1065
                    $error = 'pcre.recursion_limit reached.';
1066
                    break;
1067
                case PREG_BAD_UTF8_ERROR:
1068
                    $error = 'Malformed UTF-8 data.';
1069
                    break;
1070
                case PREG_BAD_UTF8_OFFSET_ERROR:
1071
                    $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
1072
                    break;
1073
                default:
1074
                    $error = 'Error.';
1075
            }
1076

    
1077
            throw new ParseException($error);
1078
        }
1079

    
1080
        return $ret;
1081
    }
1082

    
1083
    /**
1084
     * Trim the tag on top of the value.
1085
     *
1086
     * Prevent values such as `!foo {quz: bar}` to be considered as
1087
     * a mapping block.
1088
     */
1089
    private function trimTag($value)
1090
    {
1091
        if ('!' === $value[0]) {
1092
            return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
1093
        }
1094

    
1095
        return $value;
1096
    }
1097

    
1098
    private function getLineTag($value, $flags, $nextLineCheck = true)
1099
    {
1100
        if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
1101
            return;
1102
        }
1103

    
1104
        if ($nextLineCheck && !$this->isNextLineIndented()) {
1105
            return;
1106
        }
1107

    
1108
        $tag = substr($matches['tag'], 1);
1109

    
1110
        // Built-in tags
1111
        if ($tag && '!' === $tag[0]) {
1112
            throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag));
1113
        }
1114

    
1115
        if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
1116
            return $tag;
1117
        }
1118

    
1119
        throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']));
1120
    }
1121
}
(7-7/12)