Development

/branches/1.3/lib/util/sfDomCssSelector.class.php

You must first sign up to be able to contribute.

root/branches/1.3/lib/util/sfDomCssSelector.class.php

Revision 31890, 15.8 kB (checked in by fabien, 3 years ago)

fixed sfDomCssSelector on class names with hyphens (patch from richsage, closes #9411)

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