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
|
Chris@0
|
16 /**
|
Chris@0
|
17 * Parser parses YAML strings to convert them to PHP arrays.
|
Chris@0
|
18 *
|
Chris@0
|
19 * @author Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
20 */
|
Chris@0
|
21 class Parser
|
Chris@0
|
22 {
|
Chris@0
|
23 const TAG_PATTERN = '((?P<tag>![\w!.\/:-]+) +)?';
|
Chris@0
|
24 const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
|
Chris@0
|
25
|
Chris@0
|
26 private $offset = 0;
|
Chris@0
|
27 private $totalNumberOfLines;
|
Chris@0
|
28 private $lines = array();
|
Chris@0
|
29 private $currentLineNb = -1;
|
Chris@0
|
30 private $currentLine = '';
|
Chris@0
|
31 private $refs = array();
|
Chris@0
|
32 private $skippedLineNumbers = array();
|
Chris@0
|
33 private $locallySkippedLineNumbers = array();
|
Chris@0
|
34
|
Chris@0
|
35 /**
|
Chris@0
|
36 * Constructor.
|
Chris@0
|
37 *
|
Chris@0
|
38 * @param int $offset The offset of YAML document (used for line numbers in error messages)
|
Chris@0
|
39 * @param int|null $totalNumberOfLines The overall number of lines being parsed
|
Chris@0
|
40 * @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser
|
Chris@0
|
41 */
|
Chris@0
|
42 public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
|
Chris@0
|
43 {
|
Chris@0
|
44 $this->offset = $offset;
|
Chris@0
|
45 $this->totalNumberOfLines = $totalNumberOfLines;
|
Chris@0
|
46 $this->skippedLineNumbers = $skippedLineNumbers;
|
Chris@0
|
47 }
|
Chris@0
|
48
|
Chris@0
|
49 /**
|
Chris@0
|
50 * Parses a YAML string to a PHP value.
|
Chris@0
|
51 *
|
Chris@0
|
52 * @param string $value A YAML string
|
Chris@0
|
53 * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
|
Chris@0
|
54 *
|
Chris@0
|
55 * @return mixed A PHP value
|
Chris@0
|
56 *
|
Chris@0
|
57 * @throws ParseException If the YAML is not valid
|
Chris@0
|
58 */
|
Chris@0
|
59 public function parse($value, $flags = 0)
|
Chris@0
|
60 {
|
Chris@0
|
61 if (is_bool($flags)) {
|
Chris@0
|
62 @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
|
Chris@0
|
63
|
Chris@0
|
64 if ($flags) {
|
Chris@0
|
65 $flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
|
Chris@0
|
66 } else {
|
Chris@0
|
67 $flags = 0;
|
Chris@0
|
68 }
|
Chris@0
|
69 }
|
Chris@0
|
70
|
Chris@0
|
71 if (func_num_args() >= 3) {
|
Chris@0
|
72 @trigger_error('Passing a boolean flag to toggle object support is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.', E_USER_DEPRECATED);
|
Chris@0
|
73
|
Chris@0
|
74 if (func_get_arg(2)) {
|
Chris@0
|
75 $flags |= Yaml::PARSE_OBJECT;
|
Chris@0
|
76 }
|
Chris@0
|
77 }
|
Chris@0
|
78
|
Chris@0
|
79 if (func_num_args() >= 4) {
|
Chris@0
|
80 @trigger_error('Passing a boolean flag to toggle object for map support is deprecated since version 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.', E_USER_DEPRECATED);
|
Chris@0
|
81
|
Chris@0
|
82 if (func_get_arg(3)) {
|
Chris@0
|
83 $flags |= Yaml::PARSE_OBJECT_FOR_MAP;
|
Chris@0
|
84 }
|
Chris@0
|
85 }
|
Chris@0
|
86
|
Chris@0
|
87 if (false === preg_match('//u', $value)) {
|
Chris@0
|
88 throw new ParseException('The YAML value does not appear to be valid UTF-8.');
|
Chris@0
|
89 }
|
Chris@0
|
90
|
Chris@0
|
91 $this->refs = array();
|
Chris@0
|
92
|
Chris@0
|
93 $mbEncoding = null;
|
Chris@0
|
94 $e = null;
|
Chris@0
|
95 $data = null;
|
Chris@0
|
96
|
Chris@0
|
97 if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
|
Chris@0
|
98 $mbEncoding = mb_internal_encoding();
|
Chris@0
|
99 mb_internal_encoding('UTF-8');
|
Chris@0
|
100 }
|
Chris@0
|
101
|
Chris@0
|
102 try {
|
Chris@0
|
103 $data = $this->doParse($value, $flags);
|
Chris@0
|
104 } catch (\Exception $e) {
|
Chris@0
|
105 } catch (\Throwable $e) {
|
Chris@0
|
106 }
|
Chris@0
|
107
|
Chris@0
|
108 if (null !== $mbEncoding) {
|
Chris@0
|
109 mb_internal_encoding($mbEncoding);
|
Chris@0
|
110 }
|
Chris@0
|
111
|
Chris@0
|
112 $this->lines = array();
|
Chris@0
|
113 $this->currentLine = '';
|
Chris@0
|
114 $this->refs = array();
|
Chris@0
|
115 $this->skippedLineNumbers = array();
|
Chris@0
|
116 $this->locallySkippedLineNumbers = array();
|
Chris@0
|
117
|
Chris@0
|
118 if (null !== $e) {
|
Chris@0
|
119 throw $e;
|
Chris@0
|
120 }
|
Chris@0
|
121
|
Chris@0
|
122 return $data;
|
Chris@0
|
123 }
|
Chris@0
|
124
|
Chris@0
|
125 private function doParse($value, $flags)
|
Chris@0
|
126 {
|
Chris@0
|
127 $this->currentLineNb = -1;
|
Chris@0
|
128 $this->currentLine = '';
|
Chris@0
|
129 $value = $this->cleanup($value);
|
Chris@0
|
130 $this->lines = explode("\n", $value);
|
Chris@0
|
131 $this->locallySkippedLineNumbers = array();
|
Chris@0
|
132
|
Chris@0
|
133 if (null === $this->totalNumberOfLines) {
|
Chris@0
|
134 $this->totalNumberOfLines = count($this->lines);
|
Chris@0
|
135 }
|
Chris@0
|
136
|
Chris@0
|
137 $data = array();
|
Chris@0
|
138 $context = null;
|
Chris@0
|
139 $allowOverwrite = false;
|
Chris@0
|
140
|
Chris@0
|
141 while ($this->moveToNextLine()) {
|
Chris@0
|
142 if ($this->isCurrentLineEmpty()) {
|
Chris@0
|
143 continue;
|
Chris@0
|
144 }
|
Chris@0
|
145
|
Chris@0
|
146 // tab?
|
Chris@0
|
147 if ("\t" === $this->currentLine[0]) {
|
Chris@0
|
148 throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
149 }
|
Chris@0
|
150
|
Chris@0
|
151 $isRef = $mergeNode = false;
|
Chris@0
|
152 if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
|
Chris@0
|
153 if ($context && 'mapping' == $context) {
|
Chris@0
|
154 throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
155 }
|
Chris@0
|
156 $context = 'sequence';
|
Chris@0
|
157
|
Chris@0
|
158 if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
|
Chris@0
|
159 $isRef = $matches['ref'];
|
Chris@0
|
160 $values['value'] = $matches['value'];
|
Chris@0
|
161 }
|
Chris@0
|
162
|
Chris@0
|
163 // array
|
Chris@0
|
164 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
|
Chris@0
|
165 $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags);
|
Chris@0
|
166 } else {
|
Chris@0
|
167 if (isset($values['leadspaces'])
|
Chris@0
|
168 && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
|
Chris@0
|
169 ) {
|
Chris@0
|
170 // this is a compact notation element, add to next block and parse
|
Chris@0
|
171 $block = $values['value'];
|
Chris@0
|
172 if ($this->isNextLineIndented()) {
|
Chris@0
|
173 $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
|
Chris@0
|
174 }
|
Chris@0
|
175
|
Chris@0
|
176 $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
|
Chris@0
|
177 } else {
|
Chris@0
|
178 $data[] = $this->parseValue($values['value'], $flags, $context);
|
Chris@0
|
179 }
|
Chris@0
|
180 }
|
Chris@0
|
181 if ($isRef) {
|
Chris@0
|
182 $this->refs[$isRef] = end($data);
|
Chris@0
|
183 }
|
Chris@0
|
184 } elseif (
|
Chris@0
|
185 self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
|
Chris@0
|
186 && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
|
Chris@0
|
187 ) {
|
Chris@0
|
188 if ($context && 'sequence' == $context) {
|
Chris@0
|
189 throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
|
Chris@0
|
190 }
|
Chris@0
|
191 $context = 'mapping';
|
Chris@0
|
192
|
Chris@0
|
193 // force correct settings
|
Chris@0
|
194 Inline::parse(null, $flags, $this->refs);
|
Chris@0
|
195 try {
|
Chris@0
|
196 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
|
Chris@0
|
197 $key = Inline::parseScalar($values['key']);
|
Chris@0
|
198 } catch (ParseException $e) {
|
Chris@0
|
199 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
Chris@0
|
200 $e->setSnippet($this->currentLine);
|
Chris@0
|
201
|
Chris@0
|
202 throw $e;
|
Chris@0
|
203 }
|
Chris@0
|
204
|
Chris@0
|
205 // Convert float keys to strings, to avoid being converted to integers by PHP
|
Chris@0
|
206 if (is_float($key)) {
|
Chris@0
|
207 $key = (string) $key;
|
Chris@0
|
208 }
|
Chris@0
|
209
|
Chris@0
|
210 if ('<<' === $key) {
|
Chris@0
|
211 $mergeNode = true;
|
Chris@0
|
212 $allowOverwrite = true;
|
Chris@0
|
213 if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
|
Chris@0
|
214 $refName = substr($values['value'], 1);
|
Chris@0
|
215 if (!array_key_exists($refName, $this->refs)) {
|
Chris@0
|
216 throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
217 }
|
Chris@0
|
218
|
Chris@0
|
219 $refValue = $this->refs[$refName];
|
Chris@0
|
220
|
Chris@0
|
221 if (!is_array($refValue)) {
|
Chris@0
|
222 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
223 }
|
Chris@0
|
224
|
Chris@0
|
225 $data += $refValue; // array union
|
Chris@0
|
226 } else {
|
Chris@0
|
227 if (isset($values['value']) && $values['value'] !== '') {
|
Chris@0
|
228 $value = $values['value'];
|
Chris@0
|
229 } else {
|
Chris@0
|
230 $value = $this->getNextEmbedBlock();
|
Chris@0
|
231 }
|
Chris@0
|
232 $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
|
Chris@0
|
233
|
Chris@0
|
234 if (!is_array($parsed)) {
|
Chris@0
|
235 throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
236 }
|
Chris@0
|
237
|
Chris@0
|
238 if (isset($parsed[0])) {
|
Chris@0
|
239 // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
|
Chris@0
|
240 // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
|
Chris@0
|
241 // in the sequence override keys specified in later mapping nodes.
|
Chris@0
|
242 foreach ($parsed as $parsedItem) {
|
Chris@0
|
243 if (!is_array($parsedItem)) {
|
Chris@0
|
244 throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
|
Chris@0
|
245 }
|
Chris@0
|
246
|
Chris@0
|
247 $data += $parsedItem; // array union
|
Chris@0
|
248 }
|
Chris@0
|
249 } else {
|
Chris@0
|
250 // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
|
Chris@0
|
251 // current mapping, unless the key already exists in it.
|
Chris@0
|
252 $data += $parsed; // array union
|
Chris@0
|
253 }
|
Chris@0
|
254 }
|
Chris@0
|
255 } elseif (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
|
Chris@0
|
256 $isRef = $matches['ref'];
|
Chris@0
|
257 $values['value'] = $matches['value'];
|
Chris@0
|
258 }
|
Chris@0
|
259
|
Chris@0
|
260 if ($mergeNode) {
|
Chris@0
|
261 // Merge keys
|
Chris@0
|
262 } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
|
Chris@0
|
263 // hash
|
Chris@0
|
264 // if next line is less indented or equal, then it means that the current value is null
|
Chris@0
|
265 if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
|
Chris@0
|
266 // Spec: Keys MUST be unique; first one wins.
|
Chris@0
|
267 // But overwriting is allowed when a merge node is used in current block.
|
Chris@0
|
268 if ($allowOverwrite || !isset($data[$key])) {
|
Chris@0
|
269 $data[$key] = null;
|
Chris@0
|
270 } else {
|
Chris@0
|
271 @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
|
Chris@0
|
272 }
|
Chris@0
|
273 } else {
|
Chris@0
|
274 // remember the parsed line number here in case we need it to provide some contexts in error messages below
|
Chris@0
|
275 $realCurrentLineNbKey = $this->getRealCurrentLineNb();
|
Chris@0
|
276 $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
|
Chris@0
|
277 // Spec: Keys MUST be unique; first one wins.
|
Chris@0
|
278 // But overwriting is allowed when a merge node is used in current block.
|
Chris@0
|
279 if ($allowOverwrite || !isset($data[$key])) {
|
Chris@0
|
280 $data[$key] = $value;
|
Chris@0
|
281 } else {
|
Chris@0
|
282 @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $realCurrentLineNbKey + 1), E_USER_DEPRECATED);
|
Chris@0
|
283 }
|
Chris@0
|
284 }
|
Chris@0
|
285 } else {
|
Chris@0
|
286 $value = $this->parseValue($values['value'], $flags, $context);
|
Chris@0
|
287 // Spec: Keys MUST be unique; first one wins.
|
Chris@0
|
288 // But overwriting is allowed when a merge node is used in current block.
|
Chris@0
|
289 if ($allowOverwrite || !isset($data[$key])) {
|
Chris@0
|
290 $data[$key] = $value;
|
Chris@0
|
291 } else {
|
Chris@0
|
292 @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
|
Chris@0
|
293 }
|
Chris@0
|
294 }
|
Chris@0
|
295 if ($isRef) {
|
Chris@0
|
296 $this->refs[$isRef] = $data[$key];
|
Chris@0
|
297 }
|
Chris@0
|
298 } else {
|
Chris@0
|
299 // multiple documents are not supported
|
Chris@0
|
300 if ('---' === $this->currentLine) {
|
Chris@0
|
301 throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
|
Chris@0
|
302 }
|
Chris@0
|
303
|
Chris@0
|
304 // 1-liner optionally followed by newline(s)
|
Chris@0
|
305 if (is_string($value) && $this->lines[0] === trim($value)) {
|
Chris@0
|
306 try {
|
Chris@0
|
307 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
|
Chris@0
|
308 $value = Inline::parse($this->lines[0], $flags, $this->refs);
|
Chris@0
|
309 } catch (ParseException $e) {
|
Chris@0
|
310 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
Chris@0
|
311 $e->setSnippet($this->currentLine);
|
Chris@0
|
312
|
Chris@0
|
313 throw $e;
|
Chris@0
|
314 }
|
Chris@0
|
315
|
Chris@0
|
316 return $value;
|
Chris@0
|
317 }
|
Chris@0
|
318
|
Chris@0
|
319 throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
320 }
|
Chris@0
|
321 }
|
Chris@0
|
322
|
Chris@0
|
323 if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !is_object($data) && 'mapping' === $context) {
|
Chris@0
|
324 $object = new \stdClass();
|
Chris@0
|
325
|
Chris@0
|
326 foreach ($data as $key => $value) {
|
Chris@0
|
327 $object->$key = $value;
|
Chris@0
|
328 }
|
Chris@0
|
329
|
Chris@0
|
330 $data = $object;
|
Chris@0
|
331 }
|
Chris@0
|
332
|
Chris@0
|
333 return empty($data) ? null : $data;
|
Chris@0
|
334 }
|
Chris@0
|
335
|
Chris@0
|
336 private function parseBlock($offset, $yaml, $flags)
|
Chris@0
|
337 {
|
Chris@0
|
338 $skippedLineNumbers = $this->skippedLineNumbers;
|
Chris@0
|
339
|
Chris@0
|
340 foreach ($this->locallySkippedLineNumbers as $lineNumber) {
|
Chris@0
|
341 if ($lineNumber < $offset) {
|
Chris@0
|
342 continue;
|
Chris@0
|
343 }
|
Chris@0
|
344
|
Chris@0
|
345 $skippedLineNumbers[] = $lineNumber;
|
Chris@0
|
346 }
|
Chris@0
|
347
|
Chris@0
|
348 $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
|
Chris@0
|
349 $parser->refs = &$this->refs;
|
Chris@0
|
350
|
Chris@0
|
351 return $parser->doParse($yaml, $flags);
|
Chris@0
|
352 }
|
Chris@0
|
353
|
Chris@0
|
354 /**
|
Chris@0
|
355 * Returns the current line number (takes the offset into account).
|
Chris@0
|
356 *
|
Chris@0
|
357 * @return int The current line number
|
Chris@0
|
358 */
|
Chris@0
|
359 private function getRealCurrentLineNb()
|
Chris@0
|
360 {
|
Chris@0
|
361 $realCurrentLineNumber = $this->currentLineNb + $this->offset;
|
Chris@0
|
362
|
Chris@0
|
363 foreach ($this->skippedLineNumbers as $skippedLineNumber) {
|
Chris@0
|
364 if ($skippedLineNumber > $realCurrentLineNumber) {
|
Chris@0
|
365 break;
|
Chris@0
|
366 }
|
Chris@0
|
367
|
Chris@0
|
368 ++$realCurrentLineNumber;
|
Chris@0
|
369 }
|
Chris@0
|
370
|
Chris@0
|
371 return $realCurrentLineNumber;
|
Chris@0
|
372 }
|
Chris@0
|
373
|
Chris@0
|
374 /**
|
Chris@0
|
375 * Returns the current line indentation.
|
Chris@0
|
376 *
|
Chris@0
|
377 * @return int The current line indentation
|
Chris@0
|
378 */
|
Chris@0
|
379 private function getCurrentLineIndentation()
|
Chris@0
|
380 {
|
Chris@0
|
381 return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
|
Chris@0
|
382 }
|
Chris@0
|
383
|
Chris@0
|
384 /**
|
Chris@0
|
385 * Returns the next embed block of YAML.
|
Chris@0
|
386 *
|
Chris@0
|
387 * @param int $indentation The indent level at which the block is to be read, or null for default
|
Chris@0
|
388 * @param bool $inSequence True if the enclosing data structure is a sequence
|
Chris@0
|
389 *
|
Chris@0
|
390 * @return string A YAML string
|
Chris@0
|
391 *
|
Chris@0
|
392 * @throws ParseException When indentation problem are detected
|
Chris@0
|
393 */
|
Chris@0
|
394 private function getNextEmbedBlock($indentation = null, $inSequence = false)
|
Chris@0
|
395 {
|
Chris@0
|
396 $oldLineIndentation = $this->getCurrentLineIndentation();
|
Chris@0
|
397 $blockScalarIndentations = array();
|
Chris@0
|
398
|
Chris@0
|
399 if ($this->isBlockScalarHeader()) {
|
Chris@0
|
400 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
|
Chris@0
|
401 }
|
Chris@0
|
402
|
Chris@0
|
403 if (!$this->moveToNextLine()) {
|
Chris@0
|
404 return;
|
Chris@0
|
405 }
|
Chris@0
|
406
|
Chris@0
|
407 if (null === $indentation) {
|
Chris@0
|
408 $newIndent = $this->getCurrentLineIndentation();
|
Chris@0
|
409
|
Chris@0
|
410 $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
|
Chris@0
|
411
|
Chris@0
|
412 if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
|
Chris@0
|
413 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
414 }
|
Chris@0
|
415 } else {
|
Chris@0
|
416 $newIndent = $indentation;
|
Chris@0
|
417 }
|
Chris@0
|
418
|
Chris@0
|
419 $data = array();
|
Chris@0
|
420 if ($this->getCurrentLineIndentation() >= $newIndent) {
|
Chris@0
|
421 $data[] = substr($this->currentLine, $newIndent);
|
Chris@0
|
422 } else {
|
Chris@0
|
423 $this->moveToPreviousLine();
|
Chris@0
|
424
|
Chris@0
|
425 return;
|
Chris@0
|
426 }
|
Chris@0
|
427
|
Chris@0
|
428 if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
|
Chris@0
|
429 // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
|
Chris@0
|
430 // and therefore no nested list or mapping
|
Chris@0
|
431 $this->moveToPreviousLine();
|
Chris@0
|
432
|
Chris@0
|
433 return;
|
Chris@0
|
434 }
|
Chris@0
|
435
|
Chris@0
|
436 $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
|
Chris@0
|
437
|
Chris@0
|
438 if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
|
Chris@0
|
439 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
|
Chris@0
|
440 }
|
Chris@0
|
441
|
Chris@0
|
442 $previousLineIndentation = $this->getCurrentLineIndentation();
|
Chris@0
|
443
|
Chris@0
|
444 while ($this->moveToNextLine()) {
|
Chris@0
|
445 $indent = $this->getCurrentLineIndentation();
|
Chris@0
|
446
|
Chris@0
|
447 // terminate all block scalars that are more indented than the current line
|
Chris@0
|
448 if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
|
Chris@0
|
449 foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
|
Chris@0
|
450 if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
|
Chris@0
|
451 unset($blockScalarIndentations[$key]);
|
Chris@0
|
452 }
|
Chris@0
|
453 }
|
Chris@0
|
454 }
|
Chris@0
|
455
|
Chris@0
|
456 if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
|
Chris@0
|
457 $blockScalarIndentations[] = $this->getCurrentLineIndentation();
|
Chris@0
|
458 }
|
Chris@0
|
459
|
Chris@0
|
460 $previousLineIndentation = $indent;
|
Chris@0
|
461
|
Chris@0
|
462 if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
|
Chris@0
|
463 $this->moveToPreviousLine();
|
Chris@0
|
464 break;
|
Chris@0
|
465 }
|
Chris@0
|
466
|
Chris@0
|
467 if ($this->isCurrentLineBlank()) {
|
Chris@0
|
468 $data[] = substr($this->currentLine, $newIndent);
|
Chris@0
|
469 continue;
|
Chris@0
|
470 }
|
Chris@0
|
471
|
Chris@0
|
472 // we ignore "comment" lines only when we are not inside a scalar block
|
Chris@0
|
473 if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
|
Chris@0
|
474 // remember ignored comment lines (they are used later in nested
|
Chris@0
|
475 // parser calls to determine real line numbers)
|
Chris@0
|
476 //
|
Chris@0
|
477 // CAUTION: beware to not populate the global property here as it
|
Chris@0
|
478 // will otherwise influence the getRealCurrentLineNb() call here
|
Chris@0
|
479 // for consecutive comment lines and subsequent embedded blocks
|
Chris@0
|
480 $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
|
Chris@0
|
481
|
Chris@0
|
482 continue;
|
Chris@0
|
483 }
|
Chris@0
|
484
|
Chris@0
|
485 if ($indent >= $newIndent) {
|
Chris@0
|
486 $data[] = substr($this->currentLine, $newIndent);
|
Chris@0
|
487 } elseif (0 == $indent) {
|
Chris@0
|
488 $this->moveToPreviousLine();
|
Chris@0
|
489
|
Chris@0
|
490 break;
|
Chris@0
|
491 } else {
|
Chris@0
|
492 throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
|
Chris@0
|
493 }
|
Chris@0
|
494 }
|
Chris@0
|
495
|
Chris@0
|
496 return implode("\n", $data);
|
Chris@0
|
497 }
|
Chris@0
|
498
|
Chris@0
|
499 /**
|
Chris@0
|
500 * Moves the parser to the next line.
|
Chris@0
|
501 *
|
Chris@0
|
502 * @return bool
|
Chris@0
|
503 */
|
Chris@0
|
504 private function moveToNextLine()
|
Chris@0
|
505 {
|
Chris@0
|
506 if ($this->currentLineNb >= count($this->lines) - 1) {
|
Chris@0
|
507 return false;
|
Chris@0
|
508 }
|
Chris@0
|
509
|
Chris@0
|
510 $this->currentLine = $this->lines[++$this->currentLineNb];
|
Chris@0
|
511
|
Chris@0
|
512 return true;
|
Chris@0
|
513 }
|
Chris@0
|
514
|
Chris@0
|
515 /**
|
Chris@0
|
516 * Moves the parser to the previous line.
|
Chris@0
|
517 *
|
Chris@0
|
518 * @return bool
|
Chris@0
|
519 */
|
Chris@0
|
520 private function moveToPreviousLine()
|
Chris@0
|
521 {
|
Chris@0
|
522 if ($this->currentLineNb < 1) {
|
Chris@0
|
523 return false;
|
Chris@0
|
524 }
|
Chris@0
|
525
|
Chris@0
|
526 $this->currentLine = $this->lines[--$this->currentLineNb];
|
Chris@0
|
527
|
Chris@0
|
528 return true;
|
Chris@0
|
529 }
|
Chris@0
|
530
|
Chris@0
|
531 /**
|
Chris@0
|
532 * Parses a YAML value.
|
Chris@0
|
533 *
|
Chris@0
|
534 * @param string $value A YAML value
|
Chris@0
|
535 * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
|
Chris@0
|
536 * @param string $context The parser context (either sequence or mapping)
|
Chris@0
|
537 *
|
Chris@0
|
538 * @return mixed A PHP value
|
Chris@0
|
539 *
|
Chris@0
|
540 * @throws ParseException When reference does not exist
|
Chris@0
|
541 */
|
Chris@0
|
542 private function parseValue($value, $flags, $context)
|
Chris@0
|
543 {
|
Chris@0
|
544 if (0 === strpos($value, '*')) {
|
Chris@0
|
545 if (false !== $pos = strpos($value, '#')) {
|
Chris@0
|
546 $value = substr($value, 1, $pos - 2);
|
Chris@0
|
547 } else {
|
Chris@0
|
548 $value = substr($value, 1);
|
Chris@0
|
549 }
|
Chris@0
|
550
|
Chris@0
|
551 if (!array_key_exists($value, $this->refs)) {
|
Chris@0
|
552 throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
|
Chris@0
|
553 }
|
Chris@0
|
554
|
Chris@0
|
555 return $this->refs[$value];
|
Chris@0
|
556 }
|
Chris@0
|
557
|
Chris@0
|
558 if (self::preg_match('/^'.self::TAG_PATTERN.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
|
Chris@0
|
559 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
|
Chris@0
|
560
|
Chris@0
|
561 $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
|
Chris@0
|
562
|
Chris@0
|
563 if (isset($matches['tag']) && '!!binary' === $matches['tag']) {
|
Chris@0
|
564 return Inline::evaluateBinaryScalar($data);
|
Chris@0
|
565 }
|
Chris@0
|
566
|
Chris@0
|
567 return $data;
|
Chris@0
|
568 }
|
Chris@0
|
569
|
Chris@0
|
570 try {
|
Chris@0
|
571 $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
|
Chris@0
|
572
|
Chris@0
|
573 // do not take following lines into account when the current line is a quoted single line value
|
Chris@0
|
574 if (null !== $quotation && preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) {
|
Chris@0
|
575 return Inline::parse($value, $flags, $this->refs);
|
Chris@0
|
576 }
|
Chris@0
|
577
|
Chris@0
|
578 while ($this->moveToNextLine()) {
|
Chris@0
|
579 // unquoted strings end before the first unindented line
|
Chris@0
|
580 if (null === $quotation && $this->getCurrentLineIndentation() === 0) {
|
Chris@0
|
581 $this->moveToPreviousLine();
|
Chris@0
|
582
|
Chris@0
|
583 break;
|
Chris@0
|
584 }
|
Chris@0
|
585
|
Chris@0
|
586 $value .= ' '.trim($this->currentLine);
|
Chris@0
|
587
|
Chris@0
|
588 // quoted string values end with a line that is terminated with the quotation character
|
Chris@0
|
589 if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
|
Chris@0
|
590 break;
|
Chris@0
|
591 }
|
Chris@0
|
592 }
|
Chris@0
|
593
|
Chris@0
|
594 Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
|
Chris@0
|
595 $parsedValue = Inline::parse($value, $flags, $this->refs);
|
Chris@0
|
596
|
Chris@0
|
597 if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
|
Chris@0
|
598 throw new ParseException('A colon cannot be used in an unquoted mapping value.');
|
Chris@0
|
599 }
|
Chris@0
|
600
|
Chris@0
|
601 return $parsedValue;
|
Chris@0
|
602 } catch (ParseException $e) {
|
Chris@0
|
603 $e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
Chris@0
|
604 $e->setSnippet($this->currentLine);
|
Chris@0
|
605
|
Chris@0
|
606 throw $e;
|
Chris@0
|
607 }
|
Chris@0
|
608 }
|
Chris@0
|
609
|
Chris@0
|
610 /**
|
Chris@0
|
611 * Parses a block scalar.
|
Chris@0
|
612 *
|
Chris@0
|
613 * @param string $style The style indicator that was used to begin this block scalar (| or >)
|
Chris@0
|
614 * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
|
Chris@0
|
615 * @param int $indentation The indentation indicator that was used to begin this block scalar
|
Chris@0
|
616 *
|
Chris@0
|
617 * @return string The text value
|
Chris@0
|
618 */
|
Chris@0
|
619 private function parseBlockScalar($style, $chomping = '', $indentation = 0)
|
Chris@0
|
620 {
|
Chris@0
|
621 $notEOF = $this->moveToNextLine();
|
Chris@0
|
622 if (!$notEOF) {
|
Chris@0
|
623 return '';
|
Chris@0
|
624 }
|
Chris@0
|
625
|
Chris@0
|
626 $isCurrentLineBlank = $this->isCurrentLineBlank();
|
Chris@0
|
627 $blockLines = array();
|
Chris@0
|
628
|
Chris@0
|
629 // leading blank lines are consumed before determining indentation
|
Chris@0
|
630 while ($notEOF && $isCurrentLineBlank) {
|
Chris@0
|
631 // newline only if not EOF
|
Chris@0
|
632 if ($notEOF = $this->moveToNextLine()) {
|
Chris@0
|
633 $blockLines[] = '';
|
Chris@0
|
634 $isCurrentLineBlank = $this->isCurrentLineBlank();
|
Chris@0
|
635 }
|
Chris@0
|
636 }
|
Chris@0
|
637
|
Chris@0
|
638 // determine indentation if not specified
|
Chris@0
|
639 if (0 === $indentation) {
|
Chris@0
|
640 if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
|
Chris@0
|
641 $indentation = strlen($matches[0]);
|
Chris@0
|
642 }
|
Chris@0
|
643 }
|
Chris@0
|
644
|
Chris@0
|
645 if ($indentation > 0) {
|
Chris@0
|
646 $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
|
Chris@0
|
647
|
Chris@0
|
648 while (
|
Chris@0
|
649 $notEOF && (
|
Chris@0
|
650 $isCurrentLineBlank ||
|
Chris@0
|
651 self::preg_match($pattern, $this->currentLine, $matches)
|
Chris@0
|
652 )
|
Chris@0
|
653 ) {
|
Chris@0
|
654 if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
|
Chris@0
|
655 $blockLines[] = substr($this->currentLine, $indentation);
|
Chris@0
|
656 } elseif ($isCurrentLineBlank) {
|
Chris@0
|
657 $blockLines[] = '';
|
Chris@0
|
658 } else {
|
Chris@0
|
659 $blockLines[] = $matches[1];
|
Chris@0
|
660 }
|
Chris@0
|
661
|
Chris@0
|
662 // newline only if not EOF
|
Chris@0
|
663 if ($notEOF = $this->moveToNextLine()) {
|
Chris@0
|
664 $isCurrentLineBlank = $this->isCurrentLineBlank();
|
Chris@0
|
665 }
|
Chris@0
|
666 }
|
Chris@0
|
667 } elseif ($notEOF) {
|
Chris@0
|
668 $blockLines[] = '';
|
Chris@0
|
669 }
|
Chris@0
|
670
|
Chris@0
|
671 if ($notEOF) {
|
Chris@0
|
672 $blockLines[] = '';
|
Chris@0
|
673 $this->moveToPreviousLine();
|
Chris@0
|
674 } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
|
Chris@0
|
675 $blockLines[] = '';
|
Chris@0
|
676 }
|
Chris@0
|
677
|
Chris@0
|
678 // folded style
|
Chris@0
|
679 if ('>' === $style) {
|
Chris@0
|
680 $text = '';
|
Chris@0
|
681 $previousLineIndented = false;
|
Chris@0
|
682 $previousLineBlank = false;
|
Chris@0
|
683
|
Chris@0
|
684 for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
|
Chris@0
|
685 if ('' === $blockLines[$i]) {
|
Chris@0
|
686 $text .= "\n";
|
Chris@0
|
687 $previousLineIndented = false;
|
Chris@0
|
688 $previousLineBlank = true;
|
Chris@0
|
689 } elseif (' ' === $blockLines[$i][0]) {
|
Chris@0
|
690 $text .= "\n".$blockLines[$i];
|
Chris@0
|
691 $previousLineIndented = true;
|
Chris@0
|
692 $previousLineBlank = false;
|
Chris@0
|
693 } elseif ($previousLineIndented) {
|
Chris@0
|
694 $text .= "\n".$blockLines[$i];
|
Chris@0
|
695 $previousLineIndented = false;
|
Chris@0
|
696 $previousLineBlank = false;
|
Chris@0
|
697 } elseif ($previousLineBlank || 0 === $i) {
|
Chris@0
|
698 $text .= $blockLines[$i];
|
Chris@0
|
699 $previousLineIndented = false;
|
Chris@0
|
700 $previousLineBlank = false;
|
Chris@0
|
701 } else {
|
Chris@0
|
702 $text .= ' '.$blockLines[$i];
|
Chris@0
|
703 $previousLineIndented = false;
|
Chris@0
|
704 $previousLineBlank = false;
|
Chris@0
|
705 }
|
Chris@0
|
706 }
|
Chris@0
|
707 } else {
|
Chris@0
|
708 $text = implode("\n", $blockLines);
|
Chris@0
|
709 }
|
Chris@0
|
710
|
Chris@0
|
711 // deal with trailing newlines
|
Chris@0
|
712 if ('' === $chomping) {
|
Chris@0
|
713 $text = preg_replace('/\n+$/', "\n", $text);
|
Chris@0
|
714 } elseif ('-' === $chomping) {
|
Chris@0
|
715 $text = preg_replace('/\n+$/', '', $text);
|
Chris@0
|
716 }
|
Chris@0
|
717
|
Chris@0
|
718 return $text;
|
Chris@0
|
719 }
|
Chris@0
|
720
|
Chris@0
|
721 /**
|
Chris@0
|
722 * Returns true if the next line is indented.
|
Chris@0
|
723 *
|
Chris@0
|
724 * @return bool Returns true if the next line is indented, false otherwise
|
Chris@0
|
725 */
|
Chris@0
|
726 private function isNextLineIndented()
|
Chris@0
|
727 {
|
Chris@0
|
728 $currentIndentation = $this->getCurrentLineIndentation();
|
Chris@0
|
729 $EOF = !$this->moveToNextLine();
|
Chris@0
|
730
|
Chris@0
|
731 while (!$EOF && $this->isCurrentLineEmpty()) {
|
Chris@0
|
732 $EOF = !$this->moveToNextLine();
|
Chris@0
|
733 }
|
Chris@0
|
734
|
Chris@0
|
735 if ($EOF) {
|
Chris@0
|
736 return false;
|
Chris@0
|
737 }
|
Chris@0
|
738
|
Chris@0
|
739 $ret = $this->getCurrentLineIndentation() > $currentIndentation;
|
Chris@0
|
740
|
Chris@0
|
741 $this->moveToPreviousLine();
|
Chris@0
|
742
|
Chris@0
|
743 return $ret;
|
Chris@0
|
744 }
|
Chris@0
|
745
|
Chris@0
|
746 /**
|
Chris@0
|
747 * Returns true if the current line is blank or if it is a comment line.
|
Chris@0
|
748 *
|
Chris@0
|
749 * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
|
Chris@0
|
750 */
|
Chris@0
|
751 private function isCurrentLineEmpty()
|
Chris@0
|
752 {
|
Chris@0
|
753 return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
|
Chris@0
|
754 }
|
Chris@0
|
755
|
Chris@0
|
756 /**
|
Chris@0
|
757 * Returns true if the current line is blank.
|
Chris@0
|
758 *
|
Chris@0
|
759 * @return bool Returns true if the current line is blank, false otherwise
|
Chris@0
|
760 */
|
Chris@0
|
761 private function isCurrentLineBlank()
|
Chris@0
|
762 {
|
Chris@0
|
763 return '' == trim($this->currentLine, ' ');
|
Chris@0
|
764 }
|
Chris@0
|
765
|
Chris@0
|
766 /**
|
Chris@0
|
767 * Returns true if the current line is a comment line.
|
Chris@0
|
768 *
|
Chris@0
|
769 * @return bool Returns true if the current line is a comment line, false otherwise
|
Chris@0
|
770 */
|
Chris@0
|
771 private function isCurrentLineComment()
|
Chris@0
|
772 {
|
Chris@0
|
773 //checking explicitly the first char of the trim is faster than loops or strpos
|
Chris@0
|
774 $ltrimmedLine = ltrim($this->currentLine, ' ');
|
Chris@0
|
775
|
Chris@0
|
776 return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
|
Chris@0
|
777 }
|
Chris@0
|
778
|
Chris@0
|
779 private function isCurrentLineLastLineInDocument()
|
Chris@0
|
780 {
|
Chris@0
|
781 return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
|
Chris@0
|
782 }
|
Chris@0
|
783
|
Chris@0
|
784 /**
|
Chris@0
|
785 * Cleanups a YAML string to be parsed.
|
Chris@0
|
786 *
|
Chris@0
|
787 * @param string $value The input YAML string
|
Chris@0
|
788 *
|
Chris@0
|
789 * @return string A cleaned up YAML string
|
Chris@0
|
790 */
|
Chris@0
|
791 private function cleanup($value)
|
Chris@0
|
792 {
|
Chris@0
|
793 $value = str_replace(array("\r\n", "\r"), "\n", $value);
|
Chris@0
|
794
|
Chris@0
|
795 // strip YAML header
|
Chris@0
|
796 $count = 0;
|
Chris@0
|
797 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
|
Chris@0
|
798 $this->offset += $count;
|
Chris@0
|
799
|
Chris@0
|
800 // remove leading comments
|
Chris@0
|
801 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
|
Chris@0
|
802 if ($count == 1) {
|
Chris@0
|
803 // items have been removed, update the offset
|
Chris@0
|
804 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
|
Chris@0
|
805 $value = $trimmedValue;
|
Chris@0
|
806 }
|
Chris@0
|
807
|
Chris@0
|
808 // remove start of the document marker (---)
|
Chris@0
|
809 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
|
Chris@0
|
810 if ($count == 1) {
|
Chris@0
|
811 // items have been removed, update the offset
|
Chris@0
|
812 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
|
Chris@0
|
813 $value = $trimmedValue;
|
Chris@0
|
814
|
Chris@0
|
815 // remove end of the document marker (...)
|
Chris@0
|
816 $value = preg_replace('#\.\.\.\s*$#', '', $value);
|
Chris@0
|
817 }
|
Chris@0
|
818
|
Chris@0
|
819 return $value;
|
Chris@0
|
820 }
|
Chris@0
|
821
|
Chris@0
|
822 /**
|
Chris@0
|
823 * Returns true if the next line starts unindented collection.
|
Chris@0
|
824 *
|
Chris@0
|
825 * @return bool Returns true if the next line starts unindented collection, false otherwise
|
Chris@0
|
826 */
|
Chris@0
|
827 private function isNextLineUnIndentedCollection()
|
Chris@0
|
828 {
|
Chris@0
|
829 $currentIndentation = $this->getCurrentLineIndentation();
|
Chris@0
|
830 $notEOF = $this->moveToNextLine();
|
Chris@0
|
831
|
Chris@0
|
832 while ($notEOF && $this->isCurrentLineEmpty()) {
|
Chris@0
|
833 $notEOF = $this->moveToNextLine();
|
Chris@0
|
834 }
|
Chris@0
|
835
|
Chris@0
|
836 if (false === $notEOF) {
|
Chris@0
|
837 return false;
|
Chris@0
|
838 }
|
Chris@0
|
839
|
Chris@0
|
840 $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
|
Chris@0
|
841
|
Chris@0
|
842 $this->moveToPreviousLine();
|
Chris@0
|
843
|
Chris@0
|
844 return $ret;
|
Chris@0
|
845 }
|
Chris@0
|
846
|
Chris@0
|
847 /**
|
Chris@0
|
848 * Returns true if the string is un-indented collection item.
|
Chris@0
|
849 *
|
Chris@0
|
850 * @return bool Returns true if the string is un-indented collection item, false otherwise
|
Chris@0
|
851 */
|
Chris@0
|
852 private function isStringUnIndentedCollectionItem()
|
Chris@0
|
853 {
|
Chris@0
|
854 return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
|
Chris@0
|
855 }
|
Chris@0
|
856
|
Chris@0
|
857 /**
|
Chris@0
|
858 * Tests whether or not the current line is the header of a block scalar.
|
Chris@0
|
859 *
|
Chris@0
|
860 * @return bool
|
Chris@0
|
861 */
|
Chris@0
|
862 private function isBlockScalarHeader()
|
Chris@0
|
863 {
|
Chris@0
|
864 return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
|
Chris@0
|
865 }
|
Chris@0
|
866
|
Chris@0
|
867 /**
|
Chris@0
|
868 * A local wrapper for `preg_match` which will throw a ParseException if there
|
Chris@0
|
869 * is an internal error in the PCRE engine.
|
Chris@0
|
870 *
|
Chris@0
|
871 * This avoids us needing to check for "false" every time PCRE is used
|
Chris@0
|
872 * in the YAML engine
|
Chris@0
|
873 *
|
Chris@0
|
874 * @throws ParseException on a PCRE internal error
|
Chris@0
|
875 *
|
Chris@0
|
876 * @see preg_last_error()
|
Chris@0
|
877 *
|
Chris@0
|
878 * @internal
|
Chris@0
|
879 */
|
Chris@0
|
880 public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
|
Chris@0
|
881 {
|
Chris@0
|
882 if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
|
Chris@0
|
883 switch (preg_last_error()) {
|
Chris@0
|
884 case PREG_INTERNAL_ERROR:
|
Chris@0
|
885 $error = 'Internal PCRE error.';
|
Chris@0
|
886 break;
|
Chris@0
|
887 case PREG_BACKTRACK_LIMIT_ERROR:
|
Chris@0
|
888 $error = 'pcre.backtrack_limit reached.';
|
Chris@0
|
889 break;
|
Chris@0
|
890 case PREG_RECURSION_LIMIT_ERROR:
|
Chris@0
|
891 $error = 'pcre.recursion_limit reached.';
|
Chris@0
|
892 break;
|
Chris@0
|
893 case PREG_BAD_UTF8_ERROR:
|
Chris@0
|
894 $error = 'Malformed UTF-8 data.';
|
Chris@0
|
895 break;
|
Chris@0
|
896 case PREG_BAD_UTF8_OFFSET_ERROR:
|
Chris@0
|
897 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
|
Chris@0
|
898 break;
|
Chris@0
|
899 default:
|
Chris@0
|
900 $error = 'Error.';
|
Chris@0
|
901 }
|
Chris@0
|
902
|
Chris@0
|
903 throw new ParseException($error);
|
Chris@0
|
904 }
|
Chris@0
|
905
|
Chris@0
|
906 return $ret;
|
Chris@0
|
907 }
|
Chris@0
|
908 }
|