source: spip-zone/_plugins_/facteur/branches/v1/inline-style/CSSQuery.php @ 55173

Last change on this file since 55173 was 55173, checked in by real3t@…, 8 years ago

Il manquait 2 pseudo class : before et after

File size: 45.9 KB
Line 
1<?php
2/**
3 * This file has had some love from Christiaan Baartse <christiaan@baartse.nl>
4 *
5 *    This package contains one class for using Cascading Style Sheet
6 *    selectors to retrieve elements from a DOMDocument object similarly
7 *    to DOMXPath does with XPath selectors
8 *   
9 *    PHP version 5
10 *
11 *    @category   HTML
12 *    @package    CSSQuery
13 *    @author     Sam Shull <sam.shull@jhspecialty.com>
14 *    @copyright  Copyright (c) 2009 Sam Shull <sam.shull@jhspeicalty.com>
15 *    @license    <http://www.opensource.org/licenses/mit-license.html>
16 *    @version    1.4
17 *
18 *    Permission is hereby granted, free of charge, to any person obtaining a copy
19 *    of this software and associated documentation files (the "Software"), to deal
20 *    in the Software without restriction, including without limitation the rights
21 *    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22 *    copies of the Software, and to permit persons to whom the Software is
23 *    furnished to do so, subject to the following conditions:
24 *   
25 *    The above copyright notice and this permission notice shall be included in
26 *    all copies or substantial portions of the Software.
27 *   
28 *    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29 *    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30 *    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31 *    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32 *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33 *    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34 *    THE SOFTWARE.
35 *
36 *    CHANGES:
37 *        06-08-2009 - added normalize-space function to CSSQuery::className
38 *                     and removed unecessary sprintf(s) in favor of " strings
39 *                     and fixed runtime pass-by-reference errors
40 *        07-14-2009 - added references and type hinting to many of the functions
41 *                     in order to improve performance a little
42 *        07-25-2009 - added support for class (.) and id (#) as filters (div#id.class)
43 *        08-05-2009 - corrected my horrible typing errors
44 *                     changed the attribute filter handling to match the entire operator
45 */
46
47/**
48 *    Perform a CSS query on a DOMDocument using DOMXPath
49 *
50 *    <code>
51 *        $doc = new DOMDocument();
52 *        $doc->loadHTML('<html><body><p>hello world</p></body></html>');
53 *        $css = new CSSQuery($doc);
54 *        print count( $css->query("p:contains('hello world')") );
55 *    </code>
56 *   
57 *
58 *    @category   HTML
59 *    @package    CSSQuery
60 *    @author     Sam Shull <sam.shull@jhspecialty.com>
61 *    @copyright  Copyright (c) 2009 Sam Shull <sam.shull@jhspeicalty.com>
62 *    @license    <http://www.opensource.org/licenses/mit-license.html>
63 *    @version    Release: @package_version@
64 *    @link       
65 *    @since      Class available since Release 1.0
66 */ 
67class CSSQuery
68{
69    /**
70     *    This PCRE string matches one valid css selector at a time
71     *
72     *    @const string
73     */ 
74    const CHUNKER = '/^\s*([#\.>~\+:\[,]?)\s*(\*|[^\*#\.>~\+:\[\]\)\(\s,]*)/';
75   
76    /**
77     *    This PCRE string matches one psuedo css selector at a time
78     *
79     *    @const string
80     */ 
81    const PSUEDO = '/^\s*:([\w\-]+)\s*(\(\s*([^\(\)]*(\([^\(\)]*\))?)?\s*\))?\s*/';
82   
83    /**
84     *    This PCRE string matches one css attribute selector at a time
85     *
86     *    @const string
87     */ 
88    const ATTRIBUTES = '/\[@?([\w\-]+(\|[\w\-]+)?)\s*((\S*=)\s*([\'"]?)(?(5)([^\\5]*)\\5|([^\]]+)))?\s*\]/i';
89   
90    /**
91     *    An array of functions representing psuedo selectors
92     *
93     *    @access public
94     *
95     *    @staticvar array
96     */
97    public static $filters;
98   
99    /**
100     *    An array of functions representing attribute selectors
101     *
102     *    @access public
103     *
104     *    @staticvar array
105     */
106    public static $attributeFilters;
107   
108    /**
109     *    An instance of DOMXPath for finding the information on the document
110     *
111     *    @access public
112     *
113     *    @var DOMXPath
114     */
115    public $xpath;
116   
117    /**
118     *    The document that the queries will originate from
119     *
120     *    @access public
121     *
122     *    @var DOMDocument
123     */
124    public $document;
125   
126    /**
127     *    Initialize the object - opens a new DOMXPath
128     *
129     *    @access public
130     *
131     *    @param DOMDocument $document
132     */
133    public function __construct (DOMDocument &$document)
134    {
135        $this->xpath = new DOMXPath($document);
136        $this->document =& $document;
137    }
138   
139    /**
140     *    register a namespace
141     *
142     *    @access public
143     *
144     *    @param string $prefix
145     *    @param string $URI
146     *
147     *    @returns boolean
148     */
149    public function registerNamespace ($prefix, $URI)
150    {
151        return $this->xpath->registerNamespace($prefix, $URI);
152    }
153   
154    /**
155     *    Get an array of DOMNodes that match a CSS query expression
156     *
157     *    @access public
158     *
159     *    @param string $expression
160     *    @param mixed $context - a DOMNode or an array of DOMNodes
161     *
162     *    @returns array
163     */
164    public function query ($expression, $context=null)
165    {
166        $original_context = func_num_args() < 3 ? $context : func_get_arg(2);
167       
168        $current = $context instanceof DOMNode ? array($context) : self::makeArray($context);
169       
170        $new = array();
171       
172        $m = array('');
173   
174        if ($expression && preg_match(self::CHUNKER, $expression, $m))
175        {
176            //replace a pipe with a semi-colon in a selector
177            //for namespace uses
178            $m[2] = $m[2] ? str_replace('|', ':', $m[2]) : '*';
179           
180            switch ($m[1])
181            {
182                case ',':
183                {
184                    $new = $this->query(ltrim(substr($expression, strpos($expression, $m[1]) + 1)), array(), $original_context);
185                    $new = array_merge($current, $new);
186                    return self::unique($new);
187                }
188                //#id
189                case '#':   
190                {
191                    $new = $this->id($m[2], $current);
192                    break;
193                }
194                //.class
195                case '.':   
196                {
197                    $new = $this->className($m[2], $current);
198                    break;
199                }
200                // > child
201                case '>':   
202                {
203                    $new = $this->children($m[2], $current);
204                   
205                    break;
206                }
207                // + adjacent sibling
208                case '+':   
209                {
210                    $new = $this->adjacentSibling($m[2],$current);
211                   
212                    break;
213                }
214                // ~ general sibling
215                case '~':   
216                {
217                    $new = $this->generalSibling($m[2], $current);
218                   
219                    break;
220                }
221                //:psuedo-filter
222                case ':':
223                {
224                    if ($m[2] == 'root')
225                    {
226                        $new = array($this->document->documentElement);
227                    }
228                    //a psuedo selector is a filter
229                    elseif (preg_match(self::PSUEDO, $expression, $n))
230                    {
231                        if ($n[1] && isset(self::$filters[$n[1]]) && is_callable(self::$filters[$n[1]]))
232                        {
233                            if (!$current)
234                            {
235                                $current = $this->xpath->query('//*');
236                                $current = self::makeArray($current);
237                            }
238                           
239                            $i = 0;
240                           
241                            foreach ($current as $elem)
242                            {
243                                if ($item = call_user_func(self::$filters[$n[1]], $elem, $i++, $n, $current, $this))
244                                {
245                                    if ($item instanceof DOMNode)
246                                    {
247                                        if (self::inArray($item, $new) < 0)
248                                        {
249                                            $new[] = $item;
250                                        }
251                                    }
252                                    //usually boolean
253                                    elseif (is_scalar($item))
254                                    {
255                                        if ($item)
256                                        {
257                                            $new[] = $elem;
258                                        }
259                                    }
260                                    else
261                                    {
262                                        $new = array_merge($new, self::makeArray($item));
263                                        $new = self::unique($new);
264                                    }
265                                }
266                            }
267                        }
268                        else
269                        {
270                            throw new Exception("Unknown psuedo-filter: {$m[2]}, in {$expression}");
271                        }
272                       
273                        //set this for the substr
274                        $m[0] = $n[0];
275                    }
276                    else
277                    {
278                        throw new Exception("Unknown use of semi-colon: {$m[2]}, in {$expression}");
279                    }
280                    break;
281                }
282                //[attribute="value"] filter
283                case '[':
284                {
285                    if (preg_match(self::ATTRIBUTES, $expression, $n))
286                    {
287                        //change a pipe to a semi-colon for namespace purposes
288                        $n[1] = str_replace('|', ':', $n[1]);
289                       
290                        if (!isset($n[4]) || !$n[4])
291                        {
292                            $n[4] = '';
293                            $n[6] = null;
294                        }
295                       
296                        if (!isset(self::$attributeFilters[$n[4]]) || !is_callable(self::$attributeFilters[$n[4]]))
297                        {
298                            //print_r($n);
299                            //thrown if there is no viable attributeFilter function for the given operator
300                            throw new Exception("Unknown attribute filter: {$n[4]}");
301                        }
302                       
303                        if (!$current)
304                        {
305                            $current = $this->xpath->query('//*');
306                            $current = self::makeArray($current);
307                        }
308                       
309                        foreach ($current as $elem)
310                        {
311                            if (true === call_user_func(self::$attributeFilters[$n[4]], $elem, $n[1], $n[6], $n, $current))
312                            {
313                                $new[] = $elem;
314                            }
315                        }
316
317                        //set this for the substr
318                        $m[0] = $n[0];
319                    }
320                    else
321                    {
322                        //only thrown if query is malformed
323                        throw new Exception("Unidentified use of '[' in {$m[0]}");
324                    }
325                    break;
326                }
327                //just a tag - i.e. any descendant of the current context
328                default:
329                {
330                    $new = $this->tag($m[2], $current);
331                   
332                    break;
333                }
334            }
335           
336            //check for # or . as filter   
337            $exp = substr($expression, strlen($m[0]));
338           
339            while ($exp && ($exp[0] == "." || $exp[0] == "#"))
340            {
341                if (preg_match(self::CHUNKER, $exp, $m))
342                {
343                    $expression = $exp;
344                    $new = $m[1] == "." 
345                            ? $this->className($m[2], $new, true) 
346                            : $this->id($m[2], $new, true);
347                   
348                    $exp = substr($expression, strlen($m[0]));
349                }
350            }
351        }
352       
353        return strlen($m[0]) < strlen($expression) 
354                ? $this->query(substr($expression, strlen($m[0])), $new, $original_context) 
355                : self::unique($new);
356    }
357   
358    /**
359     *    get an element by its id attribute
360     *
361     *    @access public
362     *
363     *    @param string $id
364     *    @param array $context
365     *
366     *    @returns array
367     */
368    public function id (&$id, array &$context=array(), $filter=false)
369    {
370        $new = array();
371       
372        //if a context is present - div#id should act like a filter
373        if ($filter || $context)
374        {
375            foreach ($context as $elem)
376            {
377                if ($elem instanceof DOMElement && $elem->hasAttribute('id') && $elem->getAttribute('id') == $id)
378                {
379                    $new[] = $elem;
380                }
381            }
382        }
383        elseif (($items = $this->xpath->query("//*[@id='{$id}']")) && $items->length > 0)
384        {
385            foreach ($items as $item)
386            {
387                $new[] = $item;
388            }
389        }
390       
391        return $new;
392    }
393   
394    /**
395     *    get an element by its class attribute
396     *
397     *    @access public
398     *
399     *    @param string $id
400     *    @param array $context
401     *
402     *    @returns array
403     */
404    public function className (&$className, array &$context=array(), $filter=false)
405    {
406        $new = array();
407       
408        if ($filter && $context)
409        {
410            $regex = '/\s+' . preg_quote($className, '/') . '\s+/';
411           
412            foreach ($context as $elem)
413            {
414                if ($elem->hasAttribute('class') && preg_match($regex, " {$elem->getAttribute('class')} "))
415                {
416                    $new[] = $elem;
417                }
418            }
419        }
420        //if there is a context for the query
421        elseif ($context)
422        {
423            //06-08-2009 - added normalize-space function, http://westhoffswelt.de/blog/0036_xpath_to_select_html_by_class.html
424            $query = "./descendant::*[ @class and contains( concat(' ', normalize-space(@class), ' '), ' {$className} ') ]";
425           
426            foreach ($context as $elem)
427            {
428                if (
429                    ($items = $this->xpath->query($query, $elem)) && 
430                    $items->length > 0
431                )
432                {
433                    foreach ($items as $item)
434                    {
435                        $new[] = $item;
436                    }
437                }
438            }
439        }
440        //otherwise select any element in the document that matches the selector
441        elseif (($items = $this->xpath->query("//*[ @class and contains( concat(' ', normalize-space(@class), ' '), ' {$className} ') ]")) && $items->length > 0)
442        {
443            foreach ($items as $item)
444            {
445                $new[] = $item;
446            }
447        }
448       
449        return $new;
450    }
451   
452    /**
453     *    get the children elements
454     *
455     *    @access public
456     *
457     *    @param string $tag
458     *    @param array $context
459     *
460     *    @returns array
461     */
462    public function children (&$tag='*', array &$context=array())
463    {
464        $new = array();
465       
466        $query = "./{$tag}";
467       
468        //if there is a context for the query
469        if ($context)
470        {
471            foreach ($context as $elem)
472            {
473                if (($items = $this->xpath->query($query, $elem)) && $items->length > 0)
474                {
475                    foreach ($items as $item)
476                    {
477                        $new[] = $item;
478                    }
479                }
480            }
481        }
482        //otherwise select any element in the document that matches the selector
483        elseif (($items = $this->xpath->query($query, $this->document->documentElement)) && $items->length > 0)
484        {
485            foreach ($items as $item)
486            {
487                $new[] = $item;
488            }
489        }
490       
491        return $new;
492    }
493   
494    /**
495     *    get the adjacent sibling elements
496     *
497     *    @access public
498     *
499     *    @param string $tag
500     *    @param array $context
501     *
502     *    @returns array
503     */
504    public function adjacentSibling (&$tag='*', array &$context=array())
505    {
506        $new = array();
507       
508        $tag = strtolower($tag);
509       
510        //if there is a context for the query
511        if ($context)
512        {   
513            foreach ($context as $elem)
514            {
515                if ($tag == '*' || strtolower($elem->nextSibling->nodeName) == $tag)
516                {
517                    $new[] = $elem->nextSibling;
518                }
519            }
520        }
521       
522        return $new;
523    }
524   
525    /**
526     *    get the all sibling elements
527     *
528     *    @access public
529     *
530     *    @param string $tag
531     *    @param array $context
532     *
533     *    @returns array
534     */
535    public function generalSibling (&$tag='*', array &$context=array())
536    {
537        $new = array();
538       
539        //if there is a context for the query
540        if ($context)
541        {
542            $query = "./following-sibling::{$tag} | ./preceding-sibling::{$tag}";
543           
544            foreach ($context as $elem)
545            {
546                if (($items = $this->xpath->query($query, $elem)) && $items->length > 0)
547                {
548                    foreach ($items as $item)
549                    {
550                        $new[] = $item;
551                    }
552                }
553            }
554        }
555       
556        return $new;
557    }
558   
559    /**
560     *    get the all descendant elements
561     *
562     *    @access public
563     *
564     *    @param string $tag
565     *    @param array $context
566     *
567     *    @returns array
568     */
569    public function tag (&$tag='*', array &$context=array())
570    {
571        $new = array();
572       
573        //get all the descendants with the given tagName
574        if ($context)
575        {
576            $query = "./descendant::{$tag}";
577           
578            foreach ($context as $elem)
579            {
580                if ($items = $this->xpath->query($query, $elem))
581                {
582                    foreach ($items as $item)
583                    {
584                        $new[] = $item;
585                    }
586                }
587            }
588        }
589        //get all elements with the given tagName
590        else
591        {
592            if ($items = $this->xpath->query("//{$tag}"))
593            {
594                foreach ($items as $item)
595                {
596                    $new[] = $item;
597                }
598            }
599        }
600       
601        return $new;
602    }
603   
604    /**
605     *    A utility function for calculating nth-* style psuedo selectors
606     *
607     *    @static
608     *    @access public
609     *
610     *    @param DOMNode $context - the element whose position is being calculated
611     *    @param string $func - the name of the psuedo function that is being calculated for
612     *    @param string $expr - the string argument for the selector
613     *    @param DOMXPath $xpath - an existing xpath instance for the document that the context belong to
614     *
615     *    @returns boolean
616     */
617    public static function nthChild (DOMNode &$context, $func, $expr, DOMXPath &$xpath)
618    {
619        //remove all the whitespace
620        $expr = preg_replace('/\s+/', '', trim(strtolower($expr)));
621       
622        //all
623        if ($expr == 'n' || $expr == 'n+0' || $expr == '1n+0' || $expr == '1n')
624        {
625            return true;
626        }
627       
628        //the direction we will look for siblings
629        $DIR = (stristr($func, 'last') ? 'following' : 'preceding');
630       
631        //do a tagName check?
632        $type = stristr($func, 'type') ? '[local-name()=name(.)]' : '';
633       
634        //the position of this node
635        $count = $xpath->evaluate("count( {$DIR}-sibling::*{$type} ) + 1", $context);
636       
637        //odd
638        if($expr == 'odd' || $expr == '2n+1')
639        {
640            return $count % 2 != 0;
641        }
642        //even
643        elseif($expr == 'even' || $expr == '2n' || $expr == '2n+0')
644        {
645            return $count > 0 && $count % 2 == 0;
646        }
647        //a particular position
648        elseif(preg_match('/^([\+\-]?\d+)$/i', $expr, $mat))
649        {
650            $d = (stristr($func, 'last') ? -1 : 1) * intval($mat[1]);
651            $r = $xpath->query(sprintf('../%s', $type ? $context->tagName : '*'), $context);
652            return $r && $r->length >= abs($d) && ($d > 0 ? $r->item($d - 1)->isSameNode($context) : $r->item($r->length + $d)->isSameNode($context));
653        }
654        //grouped after a particular position
655        elseif(preg_match('/^([\+\-]?\d*)?n([\+\-]\d+)?/i', $expr, $mat))
656        {
657            $a = (isset($mat[2]) && $mat[2] ? intval($mat[2]) : 0);
658            $b = (isset($mat[2]) && $mat[2] ? intval($mat[2]) : 1);
659           
660            return ($a == 0 && $count == $b) ||
661                    ($a > 0 && $count >= $b && ($count - $b) % $a == 0) ||
662                    ($a < 0 && $count <= $b && (($b - $count) % ($a * -1)) == 0);
663        }
664       
665        return false;
666    }
667   
668    /**
669     *    A utility function for filtering inputs of a specific type
670     *
671     *    @static
672     *    @access public
673     *
674     *    @param mixed $elem
675     *    @param string $type
676     *
677     *    @returns boolean
678     */
679    public static function inputFilter (&$elem, $type)
680    {
681        $t = trim(strtolower($type));
682       
683                //gotta be a -DOMNode- DOMElement
684        return $elem instanceof DOMElement && 
685                //with the tagName input
686                strtolower($elem->tagName) == 'input' && 
687                (
688                    ($t == 'text' && !$elem->hasAttribute('type')) ||
689                    ($t == 'button' && strtolower($e->tagName) == "button") || 
690                    (
691                        //and the attribute type
692                        $elem->hasAttribute('type') && 
693                        //the attribute type should match the given variable type case insensitive
694                        trim(strtolower($elem->getAttribute('type'))) == $t
695                    )
696                );
697    }
698   
699    /**
700     *    A utility function for making an iterable object into an array
701     *
702     *    @static
703     *    @access public
704     *
705     *    @param array|Traversable $arr
706     *
707     *    @return array
708     */
709    public static function makeArray (&$arr)
710    {
711        if (is_array($arr))
712        {
713            return array_values($arr);
714        }
715       
716        $ret = array();
717       
718        if ($arr)
719        {
720            foreach ($arr as $elem)
721            {
722                $ret[count($ret)] = $elem;
723            }
724        }
725       
726        return $ret;
727    }
728   
729    /**
730     *    A utility function for stripping duplicate elements from an array
731     *    works on DOMNodes
732     *
733     *    @static
734     *    @access public
735     *
736     *    @param array|Traversable $arr
737     *
738     *    @returns array
739     */
740    public static function unique (&$arr)
741    {
742        //first step make sure all the elements are unique
743        $new = array();
744       
745        foreach ($arr as $current)
746        {
747            if (
748                //if the new array is empty
749                //just put the element in the array
750                empty($new) || 
751                (
752                    //if it is not an instance of a DOMNode
753                    //no need to check for isSameNode
754                    !($current instanceof DOMNode) && 
755                    !in_array($current, $new)
756                ) || 
757                //do DOMNode test on array
758                self::inArray($current, $new) < 0
759            )
760            {
761                $new[] = $current;
762            }
763        }
764       
765        return $new;
766    }
767   
768    /**
769     *    A utility function for determining the position of an element in an array
770     *    works on DOMNodes, returns -1 on failure
771     *
772     *    @static
773     *    @access public
774     *
775     *    @param mixed $elem
776     *    @param array|Traversable $arr
777     *
778     *    @returns integer
779     */
780    public static function inArray (DOMNode $elem, $arr)
781    {
782        $i = 0;
783       
784        foreach ($arr as $current)
785        {
786            //if it is an identical object or a DOMElement that represents the same node
787            if ($current === $elem || ($current instanceof DOMNode && $current->isSameNode($elem)))
788            {
789                return $i;
790            }
791           
792            $i += 1;
793        }
794       
795        return -1;
796    }
797   
798    /**
799     *    A utility function for filtering elements from an array or array-like object
800     *
801     *    @static
802     *    @access public
803     *
804     *    @param mixed $elem
805     *    @param array|Traversable $arr
806     *
807     *    @returns array
808     */
809    public static function filter ($array, $func)
810    {
811        $ret = array();
812       
813        if (!is_callable($func))
814        {
815            return $array;
816        }
817       
818        foreach ($array as $n => $v)
819        {
820            if (false !== call_user_func($func, $v, $n, $array, $this))
821            {
822                $ret[] = $v;
823            }
824        }
825       
826        return $ret;
827    }
828   
829    /**
830     *    A static function designed to make it easier to get the info
831     *
832     *    @static
833     *    @access public
834     *
835     *    @param string $query
836     *    @param mixed $context
837     *    @param array|Traversable $ret - passed by reference
838     *
839     *    @return array
840     */
841    public static function find ($query, $context, $ret=null)
842    {
843        $new = array();
844       
845        //query using DOMDocument
846        if ($context instanceof DOMDocument)
847        {
848            $css = new self($context);
849            $new = $css->query($query);
850        }
851        elseif ($context instanceof DOMNodeList)
852        {
853            if ($context->length)
854            {
855                $css = new self($context->item(0)->ownerDocument);
856                $new = $css->query($query, $context);
857            }
858        }
859        //should be an array if it isn't a DOMNode
860        //in which case the first element should be a DOMNode
861        //representing the desired context
862        elseif (!($context instanceof DOMNode) && count($context))
863        {
864            $css = new self($context[0]->ownerDocument);
865            $new = $css->query($query, $context);
866        }
867        //otherwise use the ownerDocument and the context as the context of the query
868        else
869        {
870            $css = new self($context->ownerDocument);
871            $new = $css->query($query, $context);
872        }
873       
874        //if there is a place to store the newly selected elements
875        if ($ret)
876        {
877            //append the newly selected elements to the given array|object
878            //or if it is an instance of ArrayAccess just push it on to the object
879            if (is_array($ret))
880            {
881                $new = array_merge($ret, $new);
882                $new = self::unique($new);
883                $ret = $new;
884            }
885            elseif (is_object($ret))
886            {
887                if ($ret instanceof ArrayAccess)
888                {
889                    foreach ($new as $elem)
890                    {
891                        $ret[count($ret)] = $elem;
892                    }
893                }
894                //appending elements to a DOMDocumentFragment is a fast way to move them around
895                elseif ($ret instanceof DOMDocumentFragment)
896                {
897                    foreach ($new as $elem)
898                    {
899                        //appendChild, but don't forget to verify same document
900                        $ret->appendChild(      !$ret->ownerDocument->isSameNode($elem->ownerDocument) 
901                                                                                        ? $ret->ownerDocument->importNode($elem, true) 
902                                                                                        : $elem);
903                    }
904                }
905                //otherwise we need to find a method to use to attach the elements
906                elseif (($m = method_exists($ret, 'push')) || method_exists($ret, 'add'))
907                {
908                    $method = $m ? 'push' : 'add';
909                   
910                    foreach ($new as $elem)
911                    {
912                        $ret->$method($elem);
913                    }
914                }
915                elseif (($m = method_exists($ret, 'concat')) || method_exists($ret, 'concatenate'))
916                {
917                    $method = $m ? 'concat' : 'concatenate';
918                   
919                    $ret->$method($new);
920                }
921            }
922            //this will save the selected elements into a string
923            elseif (is_string($ret))
924            {
925                foreach ($new as $elem)
926                {
927                    $ret .= $elem->ownerDocument->saveXML($elem);
928                }
929            }
930        }
931       
932        return $new;
933    }
934}
935
936/**
937 *    this creates the default filters array on the CSSQuery object
938 *
939 *    <code>
940 *        //prototype function (DOMNode $element, integer $i, array $matches, array $context, CSSQuery $cssQuery);
941 *        CSSQuery::$filters['myfilter'] = create_function('', '');
942 *       
943 *    </code>
944 */
945CSSQuery::$filters = new RecursiveArrayIterator(array(
946    //CSS3 selectors
947    'before'        => create_function('DOMNode $e',     'return false;'),
948                                                                       
949    'after'        => create_function('DOMNode $e',     'return false;'),
950                                                                       
951    'first-child'        => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return !$e->isSameNode($e->ownerDocument->documentElement) &&
952                                                                        $c->xpath->query("../*[position()=1]", $e)->item(0)->isSameNode($e);'),
953                                                                       
954    'last-child'         => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return !$e->isSameNode($e->ownerDocument->documentElement) &&
955                                                                        $c->xpath->query("../*[last()]", $e)->item(0)->isSameNode($e);'),
956                                                                       
957    'only-child'         => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return !$e->isSameNode($e->ownerDocument->documentElement) &&
958                                                                        $e->parentNode->getElementsByTagName("*")->length == 1;'),
959   
960    'checked'            => create_function('DOMNode $e',              'return strtolower($e->tagName) == "input" && $e->hasAttribute("checked");'),
961       
962    'disabled'           => create_function('DOMNode $e',              'return $e->hasAttribute("disabled") &&
963                                                                                stristr("|input|textarea|select|button|", "|".$e->tagName."|") !== false;'),
964                                                                                                                                                               
965    'enabled'            => create_function('DOMNode $e',              'return !$e->hasAttribute("disabled") &&
966                                                                        stristr("|input|textarea|select|button|", "|".$e->tagName . "|") !== false &&
967                                                                        (!$e->hasAttribute("type") || strtolower($e->getAttribute("type")) != "hidden");'),
968    //nth child selectors
969    "nth-child"           => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return CSSQuery::nthChild($e, "nth-child",            $m[3], $c->xpath);'),
970    "nth-last-child"      => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return CSSQuery::nthChild($e, "nth-last-child",       $m[3], $c->xpath);'),
971    "nth-of-type"         => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return CSSQuery::nthChild($e, "nth-of-type",          $m[3], $c->xpath);'),
972    "nth-last-of-type"    => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return CSSQuery::nthChild($e, "nth-last-of-type",     $m[3], $c->xpath);'),
973   
974    "first-of-type" => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', 'return call_user_func(CSSQuery::$filters["nth-of-type"],     $e, $i, array(0,1,1,1), $a, $c);'),
975    "last-of-type"  => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', 'return call_user_func(CSSQuery::$filters["nth-last-of-type"],$e, $i, array(0,1,1,1), $a, $c);'),
976    "only-of-type"  => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', 'return call_user_func(CSSQuery::$filters["first-of-type"],   $e, $i, $m,             $a, $c) &&
977                                                                                      call_user_func(CSSQuery::$filters["last-of-type"],    $e, $i, $m,             $a, $c);'),
978    //closest thing to the lang filter                                                               
979    "lang"                => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', 'return $c->xpath->evaluate(
980                                                                                                    sprintf(
981                                                                                                        "count(./ancestor-or-self::*[@lang and (@lang =".
982                                                                                                            " \"%s\" or substring(@lang, 1, %u)=\"%s-\")])",
983                                                                                                        $m[3],
984                                                                                                        strlen($m[3]) + 1,
985                                                                                                        $m[3]
986                                                                                                    ),
987                                                                                                    $e
988                                                                                                ) > 0;'),
989   
990    //negation filter
991    "not"                 => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',  'return CSSQuery::inArray($e, $c->query(trim($m[3]))) == -1;'),
992   
993    //element has no child nodes
994    "empty"               => create_function('DOMNode $e',                        'return !$e->hasChildNodes();'),
995   
996    //element has child nodes that are elements
997    "parent"              => create_function('DOMNode $e',                        'return ($n = $e->getElementsByTagName("*")) && $n->length > 0;'),
998   
999    //get the parent node of the current element
1000    "parent-node"         => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',  '//if there is no filter just return the first parentNode
1001                                                                                    if (!$m || !isset($m[3]) || !trim($m[3])) return $e->parentNode;
1002                                                                                    //otherwise if the filter is more than a tagName
1003                                                                                    return  preg_match("/^(\*|\w+)([^\w]+.+)/", trim($m[3]), $n)
1004                                                                                                                                                                                        ? CSSQuery::find(trim($n[2]), $c->xpath->query("./ancestor::{$n[1]}", $e))
1005                                                                                            //if the filter is only a tagName save the trouble
1006                                                                                            : $c->xpath->query(sprintf("./ancestor::%s", trim($m[3])), $e);'),
1007   
1008    //get the ancestors of the current element
1009    "parents"             => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', '$r = $c->xpath->query("./ancestor::*", $e);
1010                                                                                    return $m && isset($m[3]) && trim($m[3]) ? CSSQuery::find(trim($m[3]), $r) : $r;'),
1011   
1012    //the element has nextSiblings
1013    "next-sibling"       => create_function('DOMNode $e',                          'return ($n = $e->parentNode->getElementsByTagName("*"))
1014                                                                                                && !$n->item($n->length-1)->isSameNode($e);'),
1015   
1016    //the element has previousSiblings
1017    "previous-sibling"   => create_function('DOMNode $e',                          'return !$e->parentNode->getElementsByTagName("*")->item(0)->isSameNode($e);'),
1018   
1019    //get the previousSiblings of the current element
1020    "previous-siblings"  => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',  '$r = $c->xpath->query("./preceding-sibling::*", $e);
1021                                                                                    return $m && isset($m[3]) && trim($m[3]) ? CSSQuery::find(trim($m[3]), $r) : $r;'),
1022   
1023    //get the nextSiblings of the current element
1024    "next-siblings"      => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', '$r = $c->xpath->query("./following-sibling::*", $e);
1025                                                                                    return $m && isset($m[3]) && trim($m[3]) ? CSSQuery::find(trim($m[3]), $r) : $r;'),
1026   
1027    //get all the siblings of the current element
1028    "siblings"           => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c', '$r = $c->xpath->query("./preceding-sibling::* | ./following-sibling::*", $e);
1029                                                                                    return $m && isset($m[3]) && trim($m[3]) ? CSSQuery::find(trim($m[3]), $r) : $r;'),
1030   
1031    //select the header elements
1032    "header"             => create_function('DOMNode $e',                 'return (bool)preg_match("/^h[1-6]$/i", $e->tagName);'),
1033   
1034    //form element selectors
1035    "selected"           => create_function('DOMNode $e',                 'return $e->hasAttribute("selected");'),
1036   
1037    //any element that would be considered input based on tagName
1038    "input"              => create_function('DOMNode $e',                 'return stristr("|input|textarea|select|button|", "|" . $e->tagName . "|") !== false;'),
1039    //any input element and type
1040    "radio"              => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "radio");'),
1041    "checkbox"           => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "checkbox");'),
1042    "file"               => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "file");'),
1043    "password"           => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "password");'),
1044    "submit"             => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "submit");'),
1045    "image"              => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "image");'),
1046    "reset"              => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "reset");'),
1047    "button"             => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "button");'),
1048    "text"               => create_function('DOMNode $e',                 'return CSSQuery::inputFilter($e, "text");'),
1049   
1050    //limiting filter
1051    "has"                => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return count($c->query($m[3], $e)) > 0;'),
1052   
1053    //text limiting filter
1054    "contains"           => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return strstr($e->textContent,
1055                                                                                                    preg_replace("/^\s*([\'\"])(.*)\\\\1\s*$/", "\\\\2", $m[3]));'),
1056    "Contains"           => create_function('DOMNode $e,$i,$m,$a,CSSQuery $c',     'return stristr($e->textContent,
1057                                                                                                    preg_replace("/^\s*([\'\"])(.*)\\\\1\s*$/", "\\\\2", $m[3]));'),
1058   
1059    //positional selectors for the current node-set
1060    "first"              => create_function('DOMNode $e,$i',             'return $i === 0;'),
1061    "last"               => create_function('DOMNode $e,$i,$m,$a',     'return $i === (count($a) - 1);'),
1062    "lt"                 => create_function('DOMNode $e,$i,$m',         'return $i < $m[3];'),
1063    "gt"                 => create_function('DOMNode $e,$i,$m',         'return $i > $m[3];'),
1064    "eq"                 => create_function('DOMNode $e,$i,$m',         'return $i === intval($m[3]);'),
1065   
1066    //works like nth-child on the currently selected node-set
1067    "nth"                => create_function('DOMNode $e,$i,$m',         '$expr = preg_replace("/\s+/", "", strtolower(trim($m[3])));
1068                                                                       
1069                                                                            //these selectors select all so dont waste time figuring them out
1070                                                                            if ($expr == "n" || $expr == "n+0" || $expr == "1n+0" || $expr == "1n")
1071                                                                            {
1072                                                                                return true;
1073                                                                            }
1074                                                                            //even numbered elements
1075                                                                            elseif ($expr == "even" || $expr == "2n" || $expr == "2n+0")
1076                                                                            {
1077                                                                                return $i % 2 == 0;
1078                                                                            }
1079                                                                            //odd numbered elements
1080                                                                            elseif ($expr == "odd" || $expr == "2n+1")
1081                                                                            {
1082                                                                                return $i % 2 != 0;
1083                                                                            }
1084                                                                            //positional - a negative position is not supported
1085                                                                            elseif (preg_match("/^([\+\-]?\d+)$/i", $expr, $mat))
1086                                                                            {
1087                                                                                return $i == intval($mat[1]);
1088                                                                            }
1089                                                                            //grouped according to a position
1090                                                                            elseif (preg_match("/^([\+\-]?\d*)?n([\+\-]\d+)?/i", $expr, $mat))
1091                                                                            {
1092                                                                                $a = (isset($mat[2]) && $mat[2] ? intval($mat[2]) : 0);
1093                                                                                $b = (isset($mat[2]) && $mat[2] ? intval($mat[2]) : 1);
1094                                                                                return ($a == 0 && $i == $b) ||
1095                                                                                        ($a > 0 && $i >= $b && ($i - $b) % $a == 0) ||
1096                                                                                        ($a < 0 && $i <= $b && (($b - $i) % ($a * -1)) == 0);
1097                                                                            }
1098                                                                       
1099                                                                            return false;
1100                                                                '),
1101), 2);
1102
1103/**
1104 *    create a default array of attribute filters
1105 *
1106 *    <code>
1107 *        //prototype function (DOMNode $element, string $attributeName, string $value = '', array $matches, array $context=array());
1108 *        CSSQuery::$attributeFilters['>'] = create_function('', '');
1109 *       
1110 *    </code>
1111 */
1112CSSQuery::$attributeFilters = new RecursiveArrayIterator(array(
1113    //hasAttribute and/or attribute == value
1114    ""      => create_function('$e,$a,$v=null',   'return $e->hasAttribute($a);'),
1115    //hasAttribute and/or attribute == value
1116    "="     => create_function('$e,$a,$v=null',   'return $e->hasAttribute($a) && $e->getAttribute($a) == $v;'),
1117    //!hasAttribute or attribute != value
1118    "!="    => create_function('$e,$a,$v',        'return !$e->hasAttribute($a) || $e->getAttribute($a) != $v;'),
1119    //hasAttribute and the attribute begins with value
1120    "^="    => create_function('$e,$a,$v',        'return $e->hasAttribute($a) && substr($e->getAttribute($a), 0, strlen($v)) == $v;'),
1121    //hasAttribute and the attribute ends with value
1122    '$='    => create_function('$e,$a,$v',        'return $e->hasAttribute($a) && substr($e->getAttribute($a), -strlen($v)) == $v;'),
1123    //hasAttribute and the attribute begins with value . -
1124    "|="    => create_function('$e,$a,$v',        'return $e->hasAttribute($a) && substr($e->getAttribute($a), 0, strlen($v) + 1) == $v."-";'),
1125    //hasAttribute and attribute contains value
1126    "*="    => create_function('$e,$a,$v',        'return $e->hasAttribute($a) && strstr($e->getAttribute($a), $v);'),
1127   
1128    //special
1129    //hasAttribute and attribute contains value - case insensitive
1130    "%="    => create_function('$e,$a,$v',        'return $e->hasAttribute($a) && stristr($e->getAttribute($a), $v);'),
1131    //hasAttribute and the attrributes value matches the given PCRE pattern
1132    "@="    => create_function('$e,$a,$v',        'return $e->hasAttribute($a) && preg_match($v, $e->getAttribute($a));'),
1133), 2);
Note: See TracBrowser for help on using the repository browser.