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