Development

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

You must first sign up to be able to contribute.

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

Revision 21875, 14.0 kB (checked in by fabien, 5 years ago)

[1.2, 1.3] fixed PHPDoc (closes #6279)

  • 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 $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   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 = $isProcessed = 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             if (isset($values['value']) && $values['value'] !== '')
115             {
116               $value = $values['value'];
117             }
118             else
119             {
120               $value = $this->getNextEmbedBlock();
121             }
122             $c = $this->getRealCurrentLineNb() + 1;
123             $parser = new sfYamlParser($c);
124             $parser->refs =& $this->refs;
125             $parsed = $parser->parse($value);
126
127             $merged = array();
128             if (!is_array($parsed))
129             {
130               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));
131             }
132             else if (isset($parsed[0]))
133             {
134               // Numeric array, merge individual elements
135               foreach (array_reverse($parsed) as $parsedItem)
136               {
137                 if (!is_array($parsedItem))
138                 {
139                   throw new InvalidArgumentException(sprintf("Merge items must be arrays at line %s (%s).", $this->getRealCurrentLineNb() + 1, $parsedItem));
140                 }
141                 $merged = array_merge($parsedItem, $merged);
142               }
143             }
144             else
145             {
146               // Associative array, merge
147               $merged = array_merge($merge, $parsed);
148             }
149
150             $isProcessed = $merged;
151           }
152         }
153         else if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#', $values['value'], $matches))
154         {
155           $isRef = $matches['ref'];
156           $values['value'] = $matches['value'];
157         }
158
159         if ($isProcessed)
160         {
161           // Merge keys
162           $data = $isProcessed;
163         }
164         // hash
165         else if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#'))
166         {
167           // if next line is less indented or equal, then it means that the current value is null
168           if ($this->isNextLineIndented())
169           {
170             $data[$key] = null;
171           }
172           else
173           {
174             $c = $this->getRealCurrentLineNb() + 1;
175             $parser = new sfYamlParser($c);
176             $parser->refs =& $this->refs;
177             $data[$key] = $parser->parse($this->getNextEmbedBlock());
178           }
179         }
180         else
181         {
182           if ($isInPlace)
183           {
184             $data = $this->refs[$isInPlace];
185           }
186           else
187           {
188             $data[$key] = $this->parseValue($values['value']);
189           }
190         }
191       }
192       else
193       {
194         // one liner?
195         if (1 == count(explode("\n", rtrim($this->value, "\n"))))
196         {
197           $value = sfYamlInline::load($this->lines[0]);
198           if (is_array($value))
199           {
200             $first = reset($value);
201             if ('*' === substr($first, 0, 1))
202             {
203               $data = array();
204               foreach ($value as $alias)
205               {
206                 $data[] = $this->refs[substr($alias, 1)];
207               }
208               $value = $data;
209             }
210           }
211
212           return $value;
213         }
214
215         throw new InvalidArgumentException(sprintf('Unable to parse line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine));
216       }
217
218       if ($isRef)
219       {
220         $this->refs[$isRef] = end($data);
221       }
222     }
223
224     return empty($data) ? null : $data;
225   }
226
227   /**
228    * Returns the current line number (takes the offset into account).
229    *
230    * @return integer The current line number
231    */
232   protected function getRealCurrentLineNb()
233   {
234     return $this->currentLineNb + $this->offset;
235   }
236
237   /**
238    * Returns the current line indentation.
239    *
240    * @returns integer The current line indentation
241    */
242   protected function getCurrentLineIndentation()
243   {
244     return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
245   }
246
247   /**
248    * Returns the next embed block of YAML.
249    *
250    * @return string A YAML string
251    */
252   protected function getNextEmbedBlock()
253   {
254     $this->moveToNextLine();
255
256     $newIndent = $this->getCurrentLineIndentation();
257
258     if (!$this->isCurrentLineEmpty() && 0 == $newIndent)
259     {
260       throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine));
261     }
262
263     $data = array(substr($this->currentLine, $newIndent));
264
265     while ($this->moveToNextLine())
266     {
267       if ($this->isCurrentLineEmpty())
268       {
269         if ($this->isCurrentLineBlank())
270         {
271           $data[] = substr($this->currentLine, $newIndent);
272         }
273
274         continue;
275       }
276
277       $indent = $this->getCurrentLineIndentation();
278
279       if (preg_match('#^(?P<text> *)$#', $this->currentLine, $match))
280       {
281         // empty line
282         $data[] = $match['text'];
283       }
284       else if ($indent >= $newIndent)
285       {
286         $data[] = substr($this->currentLine, $newIndent);
287       }
288       else if (0 == $indent)
289       {
290         $this->moveToPreviousLine();
291
292         break;
293       }
294       else
295       {
296         throw new InvalidArgumentException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine));
297       }
298     }
299
300     return implode("\n", $data);
301   }
302
303   /**
304    * Moves the parser to the next line.
305    */
306   protected function moveToNextLine()
307   {
308     if ($this->currentLineNb >= count($this->lines) - 1)
309     {
310       return false;
311     }
312
313     $this->currentLine = $this->lines[++$this->currentLineNb];
314
315     return true;
316   }
317
318   /**
319    * Moves the parser to the previous line.
320    */
321   protected function moveToPreviousLine()
322   {
323     $this->currentLine = $this->lines[--$this->currentLineNb];
324   }
325
326   /**
327    * Parses a YAML value.
328    *
329    * @param  string $value A YAML value
330    *
331    * @return mixed  A PHP value
332    */
333   protected function parseValue($value)
334   {
335     if ('*' === substr($value, 0, 1))
336     {
337       if (false !== $pos = strpos($value, '#'))
338       {
339         $value = substr($value, 1, $pos - 2);
340       }
341       else
342       {
343         $value = substr($value, 1);
344       }
345
346       if (!array_key_exists($value, $this->refs))
347       {
348         throw new InvalidArgumentException(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine));
349       }
350       return $this->refs[$value];
351     }
352
353     if (preg_match('/^(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?$/', $value, $matches))
354     {
355       $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
356
357       return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
358     }
359     else
360     {
361       return sfYamlInline::load($value);
362     }
363   }
364
365   /**
366    * Parses a folded scalar.
367    *
368    * @param  string  $separator   The separator that was used to begin this folded scalar (| or >)
369    * @param  string  $indicator   The indicator that was used to begin this folded scalar (+ or -)
370    * @param  integer $indentation The indentation that was used to begin this folded scalar
371    *
372    * @return string  The text value
373    */
374   protected function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
375   {
376     $separator = '|' == $separator ? "\n" : ' ';
377     $text = '';
378
379     $notEOF = $this->moveToNextLine();
380
381     while ($notEOF && $this->isCurrentLineBlank())
382     {
383       $text .= "\n";
384
385       $notEOF = $this->moveToNextLine();
386     }
387
388     if (!$notEOF)
389     {
390       return '';
391     }
392
393     if (!preg_match('#^(?P<indent>'.($indentation ? str_repeat(' ', $indentation) : ' +').')(?P<text>.*)$#', $this->currentLine, $matches))
394     {
395       $this->moveToPreviousLine();
396
397       return '';
398     }
399
400     $textIndent = $matches['indent'];
401     $previousIndent = 0;
402
403     $text .= $matches['text'].$separator;
404     while ($this->currentLineNb + 1 < count($this->lines))
405     {
406       $this->moveToNextLine();
407
408       if (preg_match('#^(?P<indent> {'.strlen($textIndent).',})(?P<text>.+)$#', $this->currentLine, $matches))
409       {
410         if (' ' == $separator && $previousIndent != $matches['indent'])
411         {
412           $text = substr($text, 0, -1)."\n";
413         }
414         $previousIndent = $matches['indent'];
415
416         $text .= str_repeat(' ', $diff = strlen($matches['indent']) - strlen($textIndent)).$matches['text'].($diff ? "\n" : $separator);
417       }
418       else if (preg_match('#^(?P<text> *)$#', $this->currentLine, $matches))
419       {
420         $text .= preg_replace('#^ {1,'.strlen($textIndent).'}#', '', $matches['text'])."\n";
421       }
422       else
423       {
424         $this->moveToPreviousLine();
425
426         break;
427       }
428     }
429
430     if (' ' == $separator)
431     {
432       // replace last separator by a newline
433       $text = preg_replace('/ (\n*)$/', "\n$1", $text);
434     }
435
436     switch ($indicator)
437     {
438       case '':
439         $text = preg_replace('#\n+$#s', "\n", $text);
440         break;
441       case '+':
442         break;
443       case '-':
444         $text = preg_replace('#\n+$#s', '', $text);
445         break;
446     }
447
448     return $text;
449   }
450
451   /**
452    * Returns true if the next line is indented.
453    *
454    * @return Boolean Returns true if the next line is indented, false otherwise
455    */
456   protected function isNextLineIndented()
457   {
458     $currentIndentation = $this->getCurrentLineIndentation();
459     $notEOF = $this->moveToNextLine();
460
461     while ($notEOF && $this->isCurrentLineEmpty())
462     {
463       $notEOF = $this->moveToNextLine();
464     }
465
466     if (false === $notEOF)
467     {
468       return false;
469     }
470
471     $ret = false;
472     if ($this->getCurrentLineIndentation() <= $currentIndentation)
473     {
474       $ret = true;
475     }
476
477     $this->moveToPreviousLine();
478
479     return $ret;
480   }
481
482   /**
483    * Returns true if the current line is blank or if it is a comment line.
484    *
485    * @return Boolean Returns true if the current line is empty or if it is a comment line, false otherwise
486    */
487   protected function isCurrentLineEmpty()
488   {
489     return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
490   }
491
492   /**
493    * Returns true if the current line is blank.
494    *
495    * @return Boolean Returns true if the current line is blank, false otherwise
496    */
497   protected function isCurrentLineBlank()
498   {
499     return '' == trim($this->currentLine, ' ');
500   }
501
502   /**
503    * Returns true if the current line is a comment line.
504    *
505    * @return Boolean Returns true if the current line is a comment line, false otherwise
506    */
507   protected function isCurrentLineComment()
508   {
509     return 0 === strpos(ltrim($this->currentLine, ' '), '#');
510   }
511
512   /**
513    * Cleanups a YAML string to be parsed.
514    *
515    * @param  string $value The input YAML string
516    *
517    * @return string A cleaned up YAML string
518    */
519   protected function cleanup($value)
520   {
521     $value = str_replace(array("\r\n", "\r"), "\n", $value);
522
523     if (!preg_match("#\n$#", $value))
524     {
525       $value .= "\n";
526     }
527
528     // strip YAML header
529     preg_replace('#^\%YAML[: ][\d\.]+.*\n#s', '', $value);
530
531     // remove ---
532     $value = preg_replace('#^\-\-\-.*?\n#s', '', $value);
533
534     return $value;
535   }
536 }
537
Note: See TracBrowser for help on using the browser.