Development

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

You must first sign up to be able to contribute.

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

Revision 29521, 15.6 kB (checked in by fabien, 5 years ago)

[1.3, 1.4] fixed E_STRICT compatbility of some unit tests (closes #8522)

  • 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   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('/\b'.$className.'\b/', $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 }
627
Note: See TracBrowser for help on using the browser.