Development

/branches/1.1/lib/yaml/sfYamlParser.class.php

You must first sign up to be able to contribute.

root/branches/1.1/lib/yaml/sfYamlParser.class.php

Revision 10740, 12.3 kB (checked in by fabien, 6 years ago)

[1.1, 1.2] fixed sfYaml exception line number (now starts at 1) (closes #4026)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
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.class.php');
12
13 /**
14  * sfYamlParser class.
15  *
16  * @package    symfony
17  * @subpackage util
18  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
19  * @version    SVN: $Id$
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 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 A YAML string
45    *
46    * @return mixed  A PHP value
47    */
48   public function parse($value)
49   {
50     $this->value = $this->cleanup($value);
51     $this->currentLineNb = -1;
52     $this->currentLine = '';
53     $this->lines = explode("\n", $this->value);
54
55     $data = array();
56     while ($this->moveToNextLine())
57     {
58       if ($this->isCurrentLineEmpty())
59       {
60         continue;
61       }
62
63       // tab?
64       if (preg_match('#^\t+#', $this->currentLine))
65       {
66         throw new InvalidArgumentException(sprintf('A YAML file cannot contain tabs as indentation at line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine));
67       }
68
69       $isRef = $isInPlace = false;
70       if (preg_match('#^\-(\s+(?P<value>.+?))?\s*$#', $this->currentLine, $values))
71       {
72         if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
73         {
74           $isRef = $matches['ref'];
75           $values['value'] = $matches['value'];
76         }
77
78         // array
79         if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#'))
80         {
81           $c = $this->getRealCurrentLineNb() + 1;
82           $parser = new sfYamlParser($c);
83           $parser->refs =& $this->refs;
84           $data[] = $parser->parse($this->getNextEmbedBlock());
85         }
86         else
87         {
88           if (preg_match('/^([^ ]+)\: +({.*?)$/', $values['value'], $matches))
89           {
90             $data[] = array($matches[1] => sfYamlInline::load($matches[2]));
91           }
92           else
93           {
94             $data[] = $this->parseValue($values['value']);
95           }
96         }
97       }
98       else if (preg_match('#^(?P<key>[^ ].*?) *\:(\s+(?P<value>.+?))?\s*$#', $this->currentLine, $values))
99       {
100         $key = sfYamlInline::parseScalar($values['key']);
101
102         if ('<<' === $key)
103         {
104           if (isset($values['value']) && '*' === substr($values['value'], 0, 1))
105           {
106             $isInPlace = substr($values['value'], 1);
107             if (!array_key_exists($isInPlace, $this->refs))
108             {
109               throw new InvalidArgumentException(sprintf('Reference "%s" does not exist at line %s (%s).', $isInPlace, $this->getRealCurrentLineNb() + 1, $this->currentLine));
110             }
111           }
112           else
113           {
114             throw new InvalidArgumentException(sprintf('In place substitution must point to a reference at line %s (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine));
115           }
116         }
117         else if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
118         {
119           $isRef = $matches['ref'];
120           $values['value'] = $matches['value'];
121         }
122
123         // hash
124         if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#'))
125         {
126           // if next line is less indented or equal, then it means that the current value is null
127           if ($this->isNextLineIndented())
128           {
129             $data[$key] = null;
130           }
131           else
132           {
133             $c = $this->getRealCurrentLineNb() + 1;
134             $parser = new sfYamlParser($c);
135             $parser->refs =& $this->refs;
136             $data[$key] = $parser->parse($this->getNextEmbedBlock());
137           }
138         }
139         else
140         {
141           if ($isInPlace)
142           {
143             $data = $this->refs[$isInPlace];
144           }
145           else
146           {
147             $data[$key] = $this->parseValue($values['value']);
148           }
149         }
150       }
151       else
152       {
153         // one liner?
154         if (1 == count(explode("\n", rtrim($this->value, "\n"))))
155         {
156           return sfYamlInline::load($this->lines[0]);
157         }
158
159         throw new InvalidArgumentException(sprintf('Unable to parse line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine));
160       }
161
162       if ($isRef)
163       {
164         $this->refs[$isRef] = end($data);
165       }
166     }
167
168     return empty($data) ? null : $data;
169   }
170
171   /**
172    * Returns the current line number (takes the offset into account).
173    *
174    * @return integer The current line number
175    */
176   protected function getRealCurrentLineNb()
177   {
178     return $this->currentLineNb + $this->offset;
179   }
180
181   /**
182    * Returns the current line indentation.
183    *
184    * @returns integer The current line indentation
185    */
186   protected function getCurrentLineIndentation()
187   {
188     return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
189   }
190
191   /**
192    * Returns the next embed block of YAML.
193    *
194    * @param string A YAML string
195    */
196   protected function getNextEmbedBlock()
197   {
198     $this->moveToNextLine();
199
200     $newIndent = $this->getCurrentLineIndentation();
201
202     if (!$this->isCurrentLineEmpty() && 0 == $newIndent)
203     {
204       throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine));
205     }
206
207     $data = array(substr($this->currentLine, $newIndent));
208
209     while ($this->moveToNextLine())
210     {
211       if ($this->isCurrentLineEmpty())
212       {
213         if ($this->isCurrentLineBlank())
214         {
215           $data[] = substr($this->currentLine, $newIndent);
216         }
217
218         continue;
219       }
220
221       $indent = $this->getCurrentLineIndentation();
222
223       if (preg_match('#^(?P<text> *)$#', $this->currentLine, $match))
224       {
225         // empty line
226         $data[] = $match['text'];
227       }
228       else if ($indent >= $newIndent)
229       {
230         $data[] = substr($this->currentLine, $newIndent);
231       }
232       else if (0 == $indent)
233       {
234         $this->moveToPreviousLine();
235
236         break;
237       }
238       else
239       {
240         throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine));
241       }
242     }
243
244     return implode("\n", $data);
245   }
246
247   /**
248    * Moves the parser to the next line.
249    */
250   protected function moveToNextLine()
251   {
252     if ($this->currentLineNb >= count($this->lines) - 1)
253     {
254       return false;
255     }
256
257     $this->currentLine = $this->lines[++$this->currentLineNb];
258
259     return true;
260   }
261
262   /**
263    * Moves the parser to the previous line.
264    */
265   protected function moveToPreviousLine()
266   {
267     $this->currentLine = $this->lines[--$this->currentLineNb];
268   }
269
270   /**
271    * Parses a YAML value.
272    *
273    * @param  string A YAML value
274    *
275    * @return mixed  A PHP value
276    */
277   protected function parseValue($value)
278   {
279     if ('*' === substr($value, 0, 1))
280     {
281       if (false !== $pos = strpos($value, '#'))
282       {
283         $value = substr($value, 1, $pos - 2);
284       }
285       else
286       {
287         $value = substr($value, 1);
288       }
289
290       if (!array_key_exists($value, $this->refs))
291       {
292         throw new InvalidArgumentException(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine));
293       }
294       return $this->refs[$value];
295     }
296
297     if (preg_match('/^(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?$/', $value, $matches))
298     {
299       $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
300
301       return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
302     }
303     else
304     {
305       return sfYamlInline::load($value);
306     }
307   }
308
309   /**
310    * Parses a folded scalar.
311    *
312    * @param  string  The separator that was used to begin this folded scalar (| or >)
313    * @param  string  The indicator that was used to begin this folded scalar (+ or -)
314    * @param  integer The indentation that was used to begin this folded scalar
315    *
316    * @return string  The text value
317    */
318   protected function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
319   {
320     $separator = '|' == $separator ? "\n" : ' ';
321     $text = '';
322
323     $notEOF = $this->moveToNextLine();
324
325     while ($notEOF && $this->isCurrentLineBlank())
326     {
327       $text .= "\n";
328
329       $notEOF = $this->moveToNextLine();
330     }
331
332     if (!$notEOF)
333     {
334       return '';
335     }
336
337     if (!preg_match('#^(?P<indent>'.($indentation ? str_repeat(' ', $indentation) : ' +').')(?P<text>.*)$#', $this->currentLine, $matches))
338     {
339       $this->moveToPreviousLine();
340
341       return '';
342     }
343
344     $textIndent = $matches['indent'];
345     $previousIndent = 0;
346
347     $text .= $matches['text'].$separator;
348     while ($this->currentLineNb + 1 < count($this->lines))
349     {
350       $this->moveToNextLine();
351
352       if (preg_match('#^(?P<indent> {'.strlen($textIndent).',})(?P<text>.+)$#', $this->currentLine, $matches))
353       {
354         if (' ' == $separator && $previousIndent != $matches['indent'])
355         {
356           $text = substr($text, 0, -1)."\n";
357         }
358         $previousIndent = $matches['indent'];
359
360         $text .= str_repeat(' ', $diff = strlen($matches['indent']) - strlen($textIndent)).$matches['text'].($diff ? "\n" : $separator);
361       }
362       else if (preg_match('#^(?P<text> *)$#', $this->currentLine, $matches))
363       {
364         $text .= preg_replace('#^ {1,'.strlen($textIndent).'}#', '', $matches['text'])."\n";
365       }
366       else
367       {
368         $this->moveToPreviousLine();
369
370         break;
371       }
372     }
373
374     if (' ' == $separator)
375     {
376       // replace last separator by a newline
377       $text = preg_replace('/ (\n*)$/', "\n$1", $text);
378     }
379
380     switch ($indicator)
381     {
382       case '':
383         $text = preg_replace('#\n+$#s', "\n", $text);
384         break;
385       case '+':
386         break;
387       case '-':
388         $text = preg_replace('#\n+$#s', '', $text);
389         break;
390     }
391
392     return $text;
393   }
394
395   /**
396    * Returns true if the next line is indented.
397    *
398    * @return Boolean Returns true if the next line is indented, false otherwise
399    */
400   protected function isNextLineIndented()
401   {
402     $currentIndentation = $this->getCurrentLineIndentation();
403     $notEOF = $this->moveToNextLine();
404
405     while ($notEOF && $this->isCurrentLineEmpty())
406     {
407       $notEOF = $this->moveToNextLine();
408     }
409
410     if (false === $notEOF)
411     {
412       return false;
413     }
414
415     $ret = false;
416     if ($this->getCurrentLineIndentation() <= $currentIndentation)
417     {
418       $ret = true;
419     }
420
421     $this->moveToPreviousLine();
422
423     return $ret;
424   }
425
426   /**
427    * Returns true if the current line is blank or if it is a comment line.
428    *
429    * @return Boolean Returns true if the current line is empty or if it is a comment line, false otherwise
430    */
431   protected function isCurrentLineEmpty()
432   {
433     return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
434   }
435
436   /**
437    * Returns true if the current line is blank.
438    *
439    * @return Boolean Returns true if the current line is blank, false otherwise
440    */
441   protected function isCurrentLineBlank()
442   {
443     return '' == trim($this->currentLine, ' ');
444   }
445
446   /**
447    * Returns true if the current line is a comment line.
448    *
449    * @return Boolean Returns true if the current line is a comment line, false otherwise
450    */
451   protected function isCurrentLineComment()
452   {
453     return 0 === strpos(ltrim($this->currentLine, ' '), '#');
454   }
455
456   /**
457    * Cleanups a YAML string to be parsed.
458    *
459    * @param  string The input YAML string
460    *
461    * @return string A cleaned up YAML string
462    */
463   protected function cleanup($value)
464   {
465     $value = str_replace(array("\r\n", "\r"), "\n", $value);
466
467     if (!preg_match("#\n$#", $value))
468     {
469       $value .= "\n";
470     }
471
472     // strip YAML header
473     preg_replace('#^\%YAML[: ][\d\.]+.*\n#s', '', $value);
474
475     // remove ---
476     $value = preg_replace('#^\-\-\-.*?\n#s', '', $value);
477
478     return $value;
479   }
480 }
481
Note: See TracBrowser for help on using the browser.