1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Symfony package. |
5
|
|
|
* |
6
|
|
|
* (c) Fabien Potencier <[email protected]> |
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 <[email protected]> |
21
|
|
|
* |
22
|
|
|
* @final since version 3.4 |
23
|
|
|
*/ |
24
|
|
|
class Parser |
25
|
|
|
{ |
26
|
|
|
const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)'; |
27
|
|
|
const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?'; |
28
|
|
|
|
29
|
|
|
private $filename; |
30
|
|
|
private $offset = 0; |
31
|
|
|
private $totalNumberOfLines; |
32
|
|
|
private $lines = []; |
33
|
|
|
private $currentLineNb = -1; |
34
|
|
|
private $currentLine = ''; |
35
|
|
|
private $refs = []; |
36
|
|
|
private $skippedLineNumbers = []; |
37
|
|
|
private $locallySkippedLineNumbers = []; |
38
|
|
|
private $refsBeingParsed = []; |
39
|
|
|
|
40
|
|
|
public function __construct() |
41
|
|
|
{ |
42
|
|
|
if (\func_num_args() > 0) { |
43
|
|
|
@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); |
44
|
|
|
|
45
|
|
|
$this->offset = func_get_arg(0); |
46
|
|
|
if (\func_num_args() > 1) { |
47
|
|
|
$this->totalNumberOfLines = func_get_arg(1); |
48
|
|
|
} |
49
|
|
|
if (\func_num_args() > 2) { |
50
|
|
|
$this->skippedLineNumbers = func_get_arg(2); |
51
|
|
|
} |
52
|
|
|
} |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Parses a YAML file into a PHP value. |
57
|
|
|
* |
58
|
|
|
* @param string $filename The path to the YAML file to be parsed |
59
|
|
|
* @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior |
60
|
|
|
* |
61
|
|
|
* @return mixed The YAML converted to a PHP value |
62
|
|
|
* |
63
|
|
|
* @throws ParseException If the file could not be read or the YAML is not valid |
64
|
|
|
*/ |
65
|
|
|
public function parseFile($filename, $flags = 0) |
66
|
|
|
{ |
67
|
|
|
if (!is_file($filename)) { |
68
|
|
|
throw new ParseException(sprintf('File "%s" does not exist.', $filename)); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
if (!is_readable($filename)) { |
72
|
|
|
throw new ParseException(sprintf('File "%s" cannot be read.', $filename)); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$this->filename = $filename; |
76
|
|
|
|
77
|
|
|
try { |
78
|
|
|
return $this->parse(file_get_contents($filename), $flags); |
79
|
|
|
} finally { |
80
|
|
|
$this->filename = null; |
81
|
|
|
} |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Parses a YAML string to a PHP value. |
86
|
|
|
* |
87
|
|
|
* @param string $value A YAML string |
88
|
|
|
* @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior |
89
|
|
|
* |
90
|
|
|
* @return mixed A PHP value |
91
|
|
|
* |
92
|
|
|
* @throws ParseException If the YAML is not valid |
93
|
|
|
*/ |
94
|
|
|
public function parse($value, $flags = 0) |
95
|
|
|
{ |
96
|
|
|
if (\is_bool($flags)) { |
97
|
|
|
@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); |
98
|
|
|
|
99
|
|
|
if ($flags) { |
100
|
|
|
$flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE; |
101
|
|
|
} else { |
102
|
|
|
$flags = 0; |
103
|
|
|
} |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
if (\func_num_args() >= 3) { |
107
|
|
|
@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); |
108
|
|
|
|
109
|
|
|
if (func_get_arg(2)) { |
110
|
|
|
$flags |= Yaml::PARSE_OBJECT; |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if (\func_num_args() >= 4) { |
115
|
|
|
@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); |
116
|
|
|
|
117
|
|
|
if (func_get_arg(3)) { |
118
|
|
|
$flags |= Yaml::PARSE_OBJECT_FOR_MAP; |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
if (Yaml::PARSE_KEYS_AS_STRINGS & $flags) { |
|
|
|
|
123
|
|
|
@trigger_error('Using the Yaml::PARSE_KEYS_AS_STRINGS flag is deprecated since Symfony 3.4 as it will be removed in 4.0. Quote your keys when they are evaluable instead.', \E_USER_DEPRECATED); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
if (false === preg_match('//u', $value)) { |
127
|
|
|
throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
$this->refs = []; |
131
|
|
|
|
132
|
|
|
$mbEncoding = null; |
133
|
|
|
$e = null; |
134
|
|
|
$data = null; |
|
|
|
|
135
|
|
|
|
136
|
|
|
if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { |
137
|
|
|
$mbEncoding = mb_internal_encoding(); |
138
|
|
|
mb_internal_encoding('UTF-8'); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
try { |
142
|
|
|
$data = $this->doParse($value, $flags); |
143
|
|
|
} catch (\Exception $e) { |
|
|
|
|
144
|
|
|
} catch (\Throwable $e) { |
|
|
|
|
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
if (null !== $mbEncoding) { |
148
|
|
|
mb_internal_encoding($mbEncoding); |
|
|
|
|
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$this->lines = []; |
152
|
|
|
$this->currentLine = ''; |
153
|
|
|
$this->refs = []; |
154
|
|
|
$this->skippedLineNumbers = []; |
155
|
|
|
$this->locallySkippedLineNumbers = []; |
156
|
|
|
$this->totalNumberOfLines = null; |
157
|
|
|
|
158
|
|
|
if (null !== $e) { |
159
|
|
|
throw $e; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return $data; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
private function doParse($value, $flags) |
166
|
|
|
{ |
167
|
|
|
$this->currentLineNb = -1; |
168
|
|
|
$this->currentLine = ''; |
169
|
|
|
$value = $this->cleanup($value); |
170
|
|
|
$this->lines = explode("\n", $value); |
171
|
|
|
$this->locallySkippedLineNumbers = []; |
172
|
|
|
|
173
|
|
|
if (null === $this->totalNumberOfLines) { |
174
|
|
|
$this->totalNumberOfLines = \count($this->lines); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
if (!$this->moveToNextLine()) { |
178
|
|
|
return null; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$data = []; |
182
|
|
|
$context = null; |
183
|
|
|
$allowOverwrite = false; |
184
|
|
|
|
185
|
|
|
while ($this->isCurrentLineEmpty()) { |
186
|
|
|
if (!$this->moveToNextLine()) { |
187
|
|
|
return null; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
// Resolves the tag and returns if end of the document |
192
|
|
|
if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) { |
193
|
|
|
return new TaggedValue($tag, ''); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
do { |
197
|
|
|
if ($this->isCurrentLineEmpty()) { |
198
|
|
|
continue; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
// tab? |
202
|
|
|
if ("\t" === $this->currentLine[0]) { |
203
|
|
|
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename); |
207
|
|
|
|
208
|
|
|
$isRef = $mergeNode = false; |
209
|
|
|
if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) { |
210
|
|
|
if ($context && 'mapping' == $context) { |
211
|
|
|
throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
212
|
|
|
} |
213
|
|
|
$context = 'sequence'; |
214
|
|
|
|
215
|
|
|
if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) { |
216
|
|
|
$isRef = $matches['ref']; |
217
|
|
|
$this->refsBeingParsed[] = $isRef; |
218
|
|
|
$values['value'] = $matches['value']; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) { |
222
|
|
|
@trigger_error($this->getDeprecationMessage('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.'), \E_USER_DEPRECATED); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
// array |
226
|
|
|
if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { |
227
|
|
|
$data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags); |
228
|
|
|
} elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) { |
229
|
|
|
$data[] = new TaggedValue( |
230
|
|
|
$subTag, |
231
|
|
|
$this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags) |
232
|
|
|
); |
233
|
|
|
} else { |
234
|
|
|
if ( |
235
|
|
|
isset($values['leadspaces']) |
236
|
|
|
&& ( |
237
|
|
|
'!' === $values['value'][0] |
238
|
|
|
|| self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches) |
239
|
|
|
) |
240
|
|
|
) { |
241
|
|
|
// this is a compact notation element, add to next block and parse |
242
|
|
|
$block = $values['value']; |
243
|
|
|
if ($this->isNextLineIndented()) { |
244
|
|
|
$block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
$data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags); |
248
|
|
|
} else { |
249
|
|
|
$data[] = $this->parseValue($values['value'], $flags, $context); |
250
|
|
|
} |
251
|
|
|
} |
252
|
|
|
if ($isRef) { |
253
|
|
|
$this->refs[$isRef] = end($data); |
254
|
|
|
array_pop($this->refsBeingParsed); |
255
|
|
|
} |
256
|
|
|
} elseif ( |
257
|
|
|
self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(\s++(?P<value>.+))?$#u', rtrim($this->currentLine), $values) |
258
|
|
|
&& (false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"])) |
259
|
|
|
) { |
260
|
|
|
if ($context && 'sequence' == $context) { |
261
|
|
|
throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename); |
262
|
|
|
} |
263
|
|
|
$context = 'mapping'; |
264
|
|
|
|
265
|
|
|
try { |
266
|
|
|
$i = 0; |
267
|
|
|
$evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS & $flags); |
|
|
|
|
268
|
|
|
|
269
|
|
|
// constants in key will be evaluated anyway |
270
|
|
|
if (isset($values['key'][0]) && '!' === $values['key'][0] && Yaml::PARSE_CONSTANT & $flags) { |
271
|
|
|
$evaluateKey = true; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
$key = Inline::parseScalar($values['key'], 0, null, $i, $evaluateKey); |
275
|
|
|
} catch (ParseException $e) { |
276
|
|
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1); |
277
|
|
|
$e->setSnippet($this->currentLine); |
278
|
|
|
|
279
|
|
|
throw $e; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
if (!\is_string($key) && !\is_int($key)) { |
283
|
|
|
$keyType = is_numeric($key) ? 'numeric key' : 'non-string key'; |
284
|
|
|
@trigger_error($this->getDeprecationMessage(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.', $keyType)), \E_USER_DEPRECATED); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
// Convert float keys to strings, to avoid being converted to integers by PHP |
288
|
|
|
if (\is_float($key)) { |
289
|
|
|
$key = (string) $key; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) { |
293
|
|
|
$mergeNode = true; |
294
|
|
|
$allowOverwrite = true; |
295
|
|
|
if (isset($values['value'][0]) && '*' === $values['value'][0]) { |
296
|
|
|
$refName = substr(rtrim($values['value']), 1); |
297
|
|
|
if (!\array_key_exists($refName, $this->refs)) { |
298
|
|
|
if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) { |
299
|
|
|
throw new ParseException(sprintf('Circular reference [%s, %s] detected for reference "%s".', implode(', ', \array_slice($this->refsBeingParsed, $pos)), $refName, $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename); |
|
|
|
|
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
$refValue = $this->refs[$refName]; |
306
|
|
|
|
307
|
|
|
if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) { |
308
|
|
|
$refValue = (array) $refValue; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
if (!\is_array($refValue)) { |
312
|
|
|
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
$data += $refValue; // array union |
316
|
|
|
} else { |
317
|
|
|
if (isset($values['value']) && '' !== $values['value']) { |
318
|
|
|
$value = $values['value']; |
319
|
|
|
} else { |
320
|
|
|
$value = $this->getNextEmbedBlock(); |
321
|
|
|
} |
322
|
|
|
$parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags); |
323
|
|
|
|
324
|
|
|
if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) { |
325
|
|
|
$parsed = (array) $parsed; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
if (!\is_array($parsed)) { |
329
|
|
|
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
if (isset($parsed[0])) { |
333
|
|
|
// If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes |
334
|
|
|
// and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier |
335
|
|
|
// in the sequence override keys specified in later mapping nodes. |
336
|
|
|
foreach ($parsed as $parsedItem) { |
337
|
|
|
if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) { |
338
|
|
|
$parsedItem = (array) $parsedItem; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
if (!\is_array($parsedItem)) { |
342
|
|
|
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
$data += $parsedItem; // array union |
346
|
|
|
} |
347
|
|
|
} else { |
348
|
|
|
// If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the |
349
|
|
|
// current mapping, unless the key already exists in it. |
350
|
|
|
$data += $parsed; // array union |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
} elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u', $values['value'], $matches)) { |
354
|
|
|
$isRef = $matches['ref']; |
355
|
|
|
$this->refsBeingParsed[] = $isRef; |
356
|
|
|
$values['value'] = $matches['value']; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
$subTag = null; |
360
|
|
|
if ($mergeNode) { |
361
|
|
|
// Merge keys |
362
|
|
|
} elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) { |
363
|
|
|
// hash |
364
|
|
|
// if next line is less indented or equal, then it means that the current value is null |
365
|
|
|
if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { |
366
|
|
|
// Spec: Keys MUST be unique; first one wins. |
367
|
|
|
// But overwriting is allowed when a merge node is used in current block. |
368
|
|
|
if ($allowOverwrite || !isset($data[$key])) { |
369
|
|
|
if (null !== $subTag) { |
370
|
|
|
$data[$key] = new TaggedValue($subTag, ''); |
|
|
|
|
371
|
|
|
} else { |
372
|
|
|
$data[$key] = null; |
373
|
|
|
} |
374
|
|
|
} else { |
375
|
|
|
@trigger_error($this->getDeprecationMessage(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.', $key)), \E_USER_DEPRECATED); |
376
|
|
|
} |
377
|
|
|
} else { |
378
|
|
|
$value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags); |
379
|
|
|
if ('<<' === $key) { |
380
|
|
|
$this->refs[$refMatches['ref']] = $value; |
|
|
|
|
381
|
|
|
|
382
|
|
|
if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) { |
383
|
|
|
$value = (array) $value; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
$data += $value; |
387
|
|
|
} elseif ($allowOverwrite || !isset($data[$key])) { |
388
|
|
|
// Spec: Keys MUST be unique; first one wins. |
389
|
|
|
// But overwriting is allowed when a merge node is used in current block. |
390
|
|
|
if (null !== $subTag) { |
391
|
|
|
$data[$key] = new TaggedValue($subTag, $value); |
392
|
|
|
} else { |
393
|
|
|
$data[$key] = $value; |
394
|
|
|
} |
395
|
|
|
} else { |
396
|
|
|
@trigger_error($this->getDeprecationMessage(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.', $key)), \E_USER_DEPRECATED); |
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
} else { |
400
|
|
|
$value = $this->parseValue(rtrim($values['value']), $flags, $context); |
401
|
|
|
// Spec: Keys MUST be unique; first one wins. |
402
|
|
|
// But overwriting is allowed when a merge node is used in current block. |
403
|
|
|
if ($allowOverwrite || !isset($data[$key])) { |
404
|
|
|
$data[$key] = $value; |
405
|
|
|
} else { |
406
|
|
|
@trigger_error($this->getDeprecationMessage(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.', $key)), \E_USER_DEPRECATED); |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
if ($isRef) { |
410
|
|
|
$this->refs[$isRef] = $data[$key]; |
411
|
|
|
array_pop($this->refsBeingParsed); |
412
|
|
|
} |
413
|
|
|
} else { |
414
|
|
|
// multiple documents are not supported |
415
|
|
|
if ('---' === $this->currentLine) { |
416
|
|
|
throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) { |
420
|
|
|
@trigger_error($this->getDeprecationMessage('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.'), \E_USER_DEPRECATED); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
// 1-liner optionally followed by newline(s) |
424
|
|
|
if (\is_string($value) && $this->lines[0] === trim($value)) { |
425
|
|
|
try { |
426
|
|
|
$value = Inline::parse($this->lines[0], $flags, $this->refs); |
427
|
|
|
} catch (ParseException $e) { |
428
|
|
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1); |
429
|
|
|
$e->setSnippet($this->currentLine); |
430
|
|
|
|
431
|
|
|
throw $e; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
return $value; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
// try to parse the value as a multi-line string as a last resort |
438
|
|
|
if (0 === $this->currentLineNb) { |
439
|
|
|
$previousLineWasNewline = false; |
440
|
|
|
$previousLineWasTerminatedWithBackslash = false; |
441
|
|
|
$value = ''; |
442
|
|
|
|
443
|
|
|
foreach ($this->lines as $line) { |
444
|
|
|
if ('' !== ltrim($line) && '#' === ltrim($line)[0]) { |
445
|
|
|
continue; |
446
|
|
|
} |
447
|
|
|
// If the indentation is not consistent at offset 0, it is to be considered as a ParseError |
448
|
|
|
if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) { |
449
|
|
|
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
450
|
|
|
} |
451
|
|
|
if ('' === trim($line)) { |
452
|
|
|
$value .= "\n"; |
453
|
|
|
} elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { |
454
|
|
|
$value .= ' '; |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
if ('' !== trim($line) && '\\' === substr($line, -1)) { |
458
|
|
|
$value .= ltrim(substr($line, 0, -1)); |
459
|
|
|
} elseif ('' !== trim($line)) { |
460
|
|
|
$value .= trim($line); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
if ('' === trim($line)) { |
464
|
|
|
$previousLineWasNewline = true; |
465
|
|
|
$previousLineWasTerminatedWithBackslash = false; |
466
|
|
|
} elseif ('\\' === substr($line, -1)) { |
467
|
|
|
$previousLineWasNewline = false; |
468
|
|
|
$previousLineWasTerminatedWithBackslash = true; |
469
|
|
|
} else { |
470
|
|
|
$previousLineWasNewline = false; |
471
|
|
|
$previousLineWasTerminatedWithBackslash = false; |
472
|
|
|
} |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
try { |
476
|
|
|
return Inline::parse(trim($value)); |
477
|
|
|
} catch (ParseException $e) { |
478
|
|
|
// fall-through to the ParseException thrown below |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
483
|
|
|
} |
484
|
|
|
} while ($this->moveToNextLine()); |
485
|
|
|
|
486
|
|
|
if (null !== $tag) { |
487
|
|
|
$data = new TaggedValue($tag, $data); |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !\is_object($data) && 'mapping' === $context) { |
491
|
|
|
$object = new \stdClass(); |
492
|
|
|
|
493
|
|
|
foreach ($data as $key => $value) { |
|
|
|
|
494
|
|
|
$object->$key = $value; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
$data = $object; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
return empty($data) ? null : $data; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
private function parseBlock($offset, $yaml, $flags) |
504
|
|
|
{ |
505
|
|
|
$skippedLineNumbers = $this->skippedLineNumbers; |
506
|
|
|
|
507
|
|
|
foreach ($this->locallySkippedLineNumbers as $lineNumber) { |
508
|
|
|
if ($lineNumber < $offset) { |
509
|
|
|
continue; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
$skippedLineNumbers[] = $lineNumber; |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
$parser = new self(); |
516
|
|
|
$parser->offset = $offset; |
517
|
|
|
$parser->totalNumberOfLines = $this->totalNumberOfLines; |
518
|
|
|
$parser->skippedLineNumbers = $skippedLineNumbers; |
519
|
|
|
$parser->refs = &$this->refs; |
520
|
|
|
$parser->refsBeingParsed = $this->refsBeingParsed; |
521
|
|
|
|
522
|
|
|
return $parser->doParse($yaml, $flags); |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* Returns the current line number (takes the offset into account). |
527
|
|
|
* |
528
|
|
|
* @internal |
529
|
|
|
* |
530
|
|
|
* @return int The current line number |
531
|
|
|
*/ |
532
|
|
|
public function getRealCurrentLineNb() |
533
|
|
|
{ |
534
|
|
|
$realCurrentLineNumber = $this->currentLineNb + $this->offset; |
535
|
|
|
|
536
|
|
|
foreach ($this->skippedLineNumbers as $skippedLineNumber) { |
537
|
|
|
if ($skippedLineNumber > $realCurrentLineNumber) { |
538
|
|
|
break; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
++$realCurrentLineNumber; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
return $realCurrentLineNumber; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
/** |
548
|
|
|
* Returns the current line indentation. |
549
|
|
|
* |
550
|
|
|
* @return int The current line indentation |
551
|
|
|
*/ |
552
|
|
|
private function getCurrentLineIndentation() |
553
|
|
|
{ |
554
|
|
|
return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' ')); |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
/** |
558
|
|
|
* Returns the next embed block of YAML. |
559
|
|
|
* |
560
|
|
|
* @param int $indentation The indent level at which the block is to be read, or null for default |
561
|
|
|
* @param bool $inSequence True if the enclosing data structure is a sequence |
562
|
|
|
* |
563
|
|
|
* @return string A YAML string |
564
|
|
|
* |
565
|
|
|
* @throws ParseException When indentation problem are detected |
566
|
|
|
*/ |
567
|
|
|
private function getNextEmbedBlock($indentation = null, $inSequence = false) |
568
|
|
|
{ |
569
|
|
|
$oldLineIndentation = $this->getCurrentLineIndentation(); |
570
|
|
|
|
571
|
|
|
if (!$this->moveToNextLine()) { |
572
|
|
|
return ''; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
if (null === $indentation) { |
576
|
|
|
$newIndent = null; |
577
|
|
|
$movements = 0; |
578
|
|
|
|
579
|
|
|
do { |
580
|
|
|
$EOF = false; |
581
|
|
|
|
582
|
|
|
// empty and comment-like lines do not influence the indentation depth |
583
|
|
|
if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) { |
584
|
|
|
$EOF = !$this->moveToNextLine(); |
585
|
|
|
|
586
|
|
|
if (!$EOF) { |
587
|
|
|
++$movements; |
588
|
|
|
} |
589
|
|
|
} else { |
590
|
|
|
$newIndent = $this->getCurrentLineIndentation(); |
591
|
|
|
} |
592
|
|
|
} while (!$EOF && null === $newIndent); |
593
|
|
|
|
594
|
|
|
for ($i = 0; $i < $movements; ++$i) { |
595
|
|
|
$this->moveToPreviousLine(); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
$unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem(); |
599
|
|
|
|
600
|
|
|
if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { |
601
|
|
|
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
602
|
|
|
} |
603
|
|
|
} else { |
604
|
|
|
$newIndent = $indentation; |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
$data = []; |
608
|
|
|
if ($this->getCurrentLineIndentation() >= $newIndent) { |
609
|
|
|
$data[] = substr($this->currentLine, $newIndent); |
|
|
|
|
610
|
|
|
} elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) { |
611
|
|
|
$data[] = $this->currentLine; |
612
|
|
|
} else { |
613
|
|
|
$this->moveToPreviousLine(); |
614
|
|
|
|
615
|
|
|
return ''; |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) { |
619
|
|
|
// the previous line contained a dash but no item content, this line is a sequence item with the same indentation |
620
|
|
|
// and therefore no nested list or mapping |
621
|
|
|
$this->moveToPreviousLine(); |
622
|
|
|
|
623
|
|
|
return ''; |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); |
627
|
|
|
$isItComment = $this->isCurrentLineComment(); |
628
|
|
|
|
629
|
|
|
while ($this->moveToNextLine()) { |
630
|
|
|
if ($isItComment && !$isItUnindentedCollection) { |
631
|
|
|
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); |
632
|
|
|
$isItComment = $this->isCurrentLineComment(); |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
$indent = $this->getCurrentLineIndentation(); |
636
|
|
|
|
637
|
|
|
if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) { |
638
|
|
|
$this->moveToPreviousLine(); |
639
|
|
|
break; |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
if ($this->isCurrentLineBlank()) { |
643
|
|
|
$data[] = substr($this->currentLine, $newIndent); |
644
|
|
|
continue; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
if ($indent >= $newIndent) { |
648
|
|
|
$data[] = substr($this->currentLine, $newIndent); |
649
|
|
|
} elseif ($this->isCurrentLineComment()) { |
650
|
|
|
$data[] = $this->currentLine; |
651
|
|
|
} elseif (0 == $indent) { |
652
|
|
|
$this->moveToPreviousLine(); |
653
|
|
|
|
654
|
|
|
break; |
655
|
|
|
} else { |
656
|
|
|
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); |
657
|
|
|
} |
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
return implode("\n", $data); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
/** |
664
|
|
|
* Moves the parser to the next line. |
665
|
|
|
* |
666
|
|
|
* @return bool |
667
|
|
|
*/ |
668
|
|
|
private function moveToNextLine() |
669
|
|
|
{ |
670
|
|
|
if ($this->currentLineNb >= \count($this->lines) - 1) { |
671
|
|
|
return false; |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
$this->currentLine = $this->lines[++$this->currentLineNb]; |
675
|
|
|
|
676
|
|
|
return true; |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
/** |
680
|
|
|
* Moves the parser to the previous line. |
681
|
|
|
* |
682
|
|
|
* @return bool |
683
|
|
|
*/ |
684
|
|
|
private function moveToPreviousLine() |
685
|
|
|
{ |
686
|
|
|
if ($this->currentLineNb < 1) { |
687
|
|
|
return false; |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
$this->currentLine = $this->lines[--$this->currentLineNb]; |
691
|
|
|
|
692
|
|
|
return true; |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
/** |
696
|
|
|
* Parses a YAML value. |
697
|
|
|
* |
698
|
|
|
* @param string $value A YAML value |
699
|
|
|
* @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior |
700
|
|
|
* @param string $context The parser context (either sequence or mapping) |
701
|
|
|
* |
702
|
|
|
* @return mixed A PHP value |
703
|
|
|
* |
704
|
|
|
* @throws ParseException When reference does not exist |
705
|
|
|
*/ |
706
|
|
|
private function parseValue($value, $flags, $context) |
707
|
|
|
{ |
708
|
|
|
if (0 === strpos($value, '*')) { |
709
|
|
|
if (false !== $pos = strpos($value, '#')) { |
710
|
|
|
$value = substr($value, 1, $pos - 2); |
711
|
|
|
} else { |
712
|
|
|
$value = substr($value, 1); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
if (!\array_key_exists($value, $this->refs)) { |
716
|
|
|
if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) { |
717
|
|
|
throw new ParseException(sprintf('Circular reference [%s, %s] detected for reference "%s".', implode(', ', \array_slice($this->refsBeingParsed, $pos)), $value, $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); |
|
|
|
|
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
return $this->refs[$value]; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
if (self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { |
727
|
|
|
$modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; |
728
|
|
|
|
729
|
|
|
$data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers)); |
|
|
|
|
730
|
|
|
|
731
|
|
|
if ('' !== $matches['tag']) { |
732
|
|
|
if ('!!binary' === $matches['tag']) { |
733
|
|
|
return Inline::evaluateBinaryScalar($data); |
734
|
|
|
} elseif ('tagged' === $matches['tag']) { |
735
|
|
|
return new TaggedValue(substr($matches['tag'], 1), $data); |
736
|
|
|
} elseif ('!' !== $matches['tag']) { |
737
|
|
|
@trigger_error($this->getDeprecationMessage(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.', $matches['tag'], $data, TaggedValue::class)), \E_USER_DEPRECATED); |
738
|
|
|
} |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
return $data; |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
try { |
745
|
|
|
$quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null; |
746
|
|
|
|
747
|
|
|
// do not take following lines into account when the current line is a quoted single line value |
748
|
|
|
if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) { |
749
|
|
|
return Inline::parse($value, $flags, $this->refs); |
750
|
|
|
} |
751
|
|
|
|
752
|
|
|
$lines = []; |
753
|
|
|
|
754
|
|
|
while ($this->moveToNextLine()) { |
755
|
|
|
// unquoted strings end before the first unindented line |
756
|
|
|
if (null === $quotation && 0 === $this->getCurrentLineIndentation()) { |
757
|
|
|
$this->moveToPreviousLine(); |
758
|
|
|
|
759
|
|
|
break; |
760
|
|
|
} |
761
|
|
|
|
762
|
|
|
$lines[] = trim($this->currentLine); |
763
|
|
|
|
764
|
|
|
// quoted string values end with a line that is terminated with the quotation character |
765
|
|
|
$escapedLine = str_replace(['\\\\', '\\"'], '', $this->currentLine); |
766
|
|
|
if ('' !== $escapedLine && substr($escapedLine, -1) === $quotation) { |
767
|
|
|
break; |
768
|
|
|
} |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) { |
772
|
|
|
if ('' === $lines[$i]) { |
773
|
|
|
$value .= "\n"; |
774
|
|
|
$previousLineBlank = true; |
775
|
|
|
} elseif ($previousLineBlank) { |
776
|
|
|
$value .= $lines[$i]; |
777
|
|
|
$previousLineBlank = false; |
778
|
|
|
} else { |
779
|
|
|
$value .= ' '.$lines[$i]; |
780
|
|
|
$previousLineBlank = false; |
781
|
|
|
} |
782
|
|
|
} |
783
|
|
|
|
784
|
|
|
Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); |
785
|
|
|
|
786
|
|
|
$parsedValue = Inline::parse($value, $flags, $this->refs); |
787
|
|
|
|
788
|
|
|
if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { |
789
|
|
|
throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename); |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
return $parsedValue; |
793
|
|
|
} catch (ParseException $e) { |
794
|
|
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1); |
795
|
|
|
$e->setSnippet($this->currentLine); |
796
|
|
|
|
797
|
|
|
throw $e; |
798
|
|
|
} |
799
|
|
|
} |
800
|
|
|
|
801
|
|
|
/** |
802
|
|
|
* Parses a block scalar. |
803
|
|
|
* |
804
|
|
|
* @param string $style The style indicator that was used to begin this block scalar (| or >) |
805
|
|
|
* @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -) |
806
|
|
|
* @param int $indentation The indentation indicator that was used to begin this block scalar |
807
|
|
|
* |
808
|
|
|
* @return string The text value |
809
|
|
|
*/ |
810
|
|
|
private function parseBlockScalar($style, $chomping = '', $indentation = 0) |
811
|
|
|
{ |
812
|
|
|
$notEOF = $this->moveToNextLine(); |
813
|
|
|
if (!$notEOF) { |
814
|
|
|
return ''; |
815
|
|
|
} |
816
|
|
|
|
817
|
|
|
$isCurrentLineBlank = $this->isCurrentLineBlank(); |
818
|
|
|
$blockLines = []; |
819
|
|
|
|
820
|
|
|
// leading blank lines are consumed before determining indentation |
821
|
|
|
while ($notEOF && $isCurrentLineBlank) { |
822
|
|
|
// newline only if not EOF |
823
|
|
|
if ($notEOF = $this->moveToNextLine()) { |
824
|
|
|
$blockLines[] = ''; |
825
|
|
|
$isCurrentLineBlank = $this->isCurrentLineBlank(); |
826
|
|
|
} |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
// determine indentation if not specified |
830
|
|
|
if (0 === $indentation) { |
831
|
|
|
if (self::preg_match('/^ +/', $this->currentLine, $matches)) { |
832
|
|
|
$indentation = \strlen($matches[0]); |
833
|
|
|
} |
834
|
|
|
} |
835
|
|
|
|
836
|
|
|
if ($indentation > 0) { |
837
|
|
|
$pattern = sprintf('/^ {%d}(.*)$/', $indentation); |
838
|
|
|
|
839
|
|
|
while ( |
840
|
|
|
$notEOF && ( |
841
|
|
|
$isCurrentLineBlank || |
842
|
|
|
self::preg_match($pattern, $this->currentLine, $matches) |
843
|
|
|
) |
844
|
|
|
) { |
845
|
|
|
if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) { |
846
|
|
|
$blockLines[] = substr($this->currentLine, $indentation); |
847
|
|
|
} elseif ($isCurrentLineBlank) { |
848
|
|
|
$blockLines[] = ''; |
849
|
|
|
} else { |
850
|
|
|
$blockLines[] = $matches[1]; |
|
|
|
|
851
|
|
|
} |
852
|
|
|
|
853
|
|
|
// newline only if not EOF |
854
|
|
|
if ($notEOF = $this->moveToNextLine()) { |
855
|
|
|
$isCurrentLineBlank = $this->isCurrentLineBlank(); |
856
|
|
|
} |
857
|
|
|
} |
858
|
|
|
} elseif ($notEOF) { |
859
|
|
|
$blockLines[] = ''; |
860
|
|
|
} |
861
|
|
|
|
862
|
|
|
if ($notEOF) { |
863
|
|
|
$blockLines[] = ''; |
864
|
|
|
$this->moveToPreviousLine(); |
865
|
|
|
} elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) { |
866
|
|
|
$blockLines[] = ''; |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
// folded style |
870
|
|
|
if ('>' === $style) { |
871
|
|
|
$text = ''; |
872
|
|
|
$previousLineIndented = false; |
873
|
|
|
$previousLineBlank = false; |
874
|
|
|
|
875
|
|
|
for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) { |
876
|
|
|
if ('' === $blockLines[$i]) { |
877
|
|
|
$text .= "\n"; |
878
|
|
|
$previousLineIndented = false; |
879
|
|
|
$previousLineBlank = true; |
880
|
|
|
} elseif (' ' === $blockLines[$i][0]) { |
881
|
|
|
$text .= "\n".$blockLines[$i]; |
882
|
|
|
$previousLineIndented = true; |
883
|
|
|
$previousLineBlank = false; |
884
|
|
|
} elseif ($previousLineIndented) { |
885
|
|
|
$text .= "\n".$blockLines[$i]; |
886
|
|
|
$previousLineIndented = false; |
887
|
|
|
$previousLineBlank = false; |
888
|
|
|
} elseif ($previousLineBlank || 0 === $i) { |
889
|
|
|
$text .= $blockLines[$i]; |
890
|
|
|
$previousLineIndented = false; |
891
|
|
|
$previousLineBlank = false; |
892
|
|
|
} else { |
893
|
|
|
$text .= ' '.$blockLines[$i]; |
894
|
|
|
$previousLineIndented = false; |
895
|
|
|
$previousLineBlank = false; |
896
|
|
|
} |
897
|
|
|
} |
898
|
|
|
} else { |
899
|
|
|
$text = implode("\n", $blockLines); |
900
|
|
|
} |
901
|
|
|
|
902
|
|
|
// deal with trailing newlines |
903
|
|
|
if ('' === $chomping) { |
904
|
|
|
$text = preg_replace('/\n+$/', "\n", $text); |
905
|
|
|
} elseif ('-' === $chomping) { |
906
|
|
|
$text = preg_replace('/\n+$/', '', $text); |
907
|
|
|
} |
908
|
|
|
|
909
|
|
|
return $text; |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
/** |
913
|
|
|
* Returns true if the next line is indented. |
914
|
|
|
* |
915
|
|
|
* @return bool Returns true if the next line is indented, false otherwise |
916
|
|
|
*/ |
917
|
|
|
private function isNextLineIndented() |
918
|
|
|
{ |
919
|
|
|
$currentIndentation = $this->getCurrentLineIndentation(); |
920
|
|
|
$movements = 0; |
921
|
|
|
|
922
|
|
|
do { |
923
|
|
|
$EOF = !$this->moveToNextLine(); |
924
|
|
|
|
925
|
|
|
if (!$EOF) { |
926
|
|
|
++$movements; |
927
|
|
|
} |
928
|
|
|
} while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment())); |
929
|
|
|
|
930
|
|
|
if ($EOF) { |
931
|
|
|
return false; |
932
|
|
|
} |
933
|
|
|
|
934
|
|
|
$ret = $this->getCurrentLineIndentation() > $currentIndentation; |
935
|
|
|
|
936
|
|
|
for ($i = 0; $i < $movements; ++$i) { |
937
|
|
|
$this->moveToPreviousLine(); |
938
|
|
|
} |
939
|
|
|
|
940
|
|
|
return $ret; |
941
|
|
|
} |
942
|
|
|
|
943
|
|
|
/** |
944
|
|
|
* Returns true if the current line is blank or if it is a comment line. |
945
|
|
|
* |
946
|
|
|
* @return bool Returns true if the current line is empty or if it is a comment line, false otherwise |
947
|
|
|
*/ |
948
|
|
|
private function isCurrentLineEmpty() |
949
|
|
|
{ |
950
|
|
|
return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); |
951
|
|
|
} |
952
|
|
|
|
953
|
|
|
/** |
954
|
|
|
* Returns true if the current line is blank. |
955
|
|
|
* |
956
|
|
|
* @return bool Returns true if the current line is blank, false otherwise |
957
|
|
|
*/ |
958
|
|
|
private function isCurrentLineBlank() |
959
|
|
|
{ |
960
|
|
|
return '' == trim($this->currentLine, ' '); |
961
|
|
|
} |
962
|
|
|
|
963
|
|
|
/** |
964
|
|
|
* Returns true if the current line is a comment line. |
965
|
|
|
* |
966
|
|
|
* @return bool Returns true if the current line is a comment line, false otherwise |
967
|
|
|
*/ |
968
|
|
|
private function isCurrentLineComment() |
969
|
|
|
{ |
970
|
|
|
//checking explicitly the first char of the trim is faster than loops or strpos |
971
|
|
|
$ltrimmedLine = ltrim($this->currentLine, ' '); |
972
|
|
|
|
973
|
|
|
return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0]; |
974
|
|
|
} |
975
|
|
|
|
976
|
|
|
private function isCurrentLineLastLineInDocument() |
977
|
|
|
{ |
978
|
|
|
return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1); |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
/** |
982
|
|
|
* Cleanups a YAML string to be parsed. |
983
|
|
|
* |
984
|
|
|
* @param string $value The input YAML string |
985
|
|
|
* |
986
|
|
|
* @return string A cleaned up YAML string |
987
|
|
|
*/ |
988
|
|
|
private function cleanup($value) |
989
|
|
|
{ |
990
|
|
|
$value = str_replace(["\r\n", "\r"], "\n", $value); |
991
|
|
|
|
992
|
|
|
// strip YAML header |
993
|
|
|
$count = 0; |
994
|
|
|
$value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); |
995
|
|
|
$this->offset += $count; |
996
|
|
|
|
997
|
|
|
// remove leading comments |
998
|
|
|
$trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); |
999
|
|
|
if (1 === $count) { |
1000
|
|
|
// items have been removed, update the offset |
1001
|
|
|
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
1002
|
|
|
$value = $trimmedValue; |
1003
|
|
|
} |
1004
|
|
|
|
1005
|
|
|
// remove start of the document marker (---) |
1006
|
|
|
$trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); |
1007
|
|
|
if (1 === $count) { |
1008
|
|
|
// items have been removed, update the offset |
1009
|
|
|
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
1010
|
|
|
$value = $trimmedValue; |
1011
|
|
|
|
1012
|
|
|
// remove end of the document marker (...) |
1013
|
|
|
$value = preg_replace('#\.\.\.\s*$#', '', $value); |
1014
|
|
|
} |
1015
|
|
|
|
1016
|
|
|
return $value; |
1017
|
|
|
} |
1018
|
|
|
|
1019
|
|
|
/** |
1020
|
|
|
* Returns true if the next line starts unindented collection. |
1021
|
|
|
* |
1022
|
|
|
* @return bool Returns true if the next line starts unindented collection, false otherwise |
1023
|
|
|
*/ |
1024
|
|
|
private function isNextLineUnIndentedCollection() |
1025
|
|
|
{ |
1026
|
|
|
$currentIndentation = $this->getCurrentLineIndentation(); |
1027
|
|
|
$movements = 0; |
1028
|
|
|
|
1029
|
|
|
do { |
1030
|
|
|
$EOF = !$this->moveToNextLine(); |
1031
|
|
|
|
1032
|
|
|
if (!$EOF) { |
1033
|
|
|
++$movements; |
1034
|
|
|
} |
1035
|
|
|
} while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment())); |
1036
|
|
|
|
1037
|
|
|
if ($EOF) { |
1038
|
|
|
return false; |
1039
|
|
|
} |
1040
|
|
|
|
1041
|
|
|
$ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem(); |
1042
|
|
|
|
1043
|
|
|
for ($i = 0; $i < $movements; ++$i) { |
1044
|
|
|
$this->moveToPreviousLine(); |
1045
|
|
|
} |
1046
|
|
|
|
1047
|
|
|
return $ret; |
1048
|
|
|
} |
1049
|
|
|
|
1050
|
|
|
/** |
1051
|
|
|
* Returns true if the string is un-indented collection item. |
1052
|
|
|
* |
1053
|
|
|
* @return bool Returns true if the string is un-indented collection item, false otherwise |
1054
|
|
|
*/ |
1055
|
|
|
private function isStringUnIndentedCollectionItem() |
1056
|
|
|
{ |
1057
|
|
|
return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- '); |
1058
|
|
|
} |
1059
|
|
|
|
1060
|
|
|
/** |
1061
|
|
|
* A local wrapper for `preg_match` which will throw a ParseException if there |
1062
|
|
|
* is an internal error in the PCRE engine. |
1063
|
|
|
* |
1064
|
|
|
* This avoids us needing to check for "false" every time PCRE is used |
1065
|
|
|
* in the YAML engine |
1066
|
|
|
* |
1067
|
|
|
* @throws ParseException on a PCRE internal error |
1068
|
|
|
* |
1069
|
|
|
* @see preg_last_error() |
1070
|
|
|
* |
1071
|
|
|
* @internal |
1072
|
|
|
*/ |
1073
|
|
|
public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) |
1074
|
|
|
{ |
1075
|
|
|
if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) { |
1076
|
|
|
switch (preg_last_error()) { |
1077
|
|
|
case \PREG_INTERNAL_ERROR: |
1078
|
|
|
$error = 'Internal PCRE error.'; |
1079
|
|
|
break; |
1080
|
|
|
case \PREG_BACKTRACK_LIMIT_ERROR: |
1081
|
|
|
$error = 'pcre.backtrack_limit reached.'; |
1082
|
|
|
break; |
1083
|
|
|
case \PREG_RECURSION_LIMIT_ERROR: |
1084
|
|
|
$error = 'pcre.recursion_limit reached.'; |
1085
|
|
|
break; |
1086
|
|
|
case \PREG_BAD_UTF8_ERROR: |
1087
|
|
|
$error = 'Malformed UTF-8 data.'; |
1088
|
|
|
break; |
1089
|
|
|
case \PREG_BAD_UTF8_OFFSET_ERROR: |
1090
|
|
|
$error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; |
1091
|
|
|
break; |
1092
|
|
|
default: |
1093
|
|
|
$error = 'Error.'; |
1094
|
|
|
} |
1095
|
|
|
|
1096
|
|
|
throw new ParseException($error); |
1097
|
|
|
} |
1098
|
|
|
|
1099
|
|
|
return $ret; |
1100
|
|
|
} |
1101
|
|
|
|
1102
|
|
|
/** |
1103
|
|
|
* Trim the tag on top of the value. |
1104
|
|
|
* |
1105
|
|
|
* Prevent values such as `!foo {quz: bar}` to be considered as |
1106
|
|
|
* a mapping block. |
1107
|
|
|
*/ |
1108
|
|
|
private function trimTag($value) |
1109
|
|
|
{ |
1110
|
|
|
if ('!' === $value[0]) { |
1111
|
|
|
return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' '); |
1112
|
|
|
} |
1113
|
|
|
|
1114
|
|
|
return $value; |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
|
|
/** |
1118
|
|
|
* @return string|null |
1119
|
|
|
*/ |
1120
|
|
|
private function getLineTag($value, $flags, $nextLineCheck = true) |
1121
|
|
|
{ |
1122
|
|
|
if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) { |
1123
|
|
|
return null; |
1124
|
|
|
} |
1125
|
|
|
|
1126
|
|
|
if ($nextLineCheck && !$this->isNextLineIndented()) { |
1127
|
|
|
return null; |
1128
|
|
|
} |
1129
|
|
|
|
1130
|
|
|
$tag = substr($matches['tag'], 1); |
1131
|
|
|
|
1132
|
|
|
// Built-in tags |
1133
|
|
|
if ($tag && '!' === $tag[0]) { |
1134
|
|
|
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename); |
1135
|
|
|
} |
1136
|
|
|
|
1137
|
|
|
if (Yaml::PARSE_CUSTOM_TAGS & $flags) { |
1138
|
|
|
return $tag; |
1139
|
|
|
} |
1140
|
|
|
|
1141
|
|
|
throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename); |
1142
|
|
|
} |
1143
|
|
|
|
1144
|
|
|
private function getDeprecationMessage($message) |
1145
|
|
|
{ |
1146
|
|
|
$message = rtrim($message, '.'); |
1147
|
|
|
|
1148
|
|
|
if (null !== $this->filename) { |
1149
|
|
|
$message .= ' in '.$this->filename; |
1150
|
|
|
} |
1151
|
|
|
|
1152
|
|
|
$message .= ' on line '.($this->getRealCurrentLineNb() + 1); |
1153
|
|
|
|
1154
|
|
|
return $message.'.'; |
1155
|
|
|
} |
1156
|
|
|
} |
1157
|
|
|
|
This class constant has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.