Development

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

You must first sign up to be able to contribute.

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

Revision 14516, 14.3 kB (checked in by FabianLange, 5 years ago)

[1.2] fixed multiple selector chaining in sfDomCssSelector.class. fixed #5321

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