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
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Parser parses YAML strings to convert them to PHP arrays. |
18
|
|
|
* |
19
|
|
|
* @author Fabien Potencier <[email protected]> |
20
|
|
|
*/ |
21
|
|
|
class Parser |
22
|
|
|
{ |
23
|
|
|
const FOLDED_SCALAR_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?'; |
24
|
|
|
|
25
|
|
|
private $offset = 0; |
26
|
|
|
private $lines = array(); |
27
|
|
|
private $currentLineNb = -1; |
28
|
|
|
private $currentLine = ''; |
29
|
|
|
private $refs = array(); |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Constructor. |
33
|
|
|
* |
34
|
|
|
* @param int $offset The offset of YAML document (used for line numbers in error messages) |
35
|
|
|
*/ |
36
|
|
|
public function __construct($offset = 0) |
37
|
|
|
{ |
38
|
|
|
$this->offset = $offset; |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Parses a YAML string to a PHP value. |
43
|
|
|
* |
44
|
|
|
* @param string $value A YAML string |
45
|
|
|
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise |
46
|
|
|
* @param bool $objectSupport true if object support is enabled, false otherwise |
47
|
|
|
* @param bool $objectForMap true if maps should return a stdClass instead of array() |
48
|
|
|
* |
49
|
|
|
* @return mixed A PHP value |
50
|
|
|
* |
51
|
|
|
* @throws ParseException If the YAML is not valid |
52
|
|
|
*/ |
53
|
|
|
public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) |
54
|
|
|
{ |
55
|
|
|
if (!preg_match('//u', $value)) { |
56
|
|
|
throw new ParseException('The YAML value does not appear to be valid UTF-8.'); |
57
|
|
|
} |
58
|
|
|
$this->currentLineNb = -1; |
59
|
|
|
$this->currentLine = ''; |
60
|
|
|
$value = $this->cleanup($value); |
61
|
|
|
$this->lines = explode("\n", $value); |
62
|
|
|
|
63
|
|
View Code Duplication |
if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { |
|
|
|
|
64
|
|
|
$mbEncoding = mb_internal_encoding(); |
65
|
|
|
mb_internal_encoding('UTF-8'); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
$data = array(); |
69
|
|
|
$context = null; |
70
|
|
|
$allowOverwrite = false; |
71
|
|
|
while ($this->moveToNextLine()) { |
72
|
|
|
if ($this->isCurrentLineEmpty()) { |
73
|
|
|
continue; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
// tab? |
77
|
|
|
if ("\t" === $this->currentLine[0]) { |
78
|
|
|
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
$isRef = $mergeNode = false; |
82
|
|
|
if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) { |
83
|
|
|
if ($context && 'mapping' == $context) { |
|
|
|
|
84
|
|
|
throw new ParseException('You cannot define a sequence item when in a mapping'); |
85
|
|
|
} |
86
|
|
|
$context = 'sequence'; |
87
|
|
|
|
88
|
|
View Code Duplication |
if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) { |
|
|
|
|
89
|
|
|
$isRef = $matches['ref']; |
90
|
|
|
$values['value'] = $matches['value']; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// array |
94
|
|
|
if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { |
95
|
|
|
$c = $this->getRealCurrentLineNb() + 1; |
96
|
|
|
$parser = new self($c); |
97
|
|
|
$parser->refs = &$this->refs; |
98
|
|
|
$data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap); |
99
|
|
|
} else { |
100
|
|
|
if (isset($values['leadspaces']) |
101
|
|
|
&& preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches) |
102
|
|
|
) { |
103
|
|
|
// this is a compact notation element, add to next block and parse |
104
|
|
|
$c = $this->getRealCurrentLineNb(); |
105
|
|
|
$parser = new self($c); |
106
|
|
|
$parser->refs = &$this->refs; |
107
|
|
|
|
108
|
|
|
$block = $values['value']; |
109
|
|
|
if ($this->isNextLineIndented()) { |
110
|
|
|
$block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
$data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap); |
114
|
|
|
} else { |
115
|
|
|
$data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap); |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
} elseif (preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) { |
119
|
|
|
if ($context && 'sequence' == $context) { |
|
|
|
|
120
|
|
|
throw new ParseException('You cannot define a mapping item when in a sequence'); |
121
|
|
|
} |
122
|
|
|
$context = 'mapping'; |
123
|
|
|
|
124
|
|
|
// force correct settings |
125
|
|
|
Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); |
126
|
|
|
try { |
127
|
|
|
$key = Inline::parseScalar($values['key']); |
128
|
|
|
} catch (ParseException $e) { |
129
|
|
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1); |
130
|
|
|
$e->setSnippet($this->currentLine); |
131
|
|
|
|
132
|
|
|
throw $e; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
if ('<<' === $key) { |
136
|
|
|
$mergeNode = true; |
137
|
|
|
$allowOverwrite = true; |
138
|
|
|
if (isset($values['value']) && 0 === strpos($values['value'], '*')) { |
139
|
|
|
$refName = substr($values['value'], 1); |
140
|
|
|
if (!array_key_exists($refName, $this->refs)) { |
141
|
|
|
throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$refValue = $this->refs[$refName]; |
145
|
|
|
|
146
|
|
|
if (!is_array($refValue)) { |
147
|
|
|
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
foreach ($refValue as $key => $value) { |
151
|
|
|
if (!isset($data[$key])) { |
152
|
|
|
$data[$key] = $value; |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
} else { |
156
|
|
|
if (isset($values['value']) && $values['value'] !== '') { |
157
|
|
|
$value = $values['value']; |
158
|
|
|
} else { |
159
|
|
|
$value = $this->getNextEmbedBlock(); |
160
|
|
|
} |
161
|
|
|
$c = $this->getRealCurrentLineNb() + 1; |
162
|
|
|
$parser = new self($c); |
163
|
|
|
$parser->refs = &$this->refs; |
164
|
|
|
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap); |
165
|
|
|
|
166
|
|
|
if (!is_array($parsed)) { |
167
|
|
|
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
if (isset($parsed[0])) { |
171
|
|
|
// If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes |
172
|
|
|
// and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier |
173
|
|
|
// in the sequence override keys specified in later mapping nodes. |
174
|
|
|
foreach ($parsed as $parsedItem) { |
175
|
|
|
if (!is_array($parsedItem)) { |
176
|
|
|
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
foreach ($parsedItem as $key => $value) { |
180
|
|
|
if (!isset($data[$key])) { |
181
|
|
|
$data[$key] = $value; |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
} else { |
186
|
|
|
// If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the |
187
|
|
|
// current mapping, unless the key already exists in it. |
188
|
|
|
foreach ($parsed as $key => $value) { |
189
|
|
|
if (!isset($data[$key])) { |
190
|
|
|
$data[$key] = $value; |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
View Code Duplication |
} elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) { |
|
|
|
|
196
|
|
|
$isRef = $matches['ref']; |
197
|
|
|
$values['value'] = $matches['value']; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
if ($mergeNode) { |
201
|
|
|
// Merge keys |
202
|
|
|
} elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { |
203
|
|
|
// hash |
204
|
|
|
// if next line is less indented or equal, then it means that the current value is null |
205
|
|
|
if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { |
206
|
|
|
// Spec: Keys MUST be unique; first one wins. |
207
|
|
|
// But overwriting is allowed when a merge node is used in current block. |
208
|
|
|
if ($allowOverwrite || !isset($data[$key])) { |
209
|
|
|
$data[$key] = null; |
210
|
|
|
} |
211
|
|
|
} else { |
212
|
|
|
$c = $this->getRealCurrentLineNb() + 1; |
213
|
|
|
$parser = new self($c); |
214
|
|
|
$parser->refs = &$this->refs; |
215
|
|
|
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap); |
216
|
|
|
// Spec: Keys MUST be unique; first one wins. |
217
|
|
|
// But overwriting is allowed when a merge node is used in current block. |
218
|
|
|
if ($allowOverwrite || !isset($data[$key])) { |
219
|
|
|
$data[$key] = $value; |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
} else { |
223
|
|
|
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap); |
224
|
|
|
// Spec: Keys MUST be unique; first one wins. |
225
|
|
|
// But overwriting is allowed when a merge node is used in current block. |
226
|
|
|
if ($allowOverwrite || !isset($data[$key])) { |
227
|
|
|
$data[$key] = $value; |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
} else { |
231
|
|
|
// multiple documents are not supported |
232
|
|
|
if ('---' === $this->currentLine) { |
233
|
|
|
throw new ParseException('Multiple documents are not supported.'); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
// 1-liner optionally followed by newline(s) |
237
|
|
|
if ($this->lines[0] === trim($value)) { |
238
|
|
|
try { |
239
|
|
|
$value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); |
240
|
|
|
} catch (ParseException $e) { |
241
|
|
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1); |
242
|
|
|
$e->setSnippet($this->currentLine); |
243
|
|
|
|
244
|
|
|
throw $e; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
if (is_array($value)) { |
248
|
|
|
$first = reset($value); |
249
|
|
|
if (is_string($first) && 0 === strpos($first, '*')) { |
250
|
|
|
$data = array(); |
251
|
|
|
foreach ($value as $alias) { |
252
|
|
|
$data[] = $this->refs[substr($alias, 1)]; |
253
|
|
|
} |
254
|
|
|
$value = $data; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
if (isset($mbEncoding)) { |
259
|
|
|
mb_internal_encoding($mbEncoding); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
return $value; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
switch (preg_last_error()) { |
266
|
|
|
case PREG_INTERNAL_ERROR: |
267
|
|
|
$error = 'Internal PCRE error.'; |
268
|
|
|
break; |
269
|
|
|
case PREG_BACKTRACK_LIMIT_ERROR: |
270
|
|
|
$error = 'pcre.backtrack_limit reached.'; |
271
|
|
|
break; |
272
|
|
|
case PREG_RECURSION_LIMIT_ERROR: |
273
|
|
|
$error = 'pcre.recursion_limit reached.'; |
274
|
|
|
break; |
275
|
|
|
case PREG_BAD_UTF8_ERROR: |
276
|
|
|
$error = 'Malformed UTF-8 data.'; |
277
|
|
|
break; |
278
|
|
|
case PREG_BAD_UTF8_OFFSET_ERROR: |
279
|
|
|
$error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; |
280
|
|
|
break; |
281
|
|
|
default: |
282
|
|
|
$error = 'Unable to parse.'; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
if ($isRef) { |
|
|
|
|
289
|
|
|
$this->refs[$isRef] = end($data); |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
if (isset($mbEncoding)) { |
294
|
|
|
mb_internal_encoding($mbEncoding); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
return empty($data) ? null : $data; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Returns the current line number (takes the offset into account). |
302
|
|
|
* |
303
|
|
|
* @return int The current line number |
304
|
|
|
*/ |
305
|
|
|
private function getRealCurrentLineNb() |
306
|
|
|
{ |
307
|
|
|
return $this->currentLineNb + $this->offset; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Returns the current line indentation. |
312
|
|
|
* |
313
|
|
|
* @return int The current line indentation |
314
|
|
|
*/ |
315
|
|
|
private function getCurrentLineIndentation() |
316
|
|
|
{ |
317
|
|
|
return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' ')); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Returns the next embed block of YAML. |
322
|
|
|
* |
323
|
|
|
* @param int $indentation The indent level at which the block is to be read, or null for default |
324
|
|
|
* @param bool $inSequence True if the enclosing data structure is a sequence |
325
|
|
|
* |
326
|
|
|
* @return string A YAML string |
327
|
|
|
* |
328
|
|
|
* @throws ParseException When indentation problem are detected |
329
|
|
|
*/ |
330
|
|
|
private function getNextEmbedBlock($indentation = null, $inSequence = false) |
331
|
|
|
{ |
332
|
|
|
$oldLineIndentation = $this->getCurrentLineIndentation(); |
333
|
|
|
|
334
|
|
|
if (!$this->moveToNextLine()) { |
335
|
|
|
return; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
if (null === $indentation) { |
339
|
|
|
$newIndent = $this->getCurrentLineIndentation(); |
340
|
|
|
|
341
|
|
|
$unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem($this->currentLine); |
|
|
|
|
342
|
|
|
|
343
|
|
|
if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { |
344
|
|
|
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); |
345
|
|
|
} |
346
|
|
|
} else { |
347
|
|
|
$newIndent = $indentation; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
$data = array(); |
351
|
|
|
if ($this->getCurrentLineIndentation() >= $newIndent) { |
352
|
|
|
$data[] = substr($this->currentLine, $newIndent); |
353
|
|
|
} else { |
354
|
|
|
$this->moveToPreviousLine(); |
355
|
|
|
|
356
|
|
|
return; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
if ($inSequence && $oldLineIndentation === $newIndent && '-' === $data[0][0]) { |
360
|
|
|
// the previous line contained a dash but no item content, this line is a sequence item with the same indentation |
361
|
|
|
// and therefore no nested list or mapping |
362
|
|
|
$this->moveToPreviousLine(); |
363
|
|
|
|
364
|
|
|
return; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem($this->currentLine); |
|
|
|
|
368
|
|
|
|
369
|
|
|
// Comments must not be removed inside a string block (ie. after a line ending with "|") |
370
|
|
|
$removeCommentsPattern = '~'.self::FOLDED_SCALAR_PATTERN.'$~'; |
371
|
|
|
$removeComments = !preg_match($removeCommentsPattern, $this->currentLine); |
372
|
|
|
|
373
|
|
|
while ($this->moveToNextLine()) { |
374
|
|
|
$indent = $this->getCurrentLineIndentation(); |
375
|
|
|
|
376
|
|
|
if ($indent === $newIndent) { |
377
|
|
|
$removeComments = !preg_match($removeCommentsPattern, $this->currentLine); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem($this->currentLine) && $newIndent === $indent) { |
|
|
|
|
381
|
|
|
$this->moveToPreviousLine(); |
382
|
|
|
break; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
if ($this->isCurrentLineBlank()) { |
386
|
|
|
$data[] = substr($this->currentLine, $newIndent); |
387
|
|
|
continue; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
if ($removeComments && $this->isCurrentLineComment()) { |
391
|
|
|
continue; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
if ($indent >= $newIndent) { |
395
|
|
|
$data[] = substr($this->currentLine, $newIndent); |
396
|
|
|
} elseif (0 == $indent) { |
397
|
|
|
$this->moveToPreviousLine(); |
398
|
|
|
|
399
|
|
|
break; |
400
|
|
|
} else { |
401
|
|
|
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
return implode("\n", $data); |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Moves the parser to the next line. |
410
|
|
|
* |
411
|
|
|
* @return bool |
412
|
|
|
*/ |
413
|
|
|
private function moveToNextLine() |
414
|
|
|
{ |
415
|
|
|
if ($this->currentLineNb >= count($this->lines) - 1) { |
416
|
|
|
return false; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
$this->currentLine = $this->lines[++$this->currentLineNb]; |
420
|
|
|
|
421
|
|
|
return true; |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* Moves the parser to the previous line. |
426
|
|
|
*/ |
427
|
|
|
private function moveToPreviousLine() |
428
|
|
|
{ |
429
|
|
|
$this->currentLine = $this->lines[--$this->currentLineNb]; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* Parses a YAML value. |
434
|
|
|
* |
435
|
|
|
* @param string $value A YAML value |
436
|
|
|
* @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise |
437
|
|
|
* @param bool $objectSupport True if object support is enabled, false otherwise |
438
|
|
|
* @param bool $objectForMap true if maps should return a stdClass instead of array() |
439
|
|
|
* |
440
|
|
|
* @return mixed A PHP value |
441
|
|
|
* |
442
|
|
|
* @throws ParseException When reference does not exist |
443
|
|
|
*/ |
444
|
|
|
private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap) |
445
|
|
|
{ |
446
|
|
|
if (0 === strpos($value, '*')) { |
447
|
|
View Code Duplication |
if (false !== $pos = strpos($value, '#')) { |
|
|
|
|
448
|
|
|
$value = substr($value, 1, $pos - 2); |
449
|
|
|
} else { |
450
|
|
|
$value = substr($value, 1); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
if (!array_key_exists($value, $this->refs)) { |
454
|
|
|
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
return $this->refs[$value]; |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
if (preg_match('/^'.self::FOLDED_SCALAR_PATTERN.'$/', $value, $matches)) { |
461
|
|
|
$modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; |
462
|
|
|
|
463
|
|
|
return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers)); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
try { |
467
|
|
|
return Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); |
468
|
|
|
} catch (ParseException $e) { |
469
|
|
|
$e->setParsedLine($this->getRealCurrentLineNb() + 1); |
470
|
|
|
$e->setSnippet($this->currentLine); |
471
|
|
|
|
472
|
|
|
throw $e; |
473
|
|
|
} |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
/** |
477
|
|
|
* Parses a folded scalar. |
478
|
|
|
* |
479
|
|
|
* @param string $separator The separator that was used to begin this folded scalar (| or >) |
480
|
|
|
* @param string $indicator The indicator that was used to begin this folded scalar (+ or -) |
481
|
|
|
* @param int $indentation The indentation that was used to begin this folded scalar |
482
|
|
|
* |
483
|
|
|
* @return string The text value |
484
|
|
|
*/ |
485
|
|
|
private function parseFoldedScalar($separator, $indicator = '', $indentation = 0) |
486
|
|
|
{ |
487
|
|
|
$notEOF = $this->moveToNextLine(); |
488
|
|
|
if (!$notEOF) { |
489
|
|
|
return ''; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
$isCurrentLineBlank = $this->isCurrentLineBlank(); |
493
|
|
|
$text = ''; |
494
|
|
|
|
495
|
|
|
// leading blank lines are consumed before determining indentation |
496
|
|
|
while ($notEOF && $isCurrentLineBlank) { |
497
|
|
|
// newline only if not EOF |
498
|
|
|
if ($notEOF = $this->moveToNextLine()) { |
499
|
|
|
$text .= "\n"; |
500
|
|
|
$isCurrentLineBlank = $this->isCurrentLineBlank(); |
501
|
|
|
} |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
// determine indentation if not specified |
505
|
|
|
if (0 === $indentation) { |
506
|
|
|
if (preg_match('/^ +/', $this->currentLine, $matches)) { |
507
|
|
|
$indentation = strlen($matches[0]); |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
if ($indentation > 0) { |
512
|
|
|
$pattern = sprintf('/^ {%d}(.*)$/', $indentation); |
513
|
|
|
|
514
|
|
|
while ( |
515
|
|
|
$notEOF && ( |
516
|
|
|
$isCurrentLineBlank || |
517
|
|
|
preg_match($pattern, $this->currentLine, $matches) |
518
|
|
|
) |
519
|
|
|
) { |
520
|
|
|
if ($isCurrentLineBlank) { |
521
|
|
|
$text .= substr($this->currentLine, $indentation); |
522
|
|
|
} else { |
523
|
|
|
$text .= $matches[1]; |
|
|
|
|
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
// newline only if not EOF |
527
|
|
|
if ($notEOF = $this->moveToNextLine()) { |
528
|
|
|
$text .= "\n"; |
529
|
|
|
$isCurrentLineBlank = $this->isCurrentLineBlank(); |
530
|
|
|
} |
531
|
|
|
} |
532
|
|
|
} elseif ($notEOF) { |
533
|
|
|
$text .= "\n"; |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
if ($notEOF) { |
537
|
|
|
$this->moveToPreviousLine(); |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
// replace all non-trailing single newlines with spaces in folded blocks |
541
|
|
|
if ('>' === $separator) { |
542
|
|
|
preg_match('/(\n*)$/', $text, $matches); |
543
|
|
|
$text = preg_replace('/(?<!\n)\n(?!\n)/', ' ', rtrim($text, "\n")); |
544
|
|
|
$text .= $matches[1]; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
// deal with trailing newlines as indicated |
548
|
|
|
if ('' === $indicator) { |
549
|
|
|
$text = preg_replace('/\n+$/s', "\n", $text); |
550
|
|
|
} elseif ('-' === $indicator) { |
551
|
|
|
$text = preg_replace('/\n+$/s', '', $text); |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
return $text; |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
/** |
558
|
|
|
* Returns true if the next line is indented. |
559
|
|
|
* |
560
|
|
|
* @return bool Returns true if the next line is indented, false otherwise |
561
|
|
|
*/ |
562
|
|
|
private function isNextLineIndented() |
563
|
|
|
{ |
564
|
|
|
$currentIndentation = $this->getCurrentLineIndentation(); |
565
|
|
|
$EOF = !$this->moveToNextLine(); |
566
|
|
|
|
567
|
|
|
while (!$EOF && $this->isCurrentLineEmpty()) { |
568
|
|
|
$EOF = !$this->moveToNextLine(); |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
if ($EOF) { |
572
|
|
|
return false; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
$ret = false; |
576
|
|
|
if ($this->getCurrentLineIndentation() > $currentIndentation) { |
577
|
|
|
$ret = true; |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
$this->moveToPreviousLine(); |
581
|
|
|
|
582
|
|
|
return $ret; |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
/** |
586
|
|
|
* Returns true if the current line is blank or if it is a comment line. |
587
|
|
|
* |
588
|
|
|
* @return bool Returns true if the current line is empty or if it is a comment line, false otherwise |
589
|
|
|
*/ |
590
|
|
|
private function isCurrentLineEmpty() |
591
|
|
|
{ |
592
|
|
|
return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* Returns true if the current line is blank. |
597
|
|
|
* |
598
|
|
|
* @return bool Returns true if the current line is blank, false otherwise |
599
|
|
|
*/ |
600
|
|
|
private function isCurrentLineBlank() |
601
|
|
|
{ |
602
|
|
|
return '' == trim($this->currentLine, ' '); |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
/** |
606
|
|
|
* Returns true if the current line is a comment line. |
607
|
|
|
* |
608
|
|
|
* @return bool Returns true if the current line is a comment line, false otherwise |
609
|
|
|
*/ |
610
|
|
|
private function isCurrentLineComment() |
611
|
|
|
{ |
612
|
|
|
//checking explicitly the first char of the trim is faster than loops or strpos |
613
|
|
|
$ltrimmedLine = ltrim($this->currentLine, ' '); |
614
|
|
|
|
615
|
|
|
return $ltrimmedLine[0] === '#'; |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
/** |
619
|
|
|
* Cleanups a YAML string to be parsed. |
620
|
|
|
* |
621
|
|
|
* @param string $value The input YAML string |
622
|
|
|
* |
623
|
|
|
* @return string A cleaned up YAML string |
624
|
|
|
*/ |
625
|
|
|
private function cleanup($value) |
626
|
|
|
{ |
627
|
|
|
$value = str_replace(array("\r\n", "\r"), "\n", $value); |
628
|
|
|
|
629
|
|
|
// strip YAML header |
630
|
|
|
$count = 0; |
631
|
|
|
$value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); |
632
|
|
|
$this->offset += $count; |
633
|
|
|
|
634
|
|
|
// remove leading comments |
635
|
|
|
$trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); |
636
|
|
|
if ($count == 1) { |
637
|
|
|
// items have been removed, update the offset |
638
|
|
|
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
639
|
|
|
$value = $trimmedValue; |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
// remove start of the document marker (---) |
643
|
|
|
$trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); |
644
|
|
|
if ($count == 1) { |
645
|
|
|
// items have been removed, update the offset |
646
|
|
|
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); |
647
|
|
|
$value = $trimmedValue; |
648
|
|
|
|
649
|
|
|
// remove end of the document marker (...) |
650
|
|
|
$value = preg_replace('#\.\.\.\s*$#s', '', $value); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
return $value; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* Returns true if the next line starts unindented collection. |
658
|
|
|
* |
659
|
|
|
* @return bool Returns true if the next line starts unindented collection, false otherwise |
660
|
|
|
*/ |
661
|
|
|
private function isNextLineUnIndentedCollection() |
662
|
|
|
{ |
663
|
|
|
$currentIndentation = $this->getCurrentLineIndentation(); |
664
|
|
|
$notEOF = $this->moveToNextLine(); |
665
|
|
|
|
666
|
|
|
while ($notEOF && $this->isCurrentLineEmpty()) { |
667
|
|
|
$notEOF = $this->moveToNextLine(); |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
if (false === $notEOF) { |
671
|
|
|
return false; |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
$ret = false; |
675
|
|
|
if ( |
676
|
|
|
$this->getCurrentLineIndentation() == $currentIndentation |
677
|
|
|
&& |
678
|
|
|
$this->isStringUnIndentedCollectionItem($this->currentLine) |
|
|
|
|
679
|
|
|
) { |
680
|
|
|
$ret = true; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
$this->moveToPreviousLine(); |
684
|
|
|
|
685
|
|
|
return $ret; |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
/** |
689
|
|
|
* Returns true if the string is un-indented collection item. |
690
|
|
|
* |
691
|
|
|
* @return bool Returns true if the string is un-indented collection item, false otherwise |
692
|
|
|
*/ |
693
|
|
|
private function isStringUnIndentedCollectionItem() |
694
|
|
|
{ |
695
|
|
|
return (0 === strpos($this->currentLine, '- ')); |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.