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