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) 2004-2006 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
/**
12
 * sfDomCssSelector allows to navigate a DOM with CSS selector.
13
 *
14
 * Based on getElementsBySelector version 0.4 - Simon Willison, March 25th 2003
15
 * http://simon.incutio.com/archive/2003/03/25/getElementsBySelector
16
 *
17
 * Some methods based on the jquery library
18
 *
19
 * @package    symfony
20
 * @subpackage util
21
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
22
 * @version    SVN: $Id: sfDomCssSelector.class.php 31893 2011-01-24 18:11:45Z fabien $
23
 */
24
class sfDomCssSelector implements Countable, Iterator
25
{
26
  public $nodes = array();
27
 
28
  private $count;
29
 
30
  public function __construct($nodes)
31
  {
32
    if (!is_array($nodes))
33
    {
34
      $nodes = array($nodes);
35
    }
36
 
37
    $this->nodes = $nodes;
38
  }
39
 
40
  public function getNodes()
41
  {
42
    return $this->nodes;
43
  }
44
 
45
  public function getNode()
46
  {
47
    return $this->nodes ? $this->nodes[0] : null;
48
  }
49
 
50
  public function getValue()
51
  {
52
    return $this->nodes[0]->nodeValue;
53
  }
54
 
55
  public function getValues()
56
  {
57
    $values = array();
58
    foreach ($this->nodes as $node)
59
    {
60
      $values[] = $node->nodeValue;
61
    }
62
 
63
    return $values;
64
  }
65
 
66
  public function matchSingle($selector)
67
  {
68
    $nodes = $this->getElements($selector);
69
 
70
    return $nodes ? new sfDomCssSelector($nodes[0]) : new sfDomCssSelector(array());
71
  }
72
 
73
  public function matchAll($selector)
74
  {
75
    $nodes = $this->getElements($selector);
76
 
77
    return $nodes ? new sfDomCssSelector($nodes) : new sfDomCssSelector(array());
78
  }
79
 
80
  protected function getElements($selector)
81
  {
82
    $nodes = array();
83
    foreach ($this->nodes as $node)
84
    {
85
      $result_nodes = $this->getElementsForNode($selector, $node);
86
      if ($result_nodes)
87
      {
88
        $nodes = array_merge($nodes, $result_nodes);
89
      }
90
    }
91
 
92
    foreach ($nodes as $node)
93
    {
94
      $node->removeAttribute('sf_matched');
95
    }
96
 
97
    return $nodes;
98
  }
99
 
100
  protected function getElementsForNode($selector, $root_node)
101
  {
102
    $all_nodes = array();
103
    foreach ($this->tokenize_selectors($selector) as $selector)
104
    {
105
      $nodes = array($root_node);
106
      foreach ($this->tokenize($selector) as $token)
107
      {
108
        $combinator = $token['combinator'];
109
        $selector = $token['selector'];
110
 
111
        $token = trim($token['name']);
112
 
113
        $pos = strpos($token, '#');
114
        if (false !== $pos && preg_match('/^[A-Za-z0-9]*$/', substr($token, 0, $pos)))
115
        {
116
          // Token is an ID selector
117
          $tagName = substr($token, 0, $pos);
118
          $id = substr($token, $pos + 1);
119
          $xpath = new DomXPath($root_node);
120
          $element = $xpath->query(sprintf("//*[@id = '%s']", $id))->item(0);
121
          if (!$element || ($tagName && strtolower($element->nodeName) != $tagName))
122
          {
123
            // tag with that ID not found
124
            return array();
125
          }
126
 
127
          // Set nodes to contain just this element
128
          $nodes = array($element);
129
          $nodes = $this->matchMultipleCustomSelectors($nodes, $selector);
130
 
131
          continue; // Skip to next token
132
        }
133
 
134
        $pos = strpos($token, '.');
135
        if (false !== $pos && preg_match('/^[A-Za-z0-9\*]*$/', substr($token, 0, $pos)))
136
        {
137
          // Token contains a class selector
138
          $tagName = substr($token, 0, $pos);
139
          if (!$tagName)
140
          {
141
            $tagName = '*';
142
          }
143
          $className = substr($token, $pos + 1);
144
 
145
          // Get elements matching tag, filter them for class selector
146
          $founds = $this->getElementsByTagName($nodes, $tagName, $combinator);
147
          $nodes = array();
148
          foreach ($founds as $found)
149
          {
150
            if (preg_match('/(^|\s+)'.$className.'($|\s+)/', $found->getAttribute('class')))
151
            {
152
              $nodes[] = $found;
153
            }
154
          }
155
 
156
          $nodes = $this->matchMultipleCustomSelectors($nodes, $selector);
157
 
158
          continue; // Skip to next token
159
        }
160
 
161
        // Code to deal with attribute selectors
162
        if (preg_match('/^(\w+|\*)(\[.+\])$/', $token, $matches))
163
        {
164
          $tagName = $matches[1] ? $matches[1] : '*';
165
          preg_match_all('/
166
            \[
167
              ([\w\-]+)             # attribute
168
              ([=~\|\^\$\*]?)       # modifier (optional)
169
              =?                    # equal (optional)
170
              (
171
                "([^"]*)"           # quoted value (optional)
172
                |
173
                ([^\]]*)            # non quoted value (optional)
174
              )
175
            \]
176
          /x', $matches[2], $matches, PREG_SET_ORDER);
177
 
178
          // Grab all of the tagName elements within current node
179
          $founds = $this->getElementsByTagName($nodes, $tagName, $combinator);
180
          $nodes = array();
181
          foreach ($founds as $found)
182
          {
183
            $ok = false;
184
            foreach ($matches as $match)
185
            {
186
              $attrName = $match[1];
187
              $attrOperator = $match[2];
188
              $attrValue = $match[4] === '' ? (isset($match[5]) ? $match[5] : '') : $match[4];
189
 
190
              switch ($attrOperator)
191
              {
192
                case '=': // Equality
193
                  $ok = $found->getAttribute($attrName) == $attrValue;
194
                  break;
195
                case '~': // Match one of space seperated words
196
                  $ok = preg_match('/\b'.preg_quote($attrValue, '/').'\b/', $found->getAttribute($attrName));
197
                  break;
198
                case '|': // Match start with value followed by optional hyphen
199
                  $ok = preg_match('/^'.preg_quote($attrValue, '/').'-?/', $found->getAttribute($attrName));
200
                  break;
201
                case '^': // Match starts with value
202
                  $ok = 0 === strpos($found->getAttribute($attrName), $attrValue);
203
                  break;
204
                case '$': // Match ends with value
205
                  $ok = $attrValue == substr($found->getAttribute($attrName), -strlen($attrValue));
206
                  break;
207
                case '*': // Match ends with value
208
                  $ok = false !== strpos($found->getAttribute($attrName), $attrValue);
209
                  break;
210
                default :
211
                  // Just test for existence of attribute
212
                  $ok = $found->hasAttribute($attrName);
213
              }
214
 
215
              if (false == $ok)
216
              {
217
                break;
218
              }
219
            }
220
 
221
            if ($ok)
222
            {
223
              $nodes[] = $found;
224
            }
225
          }
226
 
227
          continue; // Skip to next token
228
        }
229
 
230
        // If we get here, token is JUST an element (not a class or ID selector)
231
        $nodes = $this->getElementsByTagName($nodes, $token, $combinator);
232
 
233
        $nodes = $this->matchMultipleCustomSelectors($nodes, $selector);
234
      }
235
 
236
      foreach ($nodes as $node)
237
      {
238
        if (!$node->getAttribute('sf_matched'))
239
        {
240
          $node->setAttribute('sf_matched', true);
241
          $all_nodes[] = $node;
242
        }
243
      }
244
    }
245
 
246
    return $all_nodes;
247
  }
248
 
249
  protected function getElementsByTagName($nodes, $tagName, $combinator = ' ')
250
  {
251
    $founds = array();
252
    foreach ($nodes as $node)
253
    {
254
      switch ($combinator)
255
      {
256
        case ' ':
257
          // Descendant selector
258
          foreach ($node->getElementsByTagName($tagName) as $element)
259
          {
260
            $founds[] = $element;
261
          }
262
          break;
263
        case '>':
264
          // Child selector
265
          foreach ($node->childNodes as $element)
266
          {
267
            if ($tagName == $element->nodeName)
268
            {
269
              $founds[] = $element;
270
            }
271
          }
272
          break;
273
        case '+':
274
          // Adjacent selector
275
          $element = $node->nextSibling;
276
          if ($element && '#text' == $element->nodeName)
277
          {
278
            $element = $element->nextSibling;
279
          }
280
 
281
          if ($element && $tagName == $element->nodeName)
282
          {
283
            $founds[] = $element;
284
          }
285
          break;
286
        default:
287
          throw new Exception(sprintf('Unrecognized combinator "%s".', $combinator));
288
      }
289
    }
290
 
291
    return $founds;
292
  }
293
 
294
  protected function tokenize_selectors($selector)
295
  {
296
    // split tokens by , except in an attribute selector
297
    $tokens = array();
298
    $quoted = false;
299
    $token = '';
300
    for ($i = 0, $max = strlen($selector); $i < $max; $i++)
301
    {
302
      if (',' == $selector[$i] && !$quoted)
303
      {
304
        $tokens[] = trim($token);
305
        $token = '';
306
      }
307
      else if ('"' == $selector[$i])
308
      {
309
        $token .= $selector[$i];
310
        $quoted = $quoted ? false : true;
311
      }
312
      else
313
      {
314
        $token .= $selector[$i];
315
      }
316
    }
317
    if ($token)
318
    {
319
      $tokens[] = trim($token);
320
    }
321
 
322
    return $tokens;
323
  }
324
 
325
  protected function tokenize($selector)
326
  {
327
    // split tokens by space except if space is in an attribute selector
328
    $tokens = array();
329
    $combinators = array(' ', '>', '+');
330
    $quoted = false;
331
    $token = array('combinator' => ' ', 'name' => '');
332
    for ($i = 0, $max = strlen($selector); $i < $max; $i++)
333
    {
334
      if (in_array($selector[$i], $combinators) && !$quoted)
335
      {
336
        // remove all whitespaces around the combinator
337
        $combinator = $selector[$i];
338
        while (in_array($selector[$i + 1], $combinators))
339
        {
340
          if (' ' != $selector[++$i])
341
          {
342
            $combinator = $selector[$i];
343
          }
344
        }
345
 
346
        $tokens[] = $token;
347
        $token = array('combinator' => $combinator, 'name' => '');
348
      }
349
      else if ('"' == $selector[$i])
350
      {
351
        $token['name'] .= $selector[$i];
352
        $quoted = $quoted ? false : true;
353
      }
354
      else
355
      {
356
        $token['name'] .= $selector[$i];
357
      }
358
    }
359
    if ($token['name'])
360
    {
361
      $tokens[] = $token;
362
    }
363
 
364
    foreach ($tokens as &$token)
365
    {
366
      list($token['name'], $token['selector']) = $this->tokenize_selector_name($token['name']);
367
    }
368
 
369
    return $tokens;
370
  }
371
 
372
  protected function tokenize_selector_name($token_name)
373
  {
374
    // split custom selector
375
    $quoted = false;
376
    $name = '';
377
    $selector = '';
378
    $in_selector = false;
379
    for ($i = 0, $max = strlen($token_name); $i < $max; $i++)
380
    {
381
      if ('"' == $token_name[$i])
382
      {
383
        $quoted = $quoted ? false : true;
384
      }
385
 
386
      if (!$quoted && ':' == $token_name[$i])
387
      {
388
        $in_selector = true;
389
      }
390
 
391
      if ($in_selector)
392
      {
393
        $selector .= $token_name[$i];
394
      }
395
      else
396
      {
397
        $name .= $token_name[$i];
398
      }
399
    }
400
 
401
    return array($name, $selector);
402
  }
403
 
404
  protected function matchMultipleCustomSelectors($nodes, $selector)
405
  {
406
    if (!$selector)
407
    {
408
      return $nodes;
409
    }
410
 
411
    foreach ($this->split_custom_selector($selector) as $selector) {
412
      $nodes = $this->matchCustomSelector($nodes, $selector);
413
    }
414
    return $nodes;
415
  }
416
 
417
  protected function matchCustomSelector($nodes, $selector)
418
  {
419
    if (!$selector)
420
    {
421
      return $nodes;
422
    }
423
 
424
    $selector = $this->tokenize_custom_selector($selector);
425
    $matchingNodes = array();
426
    for ($i = 0, $max = count($nodes); $i < $max; $i++)
427
    {
428
      switch ($selector['selector'])
429
      {
430
        case 'contains':
431
          if (false !== strpos($nodes[$i]->textContent, $selector['parameter']))
432
          {
433
            $matchingNodes[] = $nodes[$i];
434
          }
435
          break;
436
        case 'nth-child':
437
          if ($nodes[$i] === $this->nth($nodes[$i]->parentNode->firstChild, (integer) $selector['parameter']))
438
          {
439
            $matchingNodes[] = $nodes[$i];
440
          }
441
          break;
442
        case 'first-child':
443
          if ($nodes[$i] === $this->nth($nodes[$i]->parentNode->firstChild))
444
          {
445
            $matchingNodes[] = $nodes[$i];
446
          }
447
          break;
448
        case 'last-child':
449
          if ($nodes[$i] === $this->nth($nodes[$i]->parentNode->lastChild, 1, 'previousSibling'))
450
          {
451
            $matchingNodes[] = $nodes[$i];
452
          }
453
          break;
454
        case 'lt':
455
          if ($i < (integer) $selector['parameter'])
456
          {
457
            $matchingNodes[] = $nodes[$i];
458
          }
459
          break;
460
        case 'gt':
461
          if ($i > (integer) $selector['parameter'])
462
          {
463
            $matchingNodes[] = $nodes[$i];
464
          }
465
          break;
466
        case 'odd':
467
          if ($i % 2)
468
          {
469
            $matchingNodes[] = $nodes[$i];
470
          }
471
          break;
472
        case 'even':
473
          if (0 == $i % 2)
474
          {
475
            $matchingNodes[] = $nodes[$i];
476
          }
477
          break;
478
        case 'nth':
479
        case 'eq':
480
          if ($i == (integer) $selector['parameter'])
481
          {
482
            $matchingNodes[] = $nodes[$i];
483
          }
484
          break;
485
        case 'first':
486
          if ($i == 0)
487
          {
488
            $matchingNodes[] = $nodes[$i];
489
          }
490
          break;
491
        case 'last':
492
          if ($i == $max - 1)
493
          {
494
            $matchingNodes[] = $nodes[$i];
495
          }
496
          break;
497
        default:
498
          throw new Exception(sprintf('Unrecognized selector "%s".', $selector['selector']));
499
      }
500
    }
501
 
502
    return $matchingNodes;
503
  }
504
 
505
  protected function split_custom_selector($selectors)
506
  {
507
    if (!preg_match_all('/
508
      :
509
      (?:[a-zA-Z0-9\-]+)
510
      (?:
511
        \(
512
          (?:
513
            ("|\')(?:.*?)?\1
514
            |
515
            (?:.*?)
516
          )
517
        \)
518
      )?
519
    /x', $selectors, $matches, PREG_PATTERN_ORDER))
520
    {
521
      throw new Exception(sprintf('Unable to split custom selector "%s".', $selectors));
522
    }
523
    return $matches[0];
524
  }
525
 
526
  protected function tokenize_custom_selector($selector)
527
  {
528
    if (!preg_match('/
529
      ([a-zA-Z0-9\-]+)
530
      (?:
531
        \(
532
          (?:
533
            ("|\')(.*)?\2
534
            |
535
            (.*?)
536
          )
537
        \)
538
      )?
539
    /x', substr($selector, 1), $matches))
540
    {
541
      throw new Exception(sprintf('Unable to parse custom selector "%s".', $selector));
542
    }
543
    return array('selector' => $matches[1], 'parameter' => isset($matches[3]) ? ($matches[3] ? $matches[3] : $matches[4]) : '');
544
  }
545
 
546
  protected function nth($cur, $result = 1, $dir = 'nextSibling')
547
  {
548
    $num = 0;
549
    for (; $cur; $cur = $cur->$dir)
550
    {
551
      if (1 == $cur->nodeType)
552
      {
553
        ++$num;
554
      }
555
 
556
      if ($num == $result)
557
      {
558
        return $cur;
559
      }
560
    }
561
  }
562
 
563
  /**
564
   * Reset the array to the beginning (as required for the Iterator interface).
565
   */
566
  public function rewind()
567
  {
568
    reset($this->nodes);
569
 
570
    $this->count = count($this->nodes);
571
  }
572
 
573
  /**
574
   * Get the key associated with the current value (as required by the Iterator interface).
575
   *
576
   * @return string The key
577
   */
578
  public function key()
579
  {
580
    return key($this->nodes);
581
  }
582
 
583
  /**
584
   * Escapes and return the current value (as required by the Iterator interface).
585
   *
586
   * @return mixed The escaped value
587
   */
588
  public function current()
589
  {
590
    return current($this->nodes);
591
  }
592
 
593
  /**
594
   * Moves to the next element (as required by the Iterator interface).
595
   */
596
  public function next()
597
  {
598
    next($this->nodes);
599
 
600
    $this->count --;
601
  }
602
 
603
  /**
604
   * Returns true if the current element is valid (as required by the Iterator interface).
605
   *
606
   * The current element will not be valid if {@link next()} has fallen off the
607
   * end of the array or if there are no elements in the array and {@link
608
   * rewind()} was called.
609
   *
610
   * @return bool The validity of the current element; true if it is valid
611
   */
612
  public function valid()
613
  {
614
    return $this->count > 0;
615
  }
616
 
617
  /**
618
   * Returns the number of matching nodes (implements Countable).
619
   *
620
   * @param integer The number of matching nodes
621
   */
622
  public function count()
623
  {
624
    return count($this->nodes);
625
  }
626
}