4 * This file is part of the symfony package.
5 * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 require_once(dirname(__FILE__
).'/sfYamlInline.php');
14 * sfYamlParser parses YAML strings to convert them to PHP arrays.
18 * @author Fabien Potencier <fabien.potencier@symfony-project.com>
19 * @version SVN: $Id: sfYamlParser.class.php 10832 2008-08-13 07:46:08Z fabien $
34 * @param integer $offset The offset of YAML document (used for line numbers in error messages)
36 public function __construct($offset = 0)
38 $this->offset
= $offset;
42 * Parses a YAML string to a PHP value.
44 * @param string $value A YAML string
46 * @return mixed A PHP value
48 * @throws InvalidArgumentException If the YAML is not valid
50 public function parse($value)
52 $this->value
= $this->cleanup($value);
53 $this->currentLineNb
= -1;
54 $this->currentLine
= '';
55 $this->lines
= explode("\n", $this->value
);
58 while ($this->moveToNextLine())
60 if ($this->isCurrentLineEmpty())
66 if (preg_match('#^\t+#', $this->currentLine
))
68 throw new InvalidArgumentException(sprintf('A YAML file cannot contain tabs as indentation at line %d (%s).', $this->getRealCurrentLineNb() +
1, $this->currentLine
));
71 $isRef = $isInPlace = $isProcessed = false;
72 if (preg_match('#^\-(\s+(?P<value>.+?))?\s*$#', $this->currentLine
, $values))
74 if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
76 $isRef = $matches['ref'];
77 $values['value'] = $matches['value'];
81 if (!isset($values['value']) ||
'' == trim($values['value'], ' ') ||
0 === strpos(ltrim($values['value'], ' '), '#'))
83 $c = $this->getRealCurrentLineNb() +
1;
84 $parser = new sfYamlParser($c);
85 $parser->refs
=& $this->refs
;
86 $data[] = $parser->parse($this->getNextEmbedBlock());
90 if (preg_match('/^([^ ]+)\: +({.*?)$/', $values['value'], $matches))
92 $data[] = array($matches[1] => sfYamlInline
::load($matches[2]));
96 $data[] = $this->parseValue($values['value']);
100 else if (preg_match('#^(?P<key>[^ ].*?) *\:(\s+(?P<value>.+?))?\s*$#', $this->currentLine
, $values))
102 $key = sfYamlInline
::parseScalar($values['key']);
106 if (isset($values['value']) && '*' === substr($values['value'], 0, 1))
108 $isInPlace = substr($values['value'], 1);
109 if (!array_key_exists($isInPlace, $this->refs
))
111 throw new InvalidArgumentException(sprintf('Reference "%s" does not exist at line %s (%s).', $isInPlace, $this->getRealCurrentLineNb() +
1, $this->currentLine
));
116 if (isset($values['value']) && $values['value'] !== '')
118 $value = $values['value'];
122 $value = $this->getNextEmbedBlock();
124 $c = $this->getRealCurrentLineNb() +
1;
125 $parser = new sfYamlParser($c);
126 $parser->refs
=& $this->refs
;
127 $parsed = $parser->parse($value);
130 if (!is_array($parsed))
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
));
134 else if (isset($parsed[0]))
136 // Numeric array, merge individual elements
137 foreach (array_reverse($parsed) as $parsedItem)
139 if (!is_array($parsedItem))
141 throw new InvalidArgumentException(sprintf("Merge items must be arrays at line %s (%s).", $this->getRealCurrentLineNb() +
1, $parsedItem));
143 $merged = array_merge($parsedItem, $merged);
148 // Associative array, merge
149 $merged = array_merge($merge, $parsed);
152 $isProcessed = $merged;
155 else if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
157 $isRef = $matches['ref'];
158 $values['value'] = $matches['value'];
164 $data = $isProcessed;
167 else if (!isset($values['value']) ||
'' == trim($values['value'], ' ') ||
0 === strpos(ltrim($values['value'], ' '), '#'))
169 // if next line is less indented or equal, then it means that the current value is null
170 if ($this->isNextLineIndented())
176 $c = $this->getRealCurrentLineNb() +
1;
177 $parser = new sfYamlParser($c);
178 $parser->refs
=& $this->refs
;
179 $data[$key] = $parser->parse($this->getNextEmbedBlock());
186 $data = $this->refs
[$isInPlace];
190 $data[$key] = $this->parseValue($values['value']);
197 if (1 == count(explode("\n", rtrim($this->value
, "\n"))))
199 $value = sfYamlInline
::load($this->lines
[0]);
200 if (is_array($value))
202 $first = reset($value);
203 if ('*' === substr($first, 0, 1))
206 foreach ($value as $alias)
208 $data[] = $this->refs
[substr($alias, 1)];
217 throw new InvalidArgumentException(sprintf('Unable to parse line %d (%s).', $this->getRealCurrentLineNb() +
1, $this->currentLine
));
222 $this->refs
[$isRef] = end($data);
226 return empty($data) ?
null : $data;
230 * Returns the current line number (takes the offset into account).
232 * @return integer The current line number
234 protected function getRealCurrentLineNb()
236 return $this->currentLineNb +
$this->offset
;
240 * Returns the current line indentation.
242 * @return integer The current line indentation
244 protected function getCurrentLineIndentation()
246 return strlen($this->currentLine
) - strlen(ltrim($this->currentLine
, ' '));
250 * Returns the next embed block of YAML.
252 * @return string A YAML string
254 protected function getNextEmbedBlock()
256 $this->moveToNextLine();
258 $newIndent = $this->getCurrentLineIndentation();
260 if (!$this->isCurrentLineEmpty() && 0 == $newIndent)
262 throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() +
1, $this->currentLine
));
265 $data = array(substr($this->currentLine
, $newIndent));
267 while ($this->moveToNextLine())
269 if ($this->isCurrentLineEmpty())
271 if ($this->isCurrentLineBlank())
273 $data[] = substr($this->currentLine
, $newIndent);
279 $indent = $this->getCurrentLineIndentation();
281 if (preg_match('#^(?P<text> *)$#', $this->currentLine
, $match))
284 $data[] = $match['text'];
286 else if ($indent >= $newIndent)
288 $data[] = substr($this->currentLine
, $newIndent);
290 else if (0 == $indent)
292 $this->moveToPreviousLine();
298 throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() +
1, $this->currentLine
));
302 return implode("\n", $data);
306 * Moves the parser to the next line.
308 protected function moveToNextLine()
310 if ($this->currentLineNb
>= count($this->lines
) - 1)
315 $this->currentLine
= $this->lines
[++
$this->currentLineNb
];
321 * Moves the parser to the previous line.
323 protected function moveToPreviousLine()
325 $this->currentLine
= $this->lines
[--$this->currentLineNb
];
329 * Parses a YAML value.
331 * @param string $value A YAML value
333 * @return mixed A PHP value
335 protected function parseValue($value)
337 if ('*' === substr($value, 0, 1))
339 if (false !== $pos = strpos($value, '#'))
341 $value = substr($value, 1, $pos - 2);
345 $value = substr($value, 1);
348 if (!array_key_exists($value, $this->refs
))
350 throw new InvalidArgumentException(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine
));
352 return $this->refs
[$value];
355 if (preg_match('/^(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?$/', $value, $matches))
357 $modifiers = isset($matches['modifiers']) ?
$matches['modifiers'] : '';
359 return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
363 return sfYamlInline
::load($value);
368 * Parses a folded scalar.
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
374 * @return string The text value
376 protected function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
378 $separator = '|' == $separator ?
"\n" : ' ';
381 $notEOF = $this->moveToNextLine();
383 while ($notEOF && $this->isCurrentLineBlank())
387 $notEOF = $this->moveToNextLine();
395 if (!preg_match('#^(?P<indent>'.($indentation ?
str_repeat(' ', $indentation) : ' +').')(?P<text>.*)$#', $this->currentLine
, $matches))
397 $this->moveToPreviousLine();
402 $textIndent = $matches['indent'];
405 $text .= $matches['text'].$separator;
406 while ($this->currentLineNb +
1 < count($this->lines
))
408 $this->moveToNextLine();
410 if (preg_match('#^(?P<indent> {'.strlen($textIndent).',})(?P<text>.+)$#', $this->currentLine
, $matches))
412 if (' ' == $separator && $previousIndent != $matches['indent'])
414 $text = substr($text, 0, -1)."\n";
416 $previousIndent = $matches['indent'];
418 $text .= str_repeat(' ', $diff = strlen($matches['indent']) - strlen($textIndent)).$matches['text'].($diff ?
"\n" : $separator);
420 else if (preg_match('#^(?P<text> *)$#', $this->currentLine
, $matches))
422 $text .= preg_replace('#^ {1,'.strlen($textIndent).'}#', '', $matches['text'])."\n";
426 $this->moveToPreviousLine();
432 if (' ' == $separator)
434 // replace last separator by a newline
435 $text = preg_replace('/ (\n*)$/', "\n$1", $text);
441 $text = preg_replace('#\n+$#s', "\n", $text);
446 $text = preg_replace('#\n+$#s', '', $text);
454 * Returns true if the next line is indented.
456 * @return Boolean Returns true if the next line is indented, false otherwise
458 protected function isNextLineIndented()
460 $currentIndentation = $this->getCurrentLineIndentation();
461 $notEOF = $this->moveToNextLine();
463 while ($notEOF && $this->isCurrentLineEmpty())
465 $notEOF = $this->moveToNextLine();
468 if (false === $notEOF)
474 if ($this->getCurrentLineIndentation() <= $currentIndentation)
479 $this->moveToPreviousLine();
485 * Returns true if the current line is blank or if it is a comment line.
487 * @return Boolean Returns true if the current line is empty or if it is a comment line, false otherwise
489 protected function isCurrentLineEmpty()
491 return $this->isCurrentLineBlank() ||
$this->isCurrentLineComment();
495 * Returns true if the current line is blank.
497 * @return Boolean Returns true if the current line is blank, false otherwise
499 protected function isCurrentLineBlank()
501 return '' == trim($this->currentLine
, ' ');
505 * Returns true if the current line is a comment line.
507 * @return Boolean Returns true if the current line is a comment line, false otherwise
509 protected function isCurrentLineComment()
511 //checking explicitly the first char of the trim is faster than loops or strpos
512 $ltrimmedLine = ltrim($this->currentLine
, ' ');
513 return $ltrimmedLine[0] === '#';
517 * Cleanups a YAML string to be parsed.
519 * @param string $value The input YAML string
521 * @return string A cleaned up YAML string
523 protected function cleanup($value)
525 $value = str_replace(array("\r\n", "\r"), "\n", $value);
527 if (!preg_match("#\n$#", $value))
533 preg_replace('#^\%YAML[: ][\d\.]+.*\n#s', '', $value);
536 $value = preg_replace('#^\-\-\-.*?\n#s', '', $value);