[PLUGINS] +yaml
[ptitvelo/web/www.git] / www / plugins / yaml / sfyaml / sfYamlParser.php
1 <?php
2
3 /*
4 * This file is part of the symfony package.
5 * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11 require_once(dirname(__FILE__).'/sfYamlInline.php');
12
13 /**
14 * sfYamlParser parses YAML strings to convert them to PHP arrays.
15 *
16 * @package symfony
17 * @subpackage yaml
18 * @author Fabien Potencier <fabien.potencier@symfony-project.com>
19 * @version SVN: $Id: sfYamlParser.class.php 10832 2008-08-13 07:46:08Z fabien $
20 */
21 class sfYamlParser
22 {
23 protected
24 $value = '',
25 $offset = 0,
26 $lines = array(),
27 $currentLineNb = -1,
28 $currentLine = '',
29 $refs = array();
30
31 /**
32 * Constructor
33 *
34 * @param integer $offset The offset of YAML document (used for line numbers in error messages)
35 */
36 public function __construct($offset = 0)
37 {
38 $this->offset = $offset;
39 }
40
41 /**
42 * Parses a YAML string to a PHP value.
43 *
44 * @param string $value A YAML string
45 *
46 * @return mixed A PHP value
47 *
48 * @throws InvalidArgumentException If the YAML is not valid
49 */
50 public function parse($value)
51 {
52 $this->value = $this->cleanup($value);
53 $this->currentLineNb = -1;
54 $this->currentLine = '';
55 $this->lines = explode("\n", $this->value);
56
57 $data = array();
58 while ($this->moveToNextLine())
59 {
60 if ($this->isCurrentLineEmpty())
61 {
62 continue;
63 }
64
65 // tab?
66 if (preg_match('#^\t+#', $this->currentLine))
67 {
68 throw new InvalidArgumentException(sprintf('A YAML file cannot contain tabs as indentation at line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine));
69 }
70
71 $isRef = $isInPlace = $isProcessed = false;
72 if (preg_match('#^\-(\s+(?P<value>.+?))?\s*$#', $this->currentLine, $values))
73 {
74 if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
75 {
76 $isRef = $matches['ref'];
77 $values['value'] = $matches['value'];
78 }
79
80 // array
81 if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#'))
82 {
83 $c = $this->getRealCurrentLineNb() + 1;
84 $parser = new sfYamlParser($c);
85 $parser->refs =& $this->refs;
86 $data[] = $parser->parse($this->getNextEmbedBlock());
87 }
88 else
89 {
90 if (preg_match('/^([^ ]+)\: +({.*?)$/', $values['value'], $matches))
91 {
92 $data[] = array($matches[1] => sfYamlInline::load($matches[2]));
93 }
94 else
95 {
96 $data[] = $this->parseValue($values['value']);
97 }
98 }
99 }
100 else if (preg_match('#^(?P<key>[^ ].*?) *\:(\s+(?P<value>.+?))?\s*$#', $this->currentLine, $values))
101 {
102 $key = sfYamlInline::parseScalar($values['key']);
103
104 if ('<<' === $key)
105 {
106 if (isset($values['value']) && '*' === substr($values['value'], 0, 1))
107 {
108 $isInPlace = substr($values['value'], 1);
109 if (!array_key_exists($isInPlace, $this->refs))
110 {
111 throw new InvalidArgumentException(sprintf('Reference "%s" does not exist at line %s (%s).', $isInPlace, $this->getRealCurrentLineNb() + 1, $this->currentLine));
112 }
113 }
114 else
115 {
116 if (isset($values['value']) && $values['value'] !== '')
117 {
118 $value = $values['value'];
119 }
120 else
121 {
122 $value = $this->getNextEmbedBlock();
123 }
124 $c = $this->getRealCurrentLineNb() + 1;
125 $parser = new sfYamlParser($c);
126 $parser->refs =& $this->refs;
127 $parsed = $parser->parse($value);
128
129 $merged = array();
130 if (!is_array($parsed))
131 {
132 throw new InvalidArgumentException(sprintf("YAML merge keys used with a scalar value instead of an array at line %s (%s)", $this->getRealCurrentLineNb() + 1, $this->currentLine));
133 }
134 else if (isset($parsed[0]))
135 {
136 // Numeric array, merge individual elements
137 foreach (array_reverse($parsed) as $parsedItem)
138 {
139 if (!is_array($parsedItem))
140 {
141 throw new InvalidArgumentException(sprintf("Merge items must be arrays at line %s (%s).", $this->getRealCurrentLineNb() + 1, $parsedItem));
142 }
143 $merged = array_merge($parsedItem, $merged);
144 }
145 }
146 else
147 {
148 // Associative array, merge
149 $merged = array_merge($merge, $parsed);
150 }
151
152 $isProcessed = $merged;
153 }
154 }
155 else if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
156 {
157 $isRef = $matches['ref'];
158 $values['value'] = $matches['value'];
159 }
160
161 if ($isProcessed)
162 {
163 // Merge keys
164 $data = $isProcessed;
165 }
166 // hash
167 else if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#'))
168 {
169 // if next line is less indented or equal, then it means that the current value is null
170 if ($this->isNextLineIndented())
171 {
172 $data[$key] = null;
173 }
174 else
175 {
176 $c = $this->getRealCurrentLineNb() + 1;
177 $parser = new sfYamlParser($c);
178 $parser->refs =& $this->refs;
179 $data[$key] = $parser->parse($this->getNextEmbedBlock());
180 }
181 }
182 else
183 {
184 if ($isInPlace)
185 {
186 $data = $this->refs[$isInPlace];
187 }
188 else
189 {
190 $data[$key] = $this->parseValue($values['value']);
191 }
192 }
193 }
194 else
195 {
196 // one liner?
197 if (1 == count(explode("\n", rtrim($this->value, "\n"))))
198 {
199 $value = sfYamlInline::load($this->lines[0]);
200 if (is_array($value))
201 {
202 $first = reset($value);
203 if ('*' === substr($first, 0, 1))
204 {
205 $data = array();
206 foreach ($value as $alias)
207 {
208 $data[] = $this->refs[substr($alias, 1)];
209 }
210 $value = $data;
211 }
212 }
213
214 return $value;
215 }
216
217 throw new InvalidArgumentException(sprintf('Unable to parse line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine));
218 }
219
220 if ($isRef)
221 {
222 $this->refs[$isRef] = end($data);
223 }
224 }
225
226 return empty($data) ? null : $data;
227 }
228
229 /**
230 * Returns the current line number (takes the offset into account).
231 *
232 * @return integer The current line number
233 */
234 protected function getRealCurrentLineNb()
235 {
236 return $this->currentLineNb + $this->offset;
237 }
238
239 /**
240 * Returns the current line indentation.
241 *
242 * @return integer The current line indentation
243 */
244 protected function getCurrentLineIndentation()
245 {
246 return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
247 }
248
249 /**
250 * Returns the next embed block of YAML.
251 *
252 * @return string A YAML string
253 */
254 protected function getNextEmbedBlock()
255 {
256 $this->moveToNextLine();
257
258 $newIndent = $this->getCurrentLineIndentation();
259
260 if (!$this->isCurrentLineEmpty() && 0 == $newIndent)
261 {
262 throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine));
263 }
264
265 $data = array(substr($this->currentLine, $newIndent));
266
267 while ($this->moveToNextLine())
268 {
269 if ($this->isCurrentLineEmpty())
270 {
271 if ($this->isCurrentLineBlank())
272 {
273 $data[] = substr($this->currentLine, $newIndent);
274 }
275
276 continue;
277 }
278
279 $indent = $this->getCurrentLineIndentation();
280
281 if (preg_match('#^(?P<text> *)$#', $this->currentLine, $match))
282 {
283 // empty line
284 $data[] = $match['text'];
285 }
286 else if ($indent >= $newIndent)
287 {
288 $data[] = substr($this->currentLine, $newIndent);
289 }
290 else if (0 == $indent)
291 {
292 $this->moveToPreviousLine();
293
294 break;
295 }
296 else
297 {
298 throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine));
299 }
300 }
301
302 return implode("\n", $data);
303 }
304
305 /**
306 * Moves the parser to the next line.
307 */
308 protected function moveToNextLine()
309 {
310 if ($this->currentLineNb >= count($this->lines) - 1)
311 {
312 return false;
313 }
314
315 $this->currentLine = $this->lines[++$this->currentLineNb];
316
317 return true;
318 }
319
320 /**
321 * Moves the parser to the previous line.
322 */
323 protected function moveToPreviousLine()
324 {
325 $this->currentLine = $this->lines[--$this->currentLineNb];
326 }
327
328 /**
329 * Parses a YAML value.
330 *
331 * @param string $value A YAML value
332 *
333 * @return mixed A PHP value
334 */
335 protected function parseValue($value)
336 {
337 if ('*' === substr($value, 0, 1))
338 {
339 if (false !== $pos = strpos($value, '#'))
340 {
341 $value = substr($value, 1, $pos - 2);
342 }
343 else
344 {
345 $value = substr($value, 1);
346 }
347
348 if (!array_key_exists($value, $this->refs))
349 {
350 throw new InvalidArgumentException(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine));
351 }
352 return $this->refs[$value];
353 }
354
355 if (preg_match('/^(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?$/', $value, $matches))
356 {
357 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
358
359 return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
360 }
361 else
362 {
363 return sfYamlInline::load($value);
364 }
365 }
366
367 /**
368 * Parses a folded scalar.
369 *
370 * @param string $separator The separator that was used to begin this folded scalar (| or >)
371 * @param string $indicator The indicator that was used to begin this folded scalar (+ or -)
372 * @param integer $indentation The indentation that was used to begin this folded scalar
373 *
374 * @return string The text value
375 */
376 protected function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
377 {
378 $separator = '|' == $separator ? "\n" : ' ';
379 $text = '';
380
381 $notEOF = $this->moveToNextLine();
382
383 while ($notEOF && $this->isCurrentLineBlank())
384 {
385 $text .= "\n";
386
387 $notEOF = $this->moveToNextLine();
388 }
389
390 if (!$notEOF)
391 {
392 return '';
393 }
394
395 if (!preg_match('#^(?P<indent>'.($indentation ? str_repeat(' ', $indentation) : ' +').')(?P<text>.*)$#', $this->currentLine, $matches))
396 {
397 $this->moveToPreviousLine();
398
399 return '';
400 }
401
402 $textIndent = $matches['indent'];
403 $previousIndent = 0;
404
405 $text .= $matches['text'].$separator;
406 while ($this->currentLineNb + 1 < count($this->lines))
407 {
408 $this->moveToNextLine();
409
410 if (preg_match('#^(?P<indent> {'.strlen($textIndent).',})(?P<text>.+)$#', $this->currentLine, $matches))
411 {
412 if (' ' == $separator && $previousIndent != $matches['indent'])
413 {
414 $text = substr($text, 0, -1)."\n";
415 }
416 $previousIndent = $matches['indent'];
417
418 $text .= str_repeat(' ', $diff = strlen($matches['indent']) - strlen($textIndent)).$matches['text'].($diff ? "\n" : $separator);
419 }
420 else if (preg_match('#^(?P<text> *)$#', $this->currentLine, $matches))
421 {
422 $text .= preg_replace('#^ {1,'.strlen($textIndent).'}#', '', $matches['text'])."\n";
423 }
424 else
425 {
426 $this->moveToPreviousLine();
427
428 break;
429 }
430 }
431
432 if (' ' == $separator)
433 {
434 // replace last separator by a newline
435 $text = preg_replace('/ (\n*)$/', "\n$1", $text);
436 }
437
438 switch ($indicator)
439 {
440 case '':
441 $text = preg_replace('#\n+$#s', "\n", $text);
442 break;
443 case '+':
444 break;
445 case '-':
446 $text = preg_replace('#\n+$#s', '', $text);
447 break;
448 }
449
450 return $text;
451 }
452
453 /**
454 * Returns true if the next line is indented.
455 *
456 * @return Boolean Returns true if the next line is indented, false otherwise
457 */
458 protected function isNextLineIndented()
459 {
460 $currentIndentation = $this->getCurrentLineIndentation();
461 $notEOF = $this->moveToNextLine();
462
463 while ($notEOF && $this->isCurrentLineEmpty())
464 {
465 $notEOF = $this->moveToNextLine();
466 }
467
468 if (false === $notEOF)
469 {
470 return false;
471 }
472
473 $ret = false;
474 if ($this->getCurrentLineIndentation() <= $currentIndentation)
475 {
476 $ret = true;
477 }
478
479 $this->moveToPreviousLine();
480
481 return $ret;
482 }
483
484 /**
485 * Returns true if the current line is blank or if it is a comment line.
486 *
487 * @return Boolean Returns true if the current line is empty or if it is a comment line, false otherwise
488 */
489 protected function isCurrentLineEmpty()
490 {
491 return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
492 }
493
494 /**
495 * Returns true if the current line is blank.
496 *
497 * @return Boolean Returns true if the current line is blank, false otherwise
498 */
499 protected function isCurrentLineBlank()
500 {
501 return '' == trim($this->currentLine, ' ');
502 }
503
504 /**
505 * Returns true if the current line is a comment line.
506 *
507 * @return Boolean Returns true if the current line is a comment line, false otherwise
508 */
509 protected function isCurrentLineComment()
510 {
511 //checking explicitly the first char of the trim is faster than loops or strpos
512 $ltrimmedLine = ltrim($this->currentLine, ' ');
513 return $ltrimmedLine[0] === '#';
514 }
515
516 /**
517 * Cleanups a YAML string to be parsed.
518 *
519 * @param string $value The input YAML string
520 *
521 * @return string A cleaned up YAML string
522 */
523 protected function cleanup($value)
524 {
525 $value = str_replace(array("\r\n", "\r"), "\n", $value);
526
527 if (!preg_match("#\n$#", $value))
528 {
529 $value .= "\n";
530 }
531
532 // strip YAML header
533 preg_replace('#^\%YAML[: ][\d\.]+.*\n#s', '', $value);
534
535 // remove ---
536 $value = preg_replace('#^\-\-\-.*?\n#s', '', $value);
537
538 return $value;
539 }
540 }