/mvc/components/yaml
[return to app]1
<?php
2 /*
3 * This file is part of the symfony package, updated for Vork
4 * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
5 *
6 * Copyright (c) 2004-2010 Fabien Potencier
7 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
8 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the
9 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit
10 * persons to whom the Software is furnished to do so, subject to the following conditions:
11 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
the
12 * Software.
13 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE
14 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR
15 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 */
18
19 /**
20 * sfYaml offers convenience methods to load and dump YAML.
21 *
22 * @package symfony
23 * @subpackage yaml
24 * @author Fabien Potencier <fabien.potencier@symfony-project.com>
25 * @version SVN: $Id: sfYaml.class.php 8988 2008-05-15 20:24:26Z fabien $
26 */
27 class yamlComponent {
28 static protected $spec = '1.2';
29
30 /**
31 * Sets the YAML specification version to use.
32 *
33 * @param string $version The YAML specification version
34 */
35 static public function setSpecVersion($version) {
36 if (!in_array($version, array('1.1', '1.2'))) {
37 throw new Exception(
38 sprintf('Version %s of the YAML specifications is not supported', $version)
39 );
40 }
41
42 self::$spec = $version;
43 }
44
45 /**
46 * Gets the YAML specification version to use.
47 *
48 * @return string The YAML specification version
49 */
50 static public function getSpecVersion() {
51 return self::$spec;
52 }
53
54 /**
55 * Loads YAML into a PHP array.
56 * The load method, when supplied with a YAML stream (string or file),
57 * will do its best to convert YAML in a file into a PHP array.
58 * Usage:
59 * <code>
60 * $array = sfYaml::load('config.yml');
61 * print_r($array);
62 * </code>
63 *
64 * @param string $input Path of YAML file or string containing YAML
65 * @return array The YAML converted to a PHP array
66 * @throws Exception If the YAML is not valid
67 */
68 public function load($input) {
69 $file = '';
70
71 // if input is a file, process it
72 if (strpos($input, "\n") === false && is_file($input)) {
73 $file = $input;
74
75 ob_start();
76 $retval = include($input);
77 $content = ob_get_clean();
78
79 // if an array is returned by the config file assume it's in plain php form else in YAML
80 $input = is_array($retval) ? $retval : $content;
81 }
82
83 // if an array is returned by the config file assume it's in plain php form else in YAML
84 if (is_array($input)) {
85 return $input;
86 }
87
88 $yaml = new sfYamlParser();
89
90 try{
91 $ret = $yaml->parse($input);
92 }
93 catch (Exception $e) {
94 throw new Exception(
95 sprintf('Unable to parse %s: %s', $file ? sprintf('file "%s"', $file) : 'string',
$e->getMessage())
96 );
97 }
98
99 return $ret;
100 }
101
102 /**
103 * Dumps a PHP array to a YAML string.
104 * The dump method, when supplied with an array, will do its best
105 * to convert the array into friendly YAML.
106 *
107 * @param array $array PHP array
108 * @param integer $inline The level where you switch to inline YAML
109 * @return string A YAML string representing the original PHP array
110 */
111 public function dump($array, $inline = 2) {
112 $yaml = new sfYamlDumper();
113
114 return $yaml->dump($array, $inline);
115 }
116 }
117
118 /**
119 * sfYamlInline implements a YAML parser/dumper for the YAML inline syntax.
120 *
121 * @package symfony
122 * @subpackage yaml
123 * @author Fabien Potencier <fabien.potencier@symfony-project.com>
124 * @version SVN: $Id: sfYamlInline.class.php 16177 2009-03-11 08:32:48Z fabien $
125 */
126 class sfYamlInline{
127 const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')';
128
129 /**
130 * Convert a YAML string to a PHP array.
131 *
132 * @param string $value A YAML string
133 * @return array A PHP array representing the YAML string
134 */
135 static public function load($value) {
136 $value = trim($value);
137
138 if (0 == strlen($value)) {
139 return '';
140 }
141
142 if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
143 $mbEncoding = mb_internal_encoding();
144 mb_internal_encoding('ASCII');
145 }
146
147 switch ($value[0]) {
148 case '[':
149 $result = self::_parseSequence($value);
150 break;
151 case '{':
152 $result = self::_parseMapping($value);
153 break;
154 default:
155 $result = self::parseScalar($value);
156 }
157
158 if (isset($mbEncoding)) {
159 mb_internal_encoding($mbEncoding);
160 }
161
162 return $result;
163 }
164
165 /**
166 * Dumps a given PHP variable to a YAML string.
167 *
168 * @param mixed $value The PHP variable to convert
169 * @return string The YAML string representing the PHP array
170 */
171 static public function dump($value) {
172 if ('1.1' === yamlComponent::getSpecVersion()) {
173 $trueValues = array('true', 'on', '+', 'yes', 'y');
174 $falseValues = array('false', 'off', '-', 'no', 'n');
175 } else {
176 $trueValues = array('true');
177 $falseValues = array('false');
178 }
179
180 switch (true) {
181 case is_resource($value):
182 throw new Exception('Unable to dump PHP resources in a YAML file.');
183 case is_object($value):
184 return '!!php/object:'.serialize($value);
185 case is_array($value):
186 return self::_dumpArray($value);
187 case null === $value:
188 return 'null';
189 case true === $value:
190 return 'true';
191 case false === $value:
192 return 'false';
193 case ctype_digit($value):
194 return is_string($value) ? "'$value'" : (int) $value;
195 case is_numeric($value):
196 return is_infinite($value) ?
197 str_ireplace('INF', '.Inf', strval($value)) : (is_string($value) ? "'$value'" :
198 $value);
199 case false !== strpos($value, "\n") || false !== strpos($value, "\r"):
200 return sprintf('"%s"', str_replace(array('"', "\n", "\r"), array('\\"', '\n', '\r'), $value));
201 case preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ - ? | < > = ! % @ ` ]/x', $value):
202 return sprintf("'%s'", str_replace('\'', '\'\'', $value));
203 case '' == $value:
204 return "''";
205 case preg_match(self::_getTimestampRegex(), $value):
206 return "'$value'";
207 case in_array(strtolower($value), $trueValues):
208 return "'$value'";
209 case in_array(strtolower($value), $falseValues):
210 return "'$value'";
211 case in_array(strtolower($value), array('null', '~')):
212 return "'$value'";
213 default:
214 return $value;
215 }
216 }
217
218 /**
219 * Dumps a PHP array to a YAML string.
220 *
221 * @param array $value The PHP array to dump
222 * @return string The YAML string representing the PHP array
223 */
224 static protected function _dumpArray($value) {
225 // array
226 $keys = array_keys($value);
227 if ((1 == count($keys) && '0' == $keys[0]) ||
228 (count($keys) > 1 && array_reduce($keys, create_function('$v,$w', 'return (integer) $v + $w;'), 0) ==
229 count($keys) * (count($keys) - 1) / 2)) {
230 $output = array();
231 foreach ($value as $val) {
232 $output[] = self::dump($val);
233 }
234
235 return sprintf('[%s]', implode(', ', $output));
236 }
237
238 // mapping
239 $output = array();
240 foreach ($value as $key => $val) {
241 $output[] = sprintf('%s: %s', self::dump($key), self::dump($val));
242 }
243
244 return sprintf('{ %s }', implode(', ', $output));
245 }
246
247 /**
248 * Parses a scalar to a YAML string.
249 *
250 * @param scalar $scalar
251 * @param string $delimiters
252 * @param array $stringDelimiter
253 * @param integer $i
254 * @param boolean $evaluate
255 * @return string A YAML string
256 */
257 static public function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0,
258 $evaluate = true) {
259 if (in_array($scalar[$i], $stringDelimiters)) {
260 // quoted scalar
261 $output = self::_parseQuotedScalar($scalar, $i);
262 } else {
263 // "normal" string
264 if (!$delimiters) {
265 $output = substr($scalar, $i);
266 $i += strlen($output);
267
268 // remove comments
269 if (false !== $strpos = strpos($output, ' #')) {
270 $output = rtrim(substr($output, 0, $strpos));
271 }
272 } else if (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) {
273 $output = $match[1];
274 $i += strlen($output);
275 } else {
276 throw new Exception(sprintf('Malformed inline YAML string (%s).', $scalar));
277 }
278
279 $output = $evaluate ? self::_evaluateScalar($output) : $output;
280 }
281
282 return $output;
283 }
284
285 /**
286 * Parses a quoted scalar to YAML.
287 *
288 * @param string $scalar
289 * @param integer $i
290 * @return string A YAML string
291 */
292 static protected function _parseQuotedScalar($scalar, &$i) {
293 if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) {
294 throw new Exception(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i)));
295 }
296
297 $output = substr($match[0], 1, strlen($match[0]) - 2);
298
299 if ('"' == $scalar[$i]) {
300 // evaluate the string
301 $output = str_replace(array('\\"', '\\n', '\\r'), array('"', "\n", "\r"), $output);
302 } else {
303 // unescape '
304 $output = str_replace('\'\'', '\'', $output);
305 }
306
307 $i += strlen($match[0]);
308
309 return $output;
310 }
311
312 /**
313 * Parses a sequence to a YAML string.
314 *
315 * @param string $sequence
316 * @param integer $i
317 * @return string A YAML string
318 */
319 static protected function _parseSequence($sequence, &$i = 0) {
320 $output = array();
321 $len = strlen($sequence);
322 $i += 1;
323
324 // [foo, bar, ...]
325 while ($i < $len) {
326 switch ($sequence[$i]) {
327 case '[':
328 // nested sequence
329 $output[] = self::_parseSequence($sequence, $i);
330 break;
331 case '{':
332 // nested mapping
333 $output[] = self::_parseMapping($sequence, $i);
334 break;
335 case ']':
336 return $output;
337 case ',':
338 case ' ':
339 break;
340 default:
341 $isQuoted = in_array($sequence[$i], array('"', "'"));
342
343 $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i);
344
345 if (!$isQuoted && false !== strpos($value, ': ')) {
346 // embedded mapping?
347 try{
348 $value = self::_parseMapping('{'.$value.'}');
349 }
350 catch (Exception $e) {
351 // no, it's not
352 }
353 }
354
355 $output[] = $value;
356
357 --$i;
358 }
359
360 ++$i;
361 }
362
363 throw new Exception(sprintf('Malformed inline YAML string %s', $sequence));
364 }
365
366 /**
367 * Parses a mapping to a YAML string.
368 *
369 * @param string $mapping
370 * @param integer $i
371 * @return string A YAML string
372 */
373 static protected function _parseMapping($mapping, &$i = 0) {
374 $output = array();
375 $len = strlen($mapping);
376 $i += 1;
377
378 // {foo: bar, bar:foo, ...}
379 while ($i < $len) {
380 switch ($mapping[$i]) {
381 case ' ':
382 case ',':
383 ++$i;
384 continue 2;
385 case '}':
386 return $output;
387 }
388
389 // key
390 $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false);
391
392 // value
393 $done = false;
394 while ($i < $len) {
395 switch ($mapping[$i]) {
396 case '[':
397 // nested sequence
398 $output[$key] = self::_parseSequence($mapping, $i);
399 $done = true;
400 break;
401 case '{':
402 // nested mapping
403 $output[$key] = self::_parseMapping($mapping, $i);
404 $done = true;
405 break;
406 case ':':
407 case ' ':
408 break;
409 default:
410 $output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i);
411 $done = true;
412 --$i;
413 }
414
415 ++$i;
416
417 if ($done) {
418 continue 2;
419 }
420 }
421 }
422
423 throw new Exception(sprintf('Malformed inline YAML string %s', $mapping));
424 }
425
426 /**
427 * Evaluates scalars and replaces magic values.
428 *
429 * @param string $scalar
430 * @return string A YAML string
431 */
432 static protected function _evaluateScalar($scalar) {
433 $scalar = trim($scalar);
434
435 if ('1.1' === yamlComponent::getSpecVersion()) {
436 $trueValues = array('true', 'on', '+', 'yes', 'y');
437 $falseValues = array('false', 'off', '-', 'no', 'n');
438 } else {
439 $trueValues = array('true');
440 $falseValues = array('false');
441 }
442
443 switch (true) {
444 case 'null' == strtolower($scalar):
445 case '' == $scalar:
446 case '~' == $scalar:
447 return null;
448 case 0 === strpos($scalar, '!str'):
449 return (string) substr($scalar, 5);
450 case 0 === strpos($scalar, '! '):
451 return intval(self::parseScalar(substr($scalar, 2)));
452 case 0 === strpos($scalar, '!!php/object:'):
453 return unserialize(substr($scalar, 13));
454 case ctype_digit($scalar):
455 $raw = $scalar;
456 $cast = intval($scalar);
457 return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw);
458 case in_array(strtolower($scalar), $trueValues):
459 return true;
460 case in_array(strtolower($scalar), $falseValues):
461 return false;
462 case is_numeric($scalar):
463 return '0x' == $scalar[0].$scalar[1] ? hexdec($scalar) : floatval($scalar);
464 case 0 == strcasecmp($scalar, '.inf'):
465 case 0 == strcasecmp($scalar, '.NaN'):
466 return -log(0);
467 case 0 == strcasecmp($scalar, '-.inf'):
468 return log(0);
469 case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
470 return floatval(str_replace(',', '', $scalar));
471 case preg_match(self::_getTimestampRegex(), $scalar):
472 return strtotime($scalar);
473 default:
474 return (string) $scalar;
475 }
476 }
477
478 static protected function _getTimestampRegex() {
479 return <<<EOF
480 ~^
481 (?P<year>[0-9][0-9][0-9][0-9])
482 -(?P<month>[0-9][0-9]?)
483 -(?P<day>[0-9][0-9]?)
484 (?:(?:[Tt]|[ \t]+)
485 (?P<hour>[0-9][0-9]?)
486 :(?P<minute>[0-9][0-9])
487 :(?P<second>[0-9][0-9])
488 (?:\.(?P<fraction>[0-9]*))?
489 (?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
490 (?::(?P<tz_minute>[0-9][0-9]))?))?)?
491 $~x
492 EOF;
493 }
494 }
495
496 /**
497 * sfYamlDumper dumps PHP variables to YAML strings.
498 *
499 * @package symfony
500 * @subpackage yaml
501 * @author Fabien Potencier <fabien.potencier@symfony-project.com>
502 * @version SVN: $Id: sfYamlDumper.class.php 10575 2008-08-01 13:08:42Z nicolas $
503 */
504 class sfYamlDumper{
505 /**
506 * Dumps a PHP value to YAML.
507 *
508 * @param mixed $input The PHP value
509 * @param integer $inline The level where you switch to inline YAML
510 * @param integer $indent The level o indentation indentation (used internally)
511 * @return string The YAML representation of the PHP value
512 */
513 public function dump($input, $inline = 0, $indent = 0) {
514 $output = '';
515 $prefix = $indent ? str_repeat(' ', $indent) : '';
516
517 if ($inline <= 0 || !is_array($input) || empty($input)) {
518 $output .= $prefix.sfYamlInline::dump($input);
519 } else {
520 $isAHash = array_keys($input) !== range(0, count($input) - 1);
521
522 foreach ($input as $key => $value) {
523 $willBeInlined = $inline - 1 <= 0 || !is_array($value) || empty($value);
524
525 $output .= sprintf('%s%s%s%s',
526 $prefix,
527 $isAHash ? sfYamlInline::dump($key).':' : '-',
528 $willBeInlined ? ' ' : "\n",
529 $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + 2)
530 ).($willBeInlined ? "\n" : '');
531 }
532 }
533
534 return $output;
535 }
536 }
537
538 if (!defined('PREG_BAD_UTF8_OFFSET_ERROR')) {
539 define('PREG_BAD_UTF8_OFFSET_ERROR', 5);
540 }
541
542 /**
543 * sfYamlParser parses YAML strings to convert them to PHP arrays.
544 *
545 * @package symfony
546 * @subpackage yaml
547 * @author Fabien Potencier <fabien.potencier@symfony-project.com>
548 * @version SVN: $Id: sfYamlParser.class.php 10832 2008-08-13 07:46:08Z fabien $
549 */
550 class sfYamlParser{
551 protected
552 $offset = 0,
553 $lines = array(),
554 $currentLineNb = -1,
555 $currentLine = '',
556 $refs = array();
557
558 /**
559 * Constructor
560 *
561 * @param integer $offset The offset of YAML document (used for line numbers in error messages)
562 */
563 public function __construct($offset = 0) {
564 $this->offset = $offset;
565 }
566
567 /**
568 * Parses a YAML string to a PHP value.
569 *
570 * @param string $value A YAML string
571 * @return mixed A PHP value
572 * @throws Exception If the YAML is not valid
573 */
574 public function parse($value) {
575 $this->currentLineNb = -1;
576 $this->currentLine = '';
577 $this->lines = explode("\n", $this->_cleanup($value));
578
579 if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
580 $mbEncoding = mb_internal_encoding();
581 mb_internal_encoding('UTF-8');
582 }
583
584 $data = array();
585 while ($this->_moveToNextLine()) {
586 if ($this->_isCurrentLineEmpty()) {
587 continue;
588 }
589
590 // tab?
591 if (preg_match('#^\t+#', $this->currentLine)) {
592 throw new Exception(sprintf('A YAML file cannot contain tabs as indentation at line %d (%s).',
593 $this->_getRealCurrentLineNb() + 1, $this->currentLine));
594 }
595
596 $isRef = $isInPlace = $isProcessed = false;
597 if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
598 if (isset($values['value'])
599 && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
600 $isRef = $matches['ref'];
601 $values['value'] = $matches['value'];
602 }
603
604 // array
605 if (!isset($values['value']) || '' == trim($values['value'], ' ')
606 || 0 === strpos(ltrim($values['value'], ' '), '#')) {
607 $c = $this->_getRealCurrentLineNb() + 1;
608 $parser = new sfYamlParser($c);
609 $parser->refs =& $this->refs;
610 $data[] = $parser->parse($this->_getNextEmbedBlock());
611 } else {
612 if (isset($values['leadspaces'])
613 && ' ' == $values['leadspaces']
614 && preg_match('#^(?P<key>'.sfYamlInline::REGEX_QUOTED_STRING.
615 '|[^ \'"\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)) {
616 // this is a compact notation element, add to next block and parse
617 $c = $this->_getRealCurrentLineNb();
618 $parser = new sfYamlParser($c);
619 $parser->refs =& $this->refs;
620
621 $block = $values['value'];
622 if (!$this->_isNextLineIndented()) {
623 $block .= "\n".$this->_getNextEmbedBlock($this->_getCurrentLineIndentation() + 2);
624 }
625
626 $data[] = $parser->parse($block);
627 } else {
628 $data[] = $this->_parseValue($values['value']);
629 }
630 }
631 } else if (
632 preg_match('#^(?P<key>'.sfYamlInline::REGEX_QUOTED_STRING.'|[^ \'"].*?)
*\:(\s+(?P<value>.+?))?\s*$#u',
633 $this->currentLine, $values)) {
634 $key = sfYamlInline::parseScalar($values['key']);
635
636 if ('<<' === $key) {
637 if (isset($values['value']) && '*' === substr($values['value'], 0, 1)) {
638 $isInPlace = substr($values['value'], 1);
639 if (!array_key_exists($isInPlace, $this->refs)) {
640 throw new Exception(sprintf('Reference "%s" does not exist at line %s (%s).',
641 $isInPlace, $this->_getRealCurrentLineNb() + 1, $this->currentLine));
642 }
643 } else {
644 if (isset($values['value']) && $values['value'] !== '') {
645 $value = $values['value'];
646 } else {
647 $value = $this->_getNextEmbedBlock();
648 }
649 $c = $this->_getRealCurrentLineNb() + 1;
650 $parser = new sfYamlParser($c);
651 $parser->refs =& $this->refs;
652 $parsed = $parser->parse($value);
653
654 $merged = array();
655 if (!is_array($parsed)) {
656 throw new Exception(sprintf("YAML merge keys used with a scalar value instead of an
array
657 at line %s (%s)", $this->_getRealCurrentLineNb() + 1, $this->currentLine));
658 } else if (isset($parsed[0])) {
659 // Numeric array, merge individual elements
660 foreach (array_reverse($parsed) as $parsedItem) {
661 if (!is_array($parsedItem)) {
662 throw new Exception(sprintf("Merge items must be arrays at line %s (%s).",
663 $this->_getRealCurrentLineNb() + 1, $parsedItem));
664 }
665 $merged = array_merge($parsedItem, $merged);
666 }
667 } else {
668 // Associative array, merge
669 $merged = array_merge($merge, $parsed);
670 }
671
672 $isProcessed = $merged;
673 }
674 } else if (isset($values['value'])
675 && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
676 $isRef = $matches['ref'];
677 $values['value'] = $matches['value'];
678 }
679
680 if ($isProcessed) {
681 // Merge keys
682 $data = $isProcessed;
683 } else if (!isset($values['value']) || '' == trim($values['value'], ' ')
684 || 0 === strpos(ltrim($values['value'], ' '), '#')) {
685 // if next line is less indented or equal, then it means that the current value is null
686 if ($this->_isNextLineIndented()) {
687 $data[$key] = null;
688 } else {
689 $c = $this->_getRealCurrentLineNb() + 1;
690 $parser = new sfYamlParser($c);
691 $parser->refs =& $this->refs;
692 $data[$key] = $parser->parse($this->_getNextEmbedBlock());
693 }
694 } else {
695 if ($isInPlace) {
696 $data = $this->refs[$isInPlace];
697 } else {
698 $data[$key] = $this->_parseValue($values['value']);
699 }
700 }
701 } else {
702 // 1-liner followed by newline
703 if (2 == count($this->lines) && empty($this->lines[1])) {
704 $value = sfYamlInline::load($this->lines[0]);
705 if (is_array($value)) {
706 $first = reset($value);
707 if ('*' === substr($first, 0, 1)) {
708 $data = array();
709 foreach ($value as $alias) {
710 $data[] = $this->refs[substr($alias, 1)];
711 }
712 $value = $data;
713 }
714 }
715
716 if (isset($mbEncoding)) {
717 mb_internal_encoding($mbEncoding);
718 }
719
720 return $value;
721 }
722
723 switch (preg_last_error()) {
724 case PREG_INTERNAL_ERROR:
725 $error = 'Internal PCRE error on line';
726 break;
727 case PREG_BACKTRACK_LIMIT_ERROR:
728 $error = 'pcre.backtrack_limit reached on line';
729 break;
730 case PREG_RECURSION_LIMIT_ERROR:
731 $error = 'pcre.recursion_limit reached on line';
732 break;
733 case PREG_BAD_UTF8_ERROR:
734 $error = 'Malformed UTF-8 data on line';
735 break;
736 case PREG_BAD_UTF8_OFFSET_ERROR:
737 $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point on line';
738 break;
739 default:
740 $error = 'Unable to parse line';
741 }
742
743 throw new Exception(sprintf('%s %d (%s).', $error, $this->getRealCurrentLineNb() + 1,
744 $this->currentLine));
745 }
746
747 if ($isRef) {
748 $this->refs[$isRef] = end($data);
749 }
750 }
751
752 if (isset($mbEncoding)) {
753 mb_internal_encoding($mbEncoding);
754 }
755
756 return empty($data) ? null : $data;
757 }
758
759 /**
760 * Returns the current line number (takes the offset into account).
761 *
762 * @return integer The current line number
763 */
764 protected function _getRealCurrentLineNb() {
765 return $this->currentLineNb + $this->offset;
766 }
767
768 /**
769 * Returns the current line indentation.
770 *
771 * @return integer The current line indentation
772 */
773 protected function _getCurrentLineIndentation() {
774 return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
775 }
776
777 /**
778 * Returns the next embed block of YAML.
779 *
780 * @param integer $indentation The indent level at which the block is to be read, or null for default
781 * @return string A YAML string
782 */
783 protected function _getNextEmbedBlock($indentation = null) {
784 $this->_moveToNextLine();
785
786 if (null === $indentation) {
787 $newIndent = $this->_getCurrentLineIndentation();
788
789 if (!$this->_isCurrentLineEmpty() && 0 == $newIndent) {
790 throw new Exception(sprintf('Indentation problem at line %d (%s)',
791 $this->_getRealCurrentLineNb() + 1, $this->currentLine));
792 }
793 } else {
794 $newIndent = $indentation;
795 }
796
797 $data = array(substr($this->currentLine, $newIndent));
798
799 while ($this->_moveToNextLine()) {
800 if ($this->_isCurrentLineEmpty()) {
801 if ($this->_isCurrentLineBlank()) {
802 $data[] = substr($this->currentLine, $newIndent);
803 }
804
805 continue;
806 }
807
808 $indent = $this->_getCurrentLineIndentation();
809
810 if (preg_match('#^(?P<text> *)$#', $this->currentLine, $match)) {
811 // empty line
812 $data[] = $match['text'];
813 } else if ($indent >= $newIndent) {
814 $data[] = substr($this->currentLine, $newIndent);
815 } else if (0 == $indent) {
816 $this->_moveToPreviousLine();
817
818 break;
819 } else {
820 throw new Exception(sprintf('Indentation problem at line %d (%s)',
821 $this->_getRealCurrentLineNb() + 1, $this->currentLine));
822 }
823 }
824
825 return implode("\n", $data);
826 }
827
828 /**
829 * Moves the parser to the next line.
830 */
831 protected function _moveToNextLine() {
832 if ($this->currentLineNb >= count($this->lines) - 1) {
833 return false;
834 }
835
836 $this->currentLine = $this->lines[++$this->currentLineNb];
837
838 return true;
839 }
840
841 /**
842 * Moves the parser to the previous line.
843 */
844 protected function _moveToPreviousLine() {
845 $this->currentLine = $this->lines[--$this->currentLineNb];
846 }
847
848 /**
849 * Parses a YAML value.
850 *
851 * @param string $value A YAML value
852 * @return mixed A PHP value
853 */
854 protected function _parseValue($value) {
855 if ('*' === substr($value, 0, 1)) {
856 if (false !== $pos = strpos($value, '#')) {
857 $value = substr($value, 1, $pos - 2);
858 } else {
859 $value = substr($value, 1);
860 }
861
862 if (!array_key_exists($value, $this->refs)) {
863 throw new Exception(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine));
864 }
865 return $this->refs[$value];
866 }
867
868 if (preg_match('/^(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments>
+#.*)?$/',
869 $value, $matches)) {
870 $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
871
872 return $this->_parseFoldedScalar($matches['separator'],
873 preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
874 } else {
875 return sfYamlInline::load($value);
876 }
877 }
878
879 /**
880 * Parses a folded scalar.
881 *
882 * @param string $separator The separator that was used to begin this folded scalar (| or >)
883 * @param string $indicator The indicator that was used to begin this folded scalar (+ or -)
884 * @param integer $indentation The indentation that was used to begin this folded scalar
885 * @return string The text value
886 */
887 protected function _parseFoldedScalar($separator, $indicator = '', $indentation = 0) {
888 $separator = '|' == $separator ? "\n" : ' ';
889 $text = '';
890
891 $notEOF = $this->_moveToNextLine();
892
893 while ($notEOF && $this->_isCurrentLineBlank()) {
894 $text .= "\n";
895
896 $notEOF = $this->_moveToNextLine();
897 }
898
899 if (!$notEOF) {
900 return '';
901 }
902
903 if (!preg_match('#^(?P<indent>'.($indentation ? str_repeat(' ', $indentation) : ' +').')(?P<text>.*)$#u',
904 $this->currentLine, $matches)) {
905 $this->_moveToPreviousLine();
906
907 return '';
908 }
909
910 $textIndent = $matches['indent'];
911 $previousIndent = 0;
912
913 $text .= $matches['text'].$separator;
914 while ($this->currentLineNb + 1 < count($this->lines)) {
915 $this->_moveToNextLine();
916
917 if (preg_match('#^(?P<indent> {'.strlen($textIndent).',})(?P<text>.+)$#u', $this->currentLine,
$matches)) {
918 if (' ' == $separator && $previousIndent != $matches['indent']) {
919 $text = substr($text, 0, -1)."\n";
920 }
921 $previousIndent = $matches['indent'];
922
923 $text .= str_repeat(' ', $diff = strlen($matches['indent']) -
924 strlen($textIndent)).$matches['text'].($diff ? "\n" : $separator);
925 } else if (preg_match('#^(?P<text> *)$#', $this->currentLine, $matches)) {
926 $text .= preg_replace('#^ {1,'.strlen($textIndent).'}#', '', $matches['text'])."\n";
927 } else {
928 $this->_moveToPreviousLine();
929
930 break;
931 }
932 }
933
934 if (' ' == $separator) {
935 // replace last separator by a newline
936 $text = preg_replace('/ (\n*)$/', "\n$1", $text);
937 }
938
939 switch ($indicator) {
940 case '':
941 $text = preg_replace('#\n+$#s', "\n", $text);
942 break;
943 case '+':
944 break;
945 case '-':
946 $text = preg_replace('#\n+$#s', '', $text);
947 break;
948 }
949
950 return $text;
951 }
952
953 /**
954 * Returns true if the next line is indented.
955 *
956 * @return Boolean Returns true if the next line is indented, false otherwise
957 */
958 protected function _isNextLineIndented() {
959 $currentIndentation = $this->_getCurrentLineIndentation();
960 $notEOF = $this->_moveToNextLine();
961
962 while ($notEOF && $this->_isCurrentLineEmpty()) {
963 $notEOF = $this->_moveToNextLine();
964 }
965
966 if (false === $notEOF) {
967 return false;
968 }
969
970 $ret = false;
971 if ($this->_getCurrentLineIndentation() <= $currentIndentation) {
972 $ret = true;
973 }
974
975 $this->_moveToPreviousLine();
976
977 return $ret;
978 }
979
980 /**
981 * Returns true if the current line is blank or if it is a comment line.
982 *
983 * @return Boolean Returns true if the current line is empty or if it is a comment line, false otherwise
984 */
985 protected function _isCurrentLineEmpty() {
986 return $this->_isCurrentLineBlank() || $this->_isCurrentLineComment();
987 }
988
989 /**
990 * Returns true if the current line is blank.
991 *
992 * @return Boolean Returns true if the current line is blank, false otherwise
993 */
994 protected function _isCurrentLineBlank() {
995 return '' == trim($this->currentLine, ' ');
996 }
997
998 /**
999 * Returns true if the current line is a comment line.
1000 *
1001 * @return Boolean Returns true if the current line is a comment line, false otherwise
1002 */
1003 protected function _isCurrentLineComment() {
1004 //checking explicitly the first char of the trim is faster than loops or strpos
1005 $ltrimmedLine = ltrim($this->currentLine, ' ');
1006 return $ltrimmedLine[0] === '#';
1007 }
1008
1009 /**
1010 * Cleanups a YAML string to be parsed.
1011 *
1012 * @param string $value The input YAML string
1013 * @return string A cleaned up YAML string
1014 */
1015 protected function _cleanup($value) {
1016 $value = str_replace(array("\r\n", "\r"), "\n", $value);
1017
1018 if (!preg_match("#\n$#", $value)) {
1019 $value .= "\n";
1020 }
1021
1022 // strip YAML header
1023 $count = 0;
1024 $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#su', '', $value, -1, $count);
1025 $this->offset += $count;
1026
1027 // remove leading comments
1028 $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
1029 if ($count == 1) {
1030 // items have been removed, update the offset
1031 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
1032 $value = $trimmedValue;
1033 }
1034
1035 // remove start of the document marker (---)
1036 $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
1037 if ($count == 1) {
1038 // items have been removed, update the offset
1039 $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
1040 $value = $trimmedValue;
1041
1042 // remove end of the document marker (...)
1043 $value = preg_replace('#\.\.\.\s*$#s', '', $value);
1044 }
1045
1046 return $value;
1047 }
1048 }
1049