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