Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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