xref: /dokuwiki/_test/core/phpQuery-onefile.php (revision 257ea06e660866808a0725781fbc884c38bb0222)
1<?php
2/**
3 * phpQuery is a server-side, chainable, CSS3 selector driven
4 * Document Object Model (DOM) API based on jQuery JavaScript Library.
5 *
6 * @version 0.9.5
7 * @link http://code.google.com/p/phpquery/
8 * @link http://phpquery-library.blogspot.com/
9 * @link http://jquery.com/
10 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
11 * @license http://www.opensource.org/licenses/mit-license.php MIT License
12 * @package phpQuery
13 */
14
15// class names for instanceof
16// TODO move them as class constants into phpQuery
17define('DOMDOCUMENT', 'DOMDocument');
18define('DOMELEMENT', 'DOMElement');
19define('DOMNODELIST', 'DOMNodeList');
20define('DOMNODE', 'DOMNode');
21
22/**
23 * DOMEvent class.
24 *
25 * Based on
26 * @link http://developer.mozilla.org/En/DOM:event
27 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
28 * @package phpQuery
29 * @todo implement ArrayAccess ?
30 */
31class DOMEvent {
32    /**
33     * Returns a boolean indicating whether the event bubbles up through the DOM or not.
34     *
35     * @var unknown_type
36     */
37    public $bubbles = true;
38    /**
39     * Returns a boolean indicating whether the event is cancelable.
40     *
41     * @var unknown_type
42     */
43    public $cancelable = true;
44    /**
45     * Returns a reference to the currently registered target for the event.
46     *
47     * @var unknown_type
48     */
49    public $currentTarget;
50    /**
51     * Returns detail about the event, depending on the type of event.
52     *
53     * @var unknown_type
54     * @link http://developer.mozilla.org/en/DOM/event.detail
55     */
56    public $detail; // ???
57    /**
58     * Used to indicate which phase of the event flow is currently being evaluated.
59     *
60     * NOT IMPLEMENTED
61     *
62     * @var unknown_type
63     * @link http://developer.mozilla.org/en/DOM/event.eventPhase
64     */
65    public $eventPhase; // ???
66    /**
67     * The explicit original target of the event (Mozilla-specific).
68     *
69     * NOT IMPLEMENTED
70     *
71     * @var unknown_type
72     */
73    public $explicitOriginalTarget; // moz only
74    /**
75     * The original target of the event, before any retargetings (Mozilla-specific).
76     *
77     * NOT IMPLEMENTED
78     *
79     * @var unknown_type
80     */
81    public $originalTarget; // moz only
82    /**
83     * Identifies a secondary target for the event.
84     *
85     * @var unknown_type
86     */
87    public $relatedTarget;
88    /**
89     * Returns a reference to the target to which the event was originally dispatched.
90     *
91     * @var unknown_type
92     */
93    public $target;
94    /**
95     * Returns the time that the event was created.
96     *
97     * @var unknown_type
98     */
99    public $timeStamp;
100    /**
101     * Returns the name of the event (case-insensitive).
102     */
103    public $type;
104    public $runDefault = true;
105    public $data = null;
106    public function __construct($data) {
107        foreach($data as $k => $v) {
108            $this->$k = $v;
109        }
110        if (! $this->timeStamp)
111            $this->timeStamp = time();
112    }
113    /**
114     * Cancels the event (if it is cancelable).
115     *
116     */
117    public function preventDefault() {
118        $this->runDefault = false;
119    }
120    /**
121     * Stops the propagation of events further along in the DOM.
122     *
123     */
124    public function stopPropagation() {
125        $this->bubbles = false;
126    }
127}
128
129
130/**
131 * DOMDocumentWrapper class simplifies work with DOMDocument.
132 *
133 * Know bug:
134 * - in XHTML fragments, <br /> changes to <br clear="none" />
135 *
136 * @todo check XML catalogs compatibility
137 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
138 * @package phpQuery
139 */
140class DOMDocumentWrapper {
141    /**
142     * @var DOMDocument
143     */
144    public $document;
145    public $id;
146    /**
147     * @todo Rewrite as method and quess if null.
148     * @var unknown_type
149     */
150    public $contentType = '';
151    public $xpath;
152    public $uuid = 0;
153    public $data = array();
154    public $dataNodes = array();
155    public $events = array();
156    public $eventsNodes = array();
157    public $eventsGlobal = array();
158    /**
159     * @TODO iframes support http://code.google.com/p/phpquery/issues/detail?id=28
160     * @var unknown_type
161     */
162    public $frames = array();
163    /**
164     * Document root, by default equals to document itself.
165     * Used by documentFragments.
166     *
167     * @var DOMNode
168     */
169    public $root;
170    public $isDocumentFragment;
171    public $isXML = false;
172    public $isXHTML = false;
173    public $isHTML = false;
174    public $charset;
175    public function __construct($markup = null, $contentType = null, $newDocumentID = null) {
176        if (isset($markup))
177            $this->load($markup, $contentType, $newDocumentID);
178        $this->id = $newDocumentID
179            ? $newDocumentID
180            : md5(microtime());
181    }
182    public function load($markup, $contentType = null, $newDocumentID = null) {
183//      phpQuery::$documents[$id] = $this;
184        $this->contentType = strtolower($contentType);
185        if ($markup instanceof DOMDOCUMENT) {
186            $this->document = $markup;
187            $this->root = $this->document;
188            $this->charset = $this->document->encoding;
189            // TODO isDocumentFragment
190        } else {
191            $loaded = $this->loadMarkup($markup);
192        }
193        if ($loaded) {
194//          $this->document->formatOutput = true;
195            $this->document->preserveWhiteSpace = true;
196            $this->xpath = new DOMXPath($this->document);
197            $this->afterMarkupLoad();
198            return true;
199            // remember last loaded document
200//          return phpQuery::selectDocument($id);
201        }
202        return false;
203    }
204    protected function afterMarkupLoad() {
205        if ($this->isXHTML) {
206            $this->xpath->registerNamespace("html", "http://www.w3.org/1999/xhtml");
207        }
208    }
209    protected function loadMarkup($markup) {
210        $loaded = false;
211        if ($this->contentType) {
212            self::debug("Load markup for content type {$this->contentType}");
213            // content determined by contentType
214            list($contentType, $charset) = $this->contentTypeToArray($this->contentType);
215            switch($contentType) {
216                case 'text/html':
217                    phpQuery::debug("Loading HTML, content type '{$this->contentType}'");
218                    $loaded = $this->loadMarkupHTML($markup, $charset);
219                break;
220                case 'text/xml':
221                case 'application/xhtml+xml':
222                    phpQuery::debug("Loading XML, content type '{$this->contentType}'");
223                    $loaded = $this->loadMarkupXML($markup, $charset);
224                break;
225                default:
226                    // for feeds or anything that sometimes doesn't use text/xml
227                    if (strpos('xml', $this->contentType) !== false) {
228                        phpQuery::debug("Loading XML, content type '{$this->contentType}'");
229                        $loaded = $this->loadMarkupXML($markup, $charset);
230                    } else
231                        phpQuery::debug("Could not determine document type from content type '{$this->contentType}'");
232            }
233        } else {
234            // content type autodetection
235            if ($this->isXML($markup)) {
236                phpQuery::debug("Loading XML, isXML() == true");
237                $loaded = $this->loadMarkupXML($markup);
238                if (! $loaded && $this->isXHTML) {
239                    phpQuery::debug('Loading as XML failed, trying to load as HTML, isXHTML == true');
240                    $loaded = $this->loadMarkupHTML($markup);
241                }
242            } else {
243                phpQuery::debug("Loading HTML, isXML() == false");
244                $loaded = $this->loadMarkupHTML($markup);
245            }
246        }
247        return $loaded;
248    }
249    protected function loadMarkupReset() {
250        $this->isXML = $this->isXHTML = $this->isHTML = false;
251    }
252    protected function documentCreate($charset, $version = '1.0') {
253        if (! $version)
254            $version = '1.0';
255        $this->document = new DOMDocument($version, $charset);
256        $this->charset = $this->document->encoding;
257//      $this->document->encoding = $charset;
258        $this->document->formatOutput = true;
259        $this->document->preserveWhiteSpace = true;
260    }
261    protected function loadMarkupHTML($markup, $requestedCharset = null) {
262        if (phpQuery::$debug)
263            phpQuery::debug('Full markup load (HTML): '.substr($markup, 0, 250));
264        $this->loadMarkupReset();
265        $this->isHTML = true;
266        if (!isset($this->isDocumentFragment))
267            $this->isDocumentFragment = self::isDocumentFragmentHTML($markup);
268        $charset = null;
269        $documentCharset = $this->charsetFromHTML($markup);
270        $addDocumentCharset = false;
271        if ($documentCharset) {
272            $charset = $documentCharset;
273            $markup = $this->charsetFixHTML($markup);
274        } else if ($requestedCharset) {
275            $charset = $requestedCharset;
276        }
277        if (! $charset)
278            $charset = phpQuery::$defaultCharset;
279        // HTTP 1.1 says that the default charset is ISO-8859-1
280        // @see http://www.w3.org/International/O-HTTP-charset
281        if (! $documentCharset) {
282            $documentCharset = 'ISO-8859-1';
283            $addDocumentCharset = true;
284        }
285        // Should be careful here, still need 'magic encoding detection' since lots of pages have other 'default encoding'
286        // Worse, some pages can have mixed encodings... we'll try not to worry about that
287        $requestedCharset = strtoupper($requestedCharset);
288        $documentCharset = strtoupper($documentCharset);
289        phpQuery::debug("DOC: $documentCharset REQ: $requestedCharset");
290        if ($requestedCharset && $documentCharset && $requestedCharset !== $documentCharset) {
291            phpQuery::debug("CHARSET CONVERT");
292            // Document Encoding Conversion
293            // http://code.google.com/p/phpquery/issues/detail?id=86
294            if (function_exists('mb_detect_encoding')) {
295                $possibleCharsets = array($documentCharset, $requestedCharset, 'AUTO');
296                $docEncoding = mb_detect_encoding($markup, implode(', ', $possibleCharsets));
297                if (! $docEncoding)
298                    $docEncoding = $documentCharset; // ok trust the document
299                phpQuery::debug("DETECTED '$docEncoding'");
300                // Detected does not match what document says...
301                if ($docEncoding !== $documentCharset) {
302                    // Tricky..
303                }
304                if ($docEncoding !== $requestedCharset) {
305                    phpQuery::debug("CONVERT $docEncoding => $requestedCharset");
306                    $markup = mb_convert_encoding($markup, $requestedCharset, $docEncoding);
307                    $markup = $this->charsetAppendToHTML($markup, $requestedCharset);
308                    $charset = $requestedCharset;
309                }
310            } else {
311                phpQuery::debug("TODO: charset conversion without mbstring...");
312            }
313        }
314        $return = false;
315        if ($this->isDocumentFragment) {
316            phpQuery::debug("Full markup load (HTML), DocumentFragment detected, using charset '$charset'");
317            $return = $this->documentFragmentLoadMarkup($this, $charset, $markup);
318        } else {
319            if ($addDocumentCharset) {
320                phpQuery::debug("Full markup load (HTML), appending charset: '$charset'");
321                $markup = $this->charsetAppendToHTML($markup, $charset);
322            }
323            phpQuery::debug("Full markup load (HTML), documentCreate('$charset')");
324            $this->documentCreate($charset);
325            $return = phpQuery::$debug === 2
326                ? $this->document->loadHTML($markup)
327                : @$this->document->loadHTML($markup);
328            if ($return)
329                $this->root = $this->document;
330        }
331        if ($return && ! $this->contentType)
332            $this->contentType = 'text/html';
333        return $return;
334    }
335    protected function loadMarkupXML($markup, $requestedCharset = null) {
336        if (phpQuery::$debug)
337            phpQuery::debug('Full markup load (XML): '.substr($markup, 0, 250));
338        $this->loadMarkupReset();
339        $this->isXML = true;
340        // check agains XHTML in contentType or markup
341        $isContentTypeXHTML = $this->isXHTML();
342        $isMarkupXHTML = $this->isXHTML($markup);
343        if ($isContentTypeXHTML || $isMarkupXHTML) {
344            self::debug('Full markup load (XML), XHTML detected');
345            $this->isXHTML = true;
346        }
347        // determine document fragment
348        if (! isset($this->isDocumentFragment))
349            $this->isDocumentFragment = $this->isXHTML
350                ? self::isDocumentFragmentXHTML($markup)
351                : self::isDocumentFragmentXML($markup);
352        // this charset will be used
353        $charset = null;
354        // charset from XML declaration @var string
355        $documentCharset = $this->charsetFromXML($markup);
356        if (! $documentCharset) {
357            if ($this->isXHTML) {
358                // this is XHTML, try to get charset from content-type meta header
359                $documentCharset = $this->charsetFromHTML($markup);
360                if ($documentCharset) {
361                    phpQuery::debug("Full markup load (XML), appending XHTML charset '$documentCharset'");
362                    $this->charsetAppendToXML($markup, $documentCharset);
363                    $charset = $documentCharset;
364                }
365            }
366            if (! $documentCharset) {
367                // if still no document charset...
368                $charset = $requestedCharset;
369            }
370        } else if ($requestedCharset) {
371            $charset = $requestedCharset;
372        }
373        if (! $charset) {
374            $charset = phpQuery::$defaultCharset;
375        }
376        if ($requestedCharset && $documentCharset && $requestedCharset != $documentCharset) {
377            // TODO place for charset conversion
378//          $charset = $requestedCharset;
379        }
380        $return = false;
381        if ($this->isDocumentFragment) {
382            phpQuery::debug("Full markup load (XML), DocumentFragment detected, using charset '$charset'");
383            $return = $this->documentFragmentLoadMarkup($this, $charset, $markup);
384        } else {
385            // FIXME ???
386            if ($isContentTypeXHTML && ! $isMarkupXHTML)
387            if (! $documentCharset) {
388                phpQuery::debug("Full markup load (XML), appending charset '$charset'");
389                $markup = $this->charsetAppendToXML($markup, $charset);
390            }
391            // see http://php.net/manual/en/book.dom.php#78929
392            // LIBXML_DTDLOAD (>= PHP 5.1)
393            // does XML ctalogues works with LIBXML_NONET
394    //      $this->document->resolveExternals = true;
395            // TODO test LIBXML_COMPACT for performance improvement
396            // create document
397            $this->documentCreate($charset);
398            if (phpversion() < 5.1) {
399                $this->document->resolveExternals = true;
400                $return = phpQuery::$debug === 2
401                    ? $this->document->loadXML($markup)
402                    : @$this->document->loadXML($markup);
403            } else {
404                /** @link http://php.net/manual/en/libxml.constants.php */
405                $libxmlStatic = phpQuery::$debug === 2
406                    ? LIBXML_DTDLOAD|LIBXML_DTDATTR|LIBXML_NONET
407                    : LIBXML_DTDLOAD|LIBXML_DTDATTR|LIBXML_NONET|LIBXML_NOWARNING|LIBXML_NOERROR;
408                $return = $this->document->loadXML($markup, $libxmlStatic);
409//              if (! $return)
410//                  $return = $this->document->loadHTML($markup);
411            }
412            if ($return)
413                $this->root = $this->document;
414        }
415        if ($return) {
416            if (! $this->contentType) {
417                if ($this->isXHTML)
418                    $this->contentType = 'application/xhtml+xml';
419                else
420                    $this->contentType = 'text/xml';
421            }
422            return $return;
423        } else {
424            throw new Exception("Error loading XML markup");
425        }
426    }
427    protected function isXHTML($markup = null) {
428        if (! isset($markup)) {
429            return strpos($this->contentType, 'xhtml') !== false;
430        }
431        // XXX ok ?
432        return strpos($markup, "<!DOCTYPE html") !== false;
433//      return stripos($doctype, 'xhtml') !== false;
434//      $doctype = isset($dom->doctype) && is_object($dom->doctype)
435//          ? $dom->doctype->publicId
436//          : self::$defaultDoctype;
437    }
438    protected function isXML($markup) {
439//      return strpos($markup, '<?xml') !== false && stripos($markup, 'xhtml') === false;
440        return strpos(substr($markup, 0, 100), '<'.'?xml') !== false;
441    }
442    protected function contentTypeToArray($contentType) {
443        $matches = explode(';', trim(strtolower($contentType)));
444        if (isset($matches[1])) {
445            $matches[1] = explode('=', $matches[1]);
446            // strip 'charset='
447            $matches[1] = isset($matches[1][1]) && trim($matches[1][1])
448                ? $matches[1][1]
449                : $matches[1][0];
450        } else
451            $matches[1] = null;
452        return $matches;
453    }
454    /**
455     *
456     * @param $markup
457     * @return array contentType, charset
458     */
459    protected function contentTypeFromHTML($markup) {
460        $matches = array();
461        // find meta tag
462        preg_match('@<meta[^>]+http-equiv\\s*=\\s*(["|\'])Content-Type\\1([^>]+?)>@i',
463            $markup, $matches
464        );
465        if (! isset($matches[0]))
466            return array(null, null);
467        // get attr 'content'
468        preg_match('@content\\s*=\\s*(["|\'])(.+?)\\1@', $matches[0], $matches);
469        if (! isset($matches[0]))
470            return array(null, null);
471        return $this->contentTypeToArray($matches[2]);
472    }
473    protected function charsetFromHTML($markup) {
474        $contentType = $this->contentTypeFromHTML($markup);
475        return $contentType[1];
476    }
477    protected function charsetFromXML($markup) {
478        $matches;
479        // find declaration
480        preg_match('@<'.'?xml[^>]+encoding\\s*=\\s*(["|\'])(.*?)\\1@i',
481            $markup, $matches
482        );
483        return isset($matches[2])
484            ? strtolower($matches[2])
485            : null;
486    }
487    /**
488     * Repositions meta[type=charset] at the start of head. Bypasses DOMDocument bug.
489     *
490     * @link http://code.google.com/p/phpquery/issues/detail?id=80
491     * @param $html
492     */
493    protected function charsetFixHTML($markup) {
494        $matches = array();
495        // find meta tag
496        preg_match('@\s*<meta[^>]+http-equiv\\s*=\\s*(["|\'])Content-Type\\1([^>]+?)>@i',
497            $markup, $matches, PREG_OFFSET_CAPTURE
498        );
499        if (! isset($matches[0]))
500            return;
501        $metaContentType = $matches[0][0];
502        $markup = substr($markup, 0, $matches[0][1])
503            .substr($markup, $matches[0][1]+strlen($metaContentType));
504        $headStart = stripos($markup, '<head>');
505        $markup = substr($markup, 0, $headStart+6).$metaContentType
506            .substr($markup, $headStart+6);
507        return $markup;
508    }
509    protected function charsetAppendToHTML($html, $charset, $xhtml = false) {
510        // remove existing meta[type=content-type]
511        $html = preg_replace('@\s*<meta[^>]+http-equiv\\s*=\\s*(["|\'])Content-Type\\1([^>]+?)>@i', '', $html);
512        $meta = '<meta http-equiv="Content-Type" content="text/html;charset='
513            .$charset.'" '
514            .($xhtml ? '/' : '')
515            .'>';
516        if (strpos($html, '<head') === false) {
517            if (strpos($hltml, '<html') === false) {
518                return $meta.$html;
519            } else {
520                return preg_replace(
521                    '@<html(.*?)(?(?<!\?)>)@s',
522                    "<html\\1><head>{$meta}</head>",
523                    $html
524                );
525            }
526        } else {
527            return preg_replace(
528                '@<head(.*?)(?(?<!\?)>)@s',
529                '<head\\1>'.$meta,
530                $html
531            );
532        }
533    }
534    protected function charsetAppendToXML($markup, $charset) {
535        $declaration = '<'.'?xml version="1.0" encoding="'.$charset.'"?'.'>';
536        return $declaration.$markup;
537    }
538    public static function isDocumentFragmentHTML($markup) {
539        return stripos($markup, '<html') === false && stripos($markup, '<!doctype') === false;
540    }
541    public static function isDocumentFragmentXML($markup) {
542        return stripos($markup, '<'.'?xml') === false;
543    }
544    public static function isDocumentFragmentXHTML($markup) {
545        return self::isDocumentFragmentHTML($markup);
546    }
547    public function importAttr($value) {
548        // TODO
549    }
550    /**
551     *
552     * @param $source
553     * @param $target
554     * @param $sourceCharset
555     * @return array Array of imported nodes.
556     */
557    public function import($source, $sourceCharset = null) {
558        // TODO charset conversions
559        $return = array();
560        if ($source instanceof DOMNODE && !($source instanceof DOMNODELIST))
561            $source = array($source);
562//      if (is_array($source)) {
563//          foreach($source as $node) {
564//              if (is_string($node)) {
565//                  // string markup
566//                  $fake = $this->documentFragmentCreate($node, $sourceCharset);
567//                  if ($fake === false)
568//                      throw new Exception("Error loading documentFragment markup");
569//                  else
570//                      $return = array_merge($return,
571//                          $this->import($fake->root->childNodes)
572//                      );
573//              } else {
574//                  $return[] = $this->document->importNode($node, true);
575//              }
576//          }
577//          return $return;
578//      } else {
579//          // string markup
580//          $fake = $this->documentFragmentCreate($source, $sourceCharset);
581//          if ($fake === false)
582//              throw new Exception("Error loading documentFragment markup");
583//          else
584//              return $this->import($fake->root->childNodes);
585//      }
586        if (is_array($source) || $source instanceof DOMNODELIST) {
587            // dom nodes
588            self::debug('Importing nodes to document');
589            foreach($source as $node)
590                $return[] = $this->document->importNode($node, true);
591        } else {
592            // string markup
593            $fake = $this->documentFragmentCreate($source, $sourceCharset);
594            if ($fake === false)
595                throw new Exception("Error loading documentFragment markup");
596            else
597                return $this->import($fake->root->childNodes);
598        }
599        return $return;
600    }
601    /**
602     * Creates new document fragment.
603     *
604     * @param $source
605     * @return DOMDocumentWrapper
606     */
607    protected function documentFragmentCreate($source, $charset = null) {
608        $fake = new DOMDocumentWrapper();
609        $fake->contentType = $this->contentType;
610        $fake->isXML = $this->isXML;
611        $fake->isHTML = $this->isHTML;
612        $fake->isXHTML = $this->isXHTML;
613        $fake->root = $fake->document;
614        if (! $charset)
615            $charset = $this->charset;
616//  $fake->documentCreate($this->charset);
617        if ($source instanceof DOMNODE && !($source instanceof DOMNODELIST))
618            $source = array($source);
619        if (is_array($source) || $source instanceof DOMNODELIST) {
620            // dom nodes
621            // load fake document
622            if (! $this->documentFragmentLoadMarkup($fake, $charset))
623                return false;
624            $nodes = $fake->import($source);
625            foreach($nodes as $node)
626                $fake->root->appendChild($node);
627        } else {
628            // string markup
629            $this->documentFragmentLoadMarkup($fake, $charset, $source);
630        }
631        return $fake;
632    }
633    /**
634     *
635     * @param $document DOMDocumentWrapper
636     * @param $markup
637     * @return $document
638     */
639    private function documentFragmentLoadMarkup($fragment, $charset, $markup = null) {
640        // TODO error handling
641        // TODO copy doctype
642        // tempolary turn off
643        $fragment->isDocumentFragment = false;
644        if ($fragment->isXML) {
645            if ($fragment->isXHTML) {
646                // add FAKE element to set default namespace
647                $fragment->loadMarkupXML('<?xml version="1.0" encoding="'.$charset.'"?>'
648                    .'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
649                    .'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
650                    .'<fake xmlns="http://www.w3.org/1999/xhtml">'.$markup.'</fake>');
651                $fragment->root = $fragment->document->firstChild->nextSibling;
652            } else {
653                $fragment->loadMarkupXML('<?xml version="1.0" encoding="'.$charset.'"?><fake>'.$markup.'</fake>');
654                $fragment->root = $fragment->document->firstChild;
655            }
656        } else {
657            $markup2 = phpQuery::$defaultDoctype.'<html><head><meta http-equiv="Content-Type" content="text/html;charset='
658                .$charset.'"></head>';
659            $noBody = strpos($markup, '<body') === false;
660            if ($noBody)
661                $markup2 .= '<body>';
662            $markup2 .= $markup;
663            if ($noBody)
664                $markup2 .= '</body>';
665            $markup2 .= '</html>';
666            $fragment->loadMarkupHTML($markup2);
667            // TODO resolv body tag merging issue
668            $fragment->root = $noBody
669                ? $fragment->document->firstChild->nextSibling->firstChild->nextSibling
670                : $fragment->document->firstChild->nextSibling->firstChild->nextSibling;
671        }
672        if (! $fragment->root)
673            return false;
674        $fragment->isDocumentFragment = true;
675        return true;
676    }
677    protected function documentFragmentToMarkup($fragment) {
678        phpQuery::debug('documentFragmentToMarkup');
679        $tmp = $fragment->isDocumentFragment;
680        $fragment->isDocumentFragment = false;
681        $markup = $fragment->markup();
682        if ($fragment->isXML) {
683            $markup = substr($markup, 0, strrpos($markup, '</fake>'));
684            if ($fragment->isXHTML) {
685                $markup = substr($markup, strpos($markup, '<fake')+43);
686            } else {
687                $markup = substr($markup, strpos($markup, '<fake>')+6);
688            }
689        } else {
690                $markup = substr($markup, strpos($markup, '<body>')+6);
691                $markup = substr($markup, 0, strrpos($markup, '</body>'));
692        }
693        $fragment->isDocumentFragment = $tmp;
694        if (phpQuery::$debug)
695            phpQuery::debug('documentFragmentToMarkup: '.substr($markup, 0, 150));
696        return $markup;
697    }
698    /**
699     * Return document markup, starting with optional $nodes as root.
700     *
701     * @param $nodes    DOMNode|DOMNodeList
702     * @return string
703     */
704    public function markup($nodes = null, $innerMarkup = false) {
705        if (isset($nodes) && count($nodes) == 1 && $nodes[0] instanceof DOMDOCUMENT)
706            $nodes = null;
707        if (isset($nodes)) {
708            $markup = '';
709            if (!is_array($nodes) && !($nodes instanceof DOMNODELIST) )
710                $nodes = array($nodes);
711            if ($this->isDocumentFragment && ! $innerMarkup)
712                foreach($nodes as $i => $node)
713                    if ($node->isSameNode($this->root)) {
714                    //  var_dump($node);
715                        $nodes = array_slice($nodes, 0, $i)
716                            + phpQuery::DOMNodeListToArray($node->childNodes)
717                            + array_slice($nodes, $i+1);
718                        }
719            if ($this->isXML && ! $innerMarkup) {
720                self::debug("Getting outerXML with charset '{$this->charset}'");
721                // we need outerXML, so we can benefit from
722                // $node param support in saveXML()
723                foreach($nodes as $node)
724                    $markup .= $this->document->saveXML($node);
725            } else {
726                $loop = array();
727                if ($innerMarkup)
728                    foreach($nodes as $node) {
729                        if ($node->childNodes)
730                            foreach($node->childNodes as $child)
731                                $loop[] = $child;
732                        else
733                            $loop[] = $node;
734                    }
735                else
736                    $loop = $nodes;
737                self::debug("Getting markup, moving selected nodes (".count($loop).") to new DocumentFragment");
738                $fake = $this->documentFragmentCreate($loop);
739                $markup = $this->documentFragmentToMarkup($fake);
740            }
741            if ($this->isXHTML) {
742                self::debug("Fixing XHTML");
743                $markup = self::markupFixXHTML($markup);
744            }
745            self::debug("Markup: ".substr($markup, 0, 250));
746            return $markup;
747        } else {
748            if ($this->isDocumentFragment) {
749                // documentFragment, html only...
750                self::debug("Getting markup, DocumentFragment detected");
751//              return $this->markup(
752////                    $this->document->getElementsByTagName('body')->item(0)
753//                  $this->document->root, true
754//              );
755                $markup = $this->documentFragmentToMarkup($this);
756                // no need for markupFixXHTML, as it's done thought markup($nodes) method
757                return $markup;
758            } else {
759                self::debug("Getting markup (".($this->isXML?'XML':'HTML')."), final with charset '{$this->charset}'");
760                $markup = $this->isXML
761                    ? $this->document->saveXML()
762                    : $this->document->saveHTML();
763                if ($this->isXHTML) {
764                    self::debug("Fixing XHTML");
765                    $markup = self::markupFixXHTML($markup);
766                }
767                self::debug("Markup: ".substr($markup, 0, 250));
768                return $markup;
769            }
770        }
771    }
772    protected static function markupFixXHTML($markup) {
773        $markup = self::expandEmptyTag('script', $markup);
774        $markup = self::expandEmptyTag('select', $markup);
775        $markup = self::expandEmptyTag('textarea', $markup);
776        return $markup;
777    }
778    public static function debug($text) {
779        phpQuery::debug($text);
780    }
781    /**
782     * expandEmptyTag
783     *
784     * @param $tag
785     * @param $xml
786     * @return unknown_type
787     * @author mjaque at ilkebenson dot com
788     * @link http://php.net/manual/en/domdocument.savehtml.php#81256
789     */
790    public static function expandEmptyTag($tag, $xml){
791        $indice = 0;
792        while ($indice< strlen($xml)){
793            $pos = strpos($xml, "<$tag ", $indice);
794            if ($pos){
795                $posCierre = strpos($xml, ">", $pos);
796                if ($xml[$posCierre-1] == "/"){
797                    $xml = substr_replace($xml, "></$tag>", $posCierre-1, 2);
798                }
799                $indice = $posCierre;
800            }
801            else break;
802        }
803        return $xml;
804    }
805}
806
807/**
808 * Event handling class.
809 *
810 * @author Tobiasz Cudnik
811 * @package phpQuery
812 * @static
813 */
814abstract class phpQueryEvents {
815    /**
816     * Trigger a type of event on every matched element.
817     *
818     * @param DOMNode|phpQueryObject|string $document
819     * @param unknown_type $type
820     * @param unknown_type $data
821     *
822     * @TODO exclusive events (with !)
823     * @TODO global events (test)
824     * @TODO support more than event in $type (space-separated)
825     */
826    public static function trigger($document, $type, $data = array(), $node = null) {
827        // trigger: function(type, data, elem, donative, extra) {
828        $documentID = phpQuery::getDocumentID($document);
829        $namespace = null;
830        if (strpos($type, '.') !== false)
831            list($name, $namespace) = explode('.', $type);
832        else
833            $name = $type;
834        if (! $node) {
835            if (self::issetGlobal($documentID, $type)) {
836                $pq = phpQuery::getDocument($documentID);
837                // TODO check add($pq->document)
838                $pq->find('*')->add($pq->document)
839                    ->trigger($type, $data);
840            }
841        } else {
842            if (isset($data[0]) && $data[0] instanceof DOMEvent) {
843                $event = $data[0];
844                $event->relatedTarget = $event->target;
845                $event->target = $node;
846                $data = array_slice($data, 1);
847            } else {
848                $event = new DOMEvent(array(
849                    'type' => $type,
850                    'target' => $node,
851                    'timeStamp' => time(),
852                ));
853            }
854            $i = 0;
855            while($node) {
856                // TODO whois
857                phpQuery::debug("Triggering ".($i?"bubbled ":'')."event '{$type}' on "
858                    ."node \n");//.phpQueryObject::whois($node)."\n");
859                $event->currentTarget = $node;
860                $eventNode = self::getNode($documentID, $node);
861                if (isset($eventNode->eventHandlers)) {
862                    foreach($eventNode->eventHandlers as $eventType => $handlers) {
863                        $eventNamespace = null;
864                        if (strpos($type, '.') !== false)
865                            list($eventName, $eventNamespace) = explode('.', $eventType);
866                        else
867                            $eventName = $eventType;
868                        if ($name != $eventName)
869                            continue;
870                        if ($namespace && $eventNamespace && $namespace != $eventNamespace)
871                            continue;
872                        foreach($handlers as $handler) {
873                            phpQuery::debug("Calling event handler\n");
874                            $event->data = $handler['data']
875                                ? $handler['data']
876                                : null;
877                            $params = array_merge(array($event), $data);
878                            $return = phpQuery::callbackRun($handler['callback'], $params);
879                            if ($return === false) {
880                                $event->bubbles = false;
881                            }
882                        }
883                    }
884                }
885                // to bubble or not to bubble...
886                if (! $event->bubbles)
887                    break;
888                $node = $node->parentNode;
889                $i++;
890            }
891        }
892    }
893    /**
894     * Binds a handler to one or more events (like click) for each matched element.
895     * Can also bind custom events.
896     *
897     * @param DOMNode|phpQueryObject|string $document
898     * @param unknown_type $type
899     * @param unknown_type $data Optional
900     * @param unknown_type $callback
901     *
902     * @TODO support '!' (exclusive) events
903     * @TODO support more than event in $type (space-separated)
904     * @TODO support binding to global events
905     */
906    public static function add($document, $node, $type, $data, $callback = null) {
907        phpQuery::debug("Binding '$type' event");
908        $documentID = phpQuery::getDocumentID($document);
909//      if (is_null($callback) && is_callable($data)) {
910//          $callback = $data;
911//          $data = null;
912//      }
913        $eventNode = self::getNode($documentID, $node);
914        if (! $eventNode)
915            $eventNode = self::setNode($documentID, $node);
916        if (!isset($eventNode->eventHandlers[$type]))
917            $eventNode->eventHandlers[$type] = array();
918        $eventNode->eventHandlers[$type][] = array(
919            'callback' => $callback,
920            'data' => $data,
921        );
922    }
923    /**
924     * Enter description here...
925     *
926     * @param DOMNode|phpQueryObject|string $document
927     * @param unknown_type $type
928     * @param unknown_type $callback
929     *
930     * @TODO namespace events
931     * @TODO support more than event in $type (space-separated)
932     */
933    public static function remove($document, $node, $type = null, $callback = null) {
934        $documentID = phpQuery::getDocumentID($document);
935        $eventNode = self::getNode($documentID, $node);
936        if (is_object($eventNode) && isset($eventNode->eventHandlers[$type])) {
937            if ($callback) {
938                foreach($eventNode->eventHandlers[$type] as $k => $handler)
939                    if ($handler['callback'] == $callback)
940                        unset($eventNode->eventHandlers[$type][$k]);
941            } else {
942                unset($eventNode->eventHandlers[$type]);
943            }
944        }
945    }
946    protected static function getNode($documentID, $node) {
947        foreach(phpQuery::$documents[$documentID]->eventsNodes as $eventNode) {
948            if ($node->isSameNode($eventNode))
949                return $eventNode;
950        }
951    }
952    protected static function setNode($documentID, $node) {
953        phpQuery::$documents[$documentID]->eventsNodes[] = $node;
954        return phpQuery::$documents[$documentID]->eventsNodes[
955            count(phpQuery::$documents[$documentID]->eventsNodes)-1
956        ];
957    }
958    protected static function issetGlobal($documentID, $type) {
959        return isset(phpQuery::$documents[$documentID])
960            ? in_array($type, phpQuery::$documents[$documentID]->eventsGlobal)
961            : false;
962    }
963}
964
965
966interface ICallbackNamed {
967    function hasName();
968    function getName();
969}
970/**
971 * Callback class introduces currying-like pattern.
972 *
973 * Example:
974 * function foo($param1, $param2, $param3) {
975 *   var_dump($param1, $param2, $param3);
976 * }
977 * $fooCurried = new Callback('foo',
978 *   'param1 is now statically set',
979 *   new CallbackParam, new CallbackParam
980 * );
981 * phpQuery::callbackRun($fooCurried,
982 *  array('param2 value', 'param3 value'
983 * );
984 *
985 * Callback class is supported in all phpQuery methods which accepts callbacks.
986 *
987 * @link http://code.google.com/p/phpquery/wiki/Callbacks#Param_Structures
988 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
989 *
990 * @TODO??? return fake forwarding function created via create_function
991 * @TODO honor paramStructure
992 */
993class Callback
994    implements ICallbackNamed {
995    public $callback = null;
996    public $params = null;
997    protected $name;
998    public function __construct($callback, $param1 = null, $param2 = null,
999            $param3 = null) {
1000        $params = func_get_args();
1001        $params = array_slice($params, 1);
1002        if ($callback instanceof Callback) {
1003            // TODO implement recurention
1004        } else {
1005            $this->callback = $callback;
1006            $this->params = $params;
1007        }
1008    }
1009    public function getName() {
1010        return 'Callback: '.$this->name;
1011    }
1012    public function hasName() {
1013        return isset($this->name) && $this->name;
1014    }
1015    public function setName($name) {
1016        $this->name = $name;
1017        return $this;
1018    }
1019    // TODO test me
1020//  public function addParams() {
1021//      $params = func_get_args();
1022//      return new Callback($this->callback, $this->params+$params);
1023//  }
1024}
1025
1026/**
1027 * Callback type which on execution returns reference passed during creation.
1028 *
1029 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1030 */
1031class CallbackReturnReference extends Callback
1032    implements ICallbackNamed {
1033    protected $reference;
1034    public function __construct(&$reference, $name = null){
1035        $this->reference =& $reference;
1036        $this->callback = array($this, 'callback');
1037    }
1038    public function callback() {
1039        return $this->reference;
1040    }
1041    public function getName() {
1042        return 'Callback: '.$this->name;
1043    }
1044    public function hasName() {
1045        return isset($this->name) && $this->name;
1046    }
1047}
1048/**
1049 * Callback type which on execution returns value passed during creation.
1050 *
1051 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1052 */
1053class CallbackReturnValue extends Callback
1054    implements ICallbackNamed {
1055    protected $value;
1056    protected $name;
1057    public function __construct($value, $name = null){
1058        $this->value =& $value;
1059        $this->name = $name;
1060        $this->callback = array($this, 'callback');
1061    }
1062    public function callback() {
1063        return $this->value;
1064    }
1065    public function __toString() {
1066        return $this->getName();
1067    }
1068    public function getName() {
1069        return 'Callback: '.$this->name;
1070    }
1071    public function hasName() {
1072        return isset($this->name) && $this->name;
1073    }
1074}
1075/**
1076 * CallbackParameterToReference can be used when we don't really want a callback,
1077 * only parameter passed to it. CallbackParameterToReference takes first
1078 * parameter's value and passes it to reference.
1079 *
1080 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1081 */
1082class CallbackParameterToReference extends Callback {
1083    /**
1084     * @param $reference
1085     * @TODO implement $paramIndex;
1086     * param index choose which callback param will be passed to reference
1087     */
1088    public function __construct(&$reference){
1089        $this->callback =& $reference;
1090    }
1091}
1092//class CallbackReference extends Callback {
1093//  /**
1094//   *
1095//   * @param $reference
1096//   * @param $paramIndex
1097//   * @todo implement $paramIndex; param index choose which callback param will be passed to reference
1098//   */
1099//  public function __construct(&$reference, $name = null){
1100//      $this->callback =& $reference;
1101//  }
1102//}
1103class CallbackParam {}
1104
1105/**
1106 * Class representing phpQuery objects.
1107 *
1108 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1109 * @package phpQuery
1110 * @method phpQueryObject clone() clone()
1111 * @method phpQueryObject empty() empty()
1112 * @method phpQueryObject next() next($selector = null)
1113 * @method phpQueryObject prev() prev($selector = null)
1114 * @property Int $length
1115 */
1116class phpQueryObject
1117    implements Iterator, Countable, ArrayAccess {
1118    public $documentID = null;
1119    /**
1120     * DOMDocument class.
1121     *
1122     * @var DOMDocument
1123     */
1124    public $document = null;
1125    public $charset = null;
1126    /**
1127     *
1128     * @var DOMDocumentWrapper
1129     */
1130    public $documentWrapper = null;
1131    /**
1132     * XPath interface.
1133     *
1134     * @var DOMXPath
1135     */
1136    public $xpath = null;
1137    /**
1138     * Stack of selected elements.
1139     * @TODO refactor to ->nodes
1140     * @var array
1141     */
1142    public $elements = array();
1143    /**
1144     * @access private
1145     */
1146    protected $elementsBackup = array();
1147    /**
1148     * @access private
1149     */
1150    protected $previous = null;
1151    /**
1152     * @access private
1153     * @TODO deprecate
1154     */
1155    protected $root = array();
1156    /**
1157     * Indicated if doument is just a fragment (no <html> tag).
1158     *
1159     * Every document is realy a full document, so even documentFragments can
1160     * be queried against <html>, but getDocument(id)->htmlOuter() will return
1161     * only contents of <body>.
1162     *
1163     * @var bool
1164     */
1165    public $documentFragment = true;
1166    /**
1167     * Iterator interface helper
1168     * @access private
1169     */
1170    protected $elementsInterator = array();
1171    /**
1172     * Iterator interface helper
1173     * @access private
1174     */
1175    protected $valid = false;
1176    /**
1177     * Iterator interface helper
1178     * @access private
1179     */
1180    protected $current = null;
1181    /**
1182     * Enter description here...
1183     *
1184     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1185     */
1186    public function __construct($documentID) {
1187//      if ($documentID instanceof self)
1188//          var_dump($documentID->getDocumentID());
1189        $id = $documentID instanceof self
1190            ? $documentID->getDocumentID()
1191            : $documentID;
1192//      var_dump($id);
1193        if (! isset(phpQuery::$documents[$id] )) {
1194//          var_dump(phpQuery::$documents);
1195            throw new Exception("Document with ID '{$id}' isn't loaded. Use phpQuery::newDocument(\$html) or phpQuery::newDocumentFile(\$file) first.");
1196        }
1197        $this->documentID = $id;
1198        $this->documentWrapper =& phpQuery::$documents[$id];
1199        $this->document =& $this->documentWrapper->document;
1200        $this->xpath =& $this->documentWrapper->xpath;
1201        $this->charset =& $this->documentWrapper->charset;
1202        $this->documentFragment =& $this->documentWrapper->isDocumentFragment;
1203        // TODO check $this->DOM->documentElement;
1204//      $this->root = $this->document->documentElement;
1205        $this->root =& $this->documentWrapper->root;
1206//      $this->toRoot();
1207        $this->elements = array($this->root);
1208    }
1209    /**
1210     *
1211     * @access private
1212     * @param $attr
1213     * @return unknown_type
1214     */
1215    public function __get($attr) {
1216        switch($attr) {
1217            // FIXME doesnt work at all ?
1218            case 'length':
1219                return $this->size();
1220            break;
1221            default:
1222                return $this->$attr;
1223        }
1224    }
1225    /**
1226     * Saves actual object to $var by reference.
1227     * Useful when need to break chain.
1228     * @param phpQueryObject $var
1229     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1230     */
1231    public function toReference(&$var) {
1232        return $var = $this;
1233    }
1234    public function documentFragment($state = null) {
1235        if ($state) {
1236            phpQuery::$documents[$this->getDocumentID()]['documentFragment'] = $state;
1237            return $this;
1238        }
1239        return $this->documentFragment;
1240    }
1241    /**
1242   * @access private
1243   * @TODO documentWrapper
1244     */
1245    protected function isRoot( $node) {
1246//      return $node instanceof DOMDOCUMENT || $node->tagName == 'html';
1247        return $node instanceof DOMDOCUMENT
1248            || ($node instanceof DOMELEMENT && $node->tagName == 'html')
1249            || $this->root->isSameNode($node);
1250    }
1251    /**
1252   * @access private
1253     */
1254    protected function stackIsRoot() {
1255        return $this->size() == 1 && $this->isRoot($this->elements[0]);
1256    }
1257    /**
1258     * Enter description here...
1259     * NON JQUERY METHOD
1260     *
1261     * Watch out, it doesn't creates new instance, can be reverted with end().
1262     *
1263     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1264     */
1265    public function toRoot() {
1266        $this->elements = array($this->root);
1267        return $this;
1268//      return $this->newInstance(array($this->root));
1269    }
1270    /**
1271     * Saves object's DocumentID to $var by reference.
1272     * <code>
1273     * $myDocumentId;
1274     * phpQuery::newDocument('<div/>')
1275     *     ->getDocumentIDRef($myDocumentId)
1276     *     ->find('div')->...
1277     * </code>
1278     *
1279     * @param unknown_type $domId
1280     * @see phpQuery::newDocument
1281     * @see phpQuery::newDocumentFile
1282     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1283     */
1284    public function getDocumentIDRef(&$documentID) {
1285        $documentID = $this->getDocumentID();
1286        return $this;
1287    }
1288    /**
1289     * Returns object with stack set to document root.
1290     *
1291     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1292     */
1293    public function getDocument() {
1294        return phpQuery::getDocument($this->getDocumentID());
1295    }
1296    /**
1297     *
1298     * @return DOMDocument
1299     */
1300    public function getDOMDocument() {
1301        return $this->document;
1302    }
1303    /**
1304     * Get object's Document ID.
1305     *
1306     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1307     */
1308    public function getDocumentID() {
1309        return $this->documentID;
1310    }
1311    /**
1312     * Unloads whole document from memory.
1313     * CAUTION! None further operations will be possible on this document.
1314     * All objects refering to it will be useless.
1315     *
1316     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1317     */
1318    public function unloadDocument() {
1319        phpQuery::unloadDocuments($this->getDocumentID());
1320    }
1321    public function isHTML() {
1322        return $this->documentWrapper->isHTML;
1323    }
1324    public function isXHTML() {
1325        return $this->documentWrapper->isXHTML;
1326    }
1327    public function isXML() {
1328        return $this->documentWrapper->isXML;
1329    }
1330    /**
1331     * Enter description here...
1332     *
1333     * @link http://docs.jquery.com/Ajax/serialize
1334     * @return string
1335     */
1336    public function serialize() {
1337        return phpQuery::param($this->serializeArray());
1338    }
1339    /**
1340     * Enter description here...
1341     *
1342     * @link http://docs.jquery.com/Ajax/serializeArray
1343     * @return array
1344     */
1345    public function serializeArray($submit = null) {
1346        $source = $this->filter('form, input, select, textarea')
1347            ->find('input, select, textarea')
1348            ->andSelf()
1349            ->not('form');
1350        $return = array();
1351//      $source->dumpDie();
1352        foreach($source as $input) {
1353            $input = phpQuery::pq($input);
1354            if ($input->is('[disabled]'))
1355                continue;
1356            if (!$input->is('[name]'))
1357                continue;
1358            if ($input->is('[type=checkbox]') && !$input->is('[checked]'))
1359                continue;
1360            // jquery diff
1361            if ($submit && $input->is('[type=submit]')) {
1362                if ($submit instanceof DOMELEMENT && ! $input->elements[0]->isSameNode($submit))
1363                    continue;
1364                else if (is_string($submit) && $input->attr('name') != $submit)
1365                    continue;
1366            }
1367            $return[] = array(
1368                'name' => $input->attr('name'),
1369                'value' => $input->val(),
1370            );
1371        }
1372        return $return;
1373    }
1374    /**
1375     * @access private
1376     */
1377    protected function debug($in) {
1378        if (! phpQuery::$debug )
1379            return;
1380        print('<pre>');
1381        print_r($in);
1382        // file debug
1383//      file_put_contents(dirname(__FILE__).'/phpQuery.log', print_r($in, true)."\n", FILE_APPEND);
1384        // quite handy debug trace
1385//      if ( is_array($in))
1386//          print_r(array_slice(debug_backtrace(), 3));
1387        print("</pre>\n");
1388    }
1389    /**
1390     * @access private
1391     */
1392    protected function isRegexp($pattern) {
1393        return in_array(
1394            $pattern[ mb_strlen($pattern)-1 ],
1395            array('^','*','$')
1396        );
1397    }
1398    /**
1399     * Determines if $char is really a char.
1400     *
1401     * @param string $char
1402     * @return bool
1403     * @todo rewrite me to charcode range ! ;)
1404     * @access private
1405     */
1406    protected function isChar($char) {
1407        return extension_loaded('mbstring') && phpQuery::$mbstringSupport
1408            ? mb_eregi('\w', $char)
1409            : preg_match('@\w@', $char);
1410    }
1411    /**
1412     * @access private
1413     */
1414    protected function parseSelector($query) {
1415        // clean spaces
1416        // TODO include this inside parsing ?
1417        $query = trim(
1418            preg_replace('@\s+@', ' ',
1419                preg_replace('@\s*(>|\\+|~)\s*@', '\\1', $query)
1420            )
1421        );
1422        $queries = array(array());
1423        if (! $query)
1424            return $queries;
1425        $return =& $queries[0];
1426        $specialChars = array('>',' ');
1427//      $specialCharsMapping = array('/' => '>');
1428        $specialCharsMapping = array();
1429        $strlen = mb_strlen($query);
1430        $classChars = array('.', '-');
1431        $pseudoChars = array('-');
1432        $tagChars = array('*', '|', '-');
1433        // split multibyte string
1434        // http://code.google.com/p/phpquery/issues/detail?id=76
1435        $_query = array();
1436        for ($i=0; $i<$strlen; $i++)
1437            $_query[] = mb_substr($query, $i, 1);
1438        $query = $_query;
1439        // it works, but i dont like it...
1440        $i = 0;
1441        while( $i < $strlen) {
1442            $c = $query[$i];
1443            $tmp = '';
1444            // TAG
1445            if ($this->isChar($c) || in_array($c, $tagChars)) {
1446                while(isset($query[$i])
1447                    && ($this->isChar($query[$i]) || in_array($query[$i], $tagChars))) {
1448                    $tmp .= $query[$i];
1449                    $i++;
1450                }
1451                $return[] = $tmp;
1452            // IDs
1453            } else if ( $c == '#') {
1454                $i++;
1455                while( isset($query[$i]) && ($this->isChar($query[$i]) || $query[$i] == '-')) {
1456                    $tmp .= $query[$i];
1457                    $i++;
1458                }
1459                $return[] = '#'.$tmp;
1460            // SPECIAL CHARS
1461            } else if (in_array($c, $specialChars)) {
1462                $return[] = $c;
1463                $i++;
1464            // MAPPED SPECIAL MULTICHARS
1465//          } else if ( $c.$query[$i+1] == '//') {
1466//              $return[] = ' ';
1467//              $i = $i+2;
1468            // MAPPED SPECIAL CHARS
1469            } else if ( isset($specialCharsMapping[$c])) {
1470                $return[] = $specialCharsMapping[$c];
1471                $i++;
1472            // COMMA
1473            } else if ( $c == ',') {
1474                $queries[] = array();
1475                $return =& $queries[ count($queries)-1 ];
1476                $i++;
1477                while( isset($query[$i]) && $query[$i] == ' ')
1478                    $i++;
1479            // CLASSES
1480            } else if ($c == '.') {
1481                while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $classChars))) {
1482                    $tmp .= $query[$i];
1483                    $i++;
1484                }
1485                $return[] = $tmp;
1486            // ~ General Sibling Selector
1487            } else if ($c == '~') {
1488                $spaceAllowed = true;
1489                $tmp .= $query[$i++];
1490                while( isset($query[$i])
1491                    && ($this->isChar($query[$i])
1492                        || in_array($query[$i], $classChars)
1493                        || $query[$i] == '*'
1494                        || ($query[$i] == ' ' && $spaceAllowed)
1495                    )) {
1496                    if ($query[$i] != ' ')
1497                        $spaceAllowed = false;
1498                    $tmp .= $query[$i];
1499                    $i++;
1500                }
1501                $return[] = $tmp;
1502            // + Adjacent sibling selectors
1503            } else if ($c == '+') {
1504                $spaceAllowed = true;
1505                $tmp .= $query[$i++];
1506                while( isset($query[$i])
1507                    && ($this->isChar($query[$i])
1508                        || in_array($query[$i], $classChars)
1509                        || $query[$i] == '*'
1510                        || ($spaceAllowed && $query[$i] == ' ')
1511                    )) {
1512                    if ($query[$i] != ' ')
1513                        $spaceAllowed = false;
1514                    $tmp .= $query[$i];
1515                    $i++;
1516                }
1517                $return[] = $tmp;
1518            // ATTRS
1519            } else if ($c == '[') {
1520                $stack = 1;
1521                $tmp .= $c;
1522                while( isset($query[++$i])) {
1523                    $tmp .= $query[$i];
1524                    if ( $query[$i] == '[') {
1525                        $stack++;
1526                    } else if ( $query[$i] == ']') {
1527                        $stack--;
1528                        if (! $stack )
1529                            break;
1530                    }
1531                }
1532                $return[] = $tmp;
1533                $i++;
1534            // PSEUDO CLASSES
1535            } else if ($c == ':') {
1536                $stack = 1;
1537                $tmp .= $query[$i++];
1538                while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $pseudoChars))) {
1539                    $tmp .= $query[$i];
1540                    $i++;
1541                }
1542                // with arguments ?
1543                if ( isset($query[$i]) && $query[$i] == '(') {
1544                    $tmp .= $query[$i];
1545                    $stack = 1;
1546                    while( isset($query[++$i])) {
1547                        $tmp .= $query[$i];
1548                        if ( $query[$i] == '(') {
1549                            $stack++;
1550                        } else if ( $query[$i] == ')') {
1551                            $stack--;
1552                            if (! $stack )
1553                                break;
1554                        }
1555                    }
1556                    $return[] = $tmp;
1557                    $i++;
1558                } else {
1559                    $return[] = $tmp;
1560                }
1561            } else {
1562                $i++;
1563            }
1564        }
1565        foreach($queries as $k => $q) {
1566            if (isset($q[0])) {
1567                if (isset($q[0][0]) && $q[0][0] == ':')
1568                    array_unshift($queries[$k], '*');
1569                if ($q[0] != '>')
1570                    array_unshift($queries[$k], ' ');
1571            }
1572        }
1573        return $queries;
1574    }
1575    /**
1576     * Return matched DOM nodes.
1577     *
1578     * @param int $index
1579     * @return array|DOMElement Single DOMElement or array of DOMElement.
1580     */
1581    public function get($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1582        $return = isset($index)
1583            ? (isset($this->elements[$index]) ? $this->elements[$index] : null)
1584            : $this->elements;
1585        // pass thou callbacks
1586        $args = func_get_args();
1587        $args = array_slice($args, 1);
1588        foreach($args as $callback) {
1589            if (is_array($return))
1590                foreach($return as $k => $v)
1591                    $return[$k] = phpQuery::callbackRun($callback, array($v));
1592            else
1593                $return = phpQuery::callbackRun($callback, array($return));
1594        }
1595        return $return;
1596    }
1597    /**
1598     * Return matched DOM nodes.
1599     * jQuery difference.
1600     *
1601     * @param int $index
1602     * @return array|string Returns string if $index != null
1603     * @todo implement callbacks
1604     * @todo return only arrays ?
1605     * @todo maybe other name...
1606     */
1607    public function getString($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1608        if ($index)
1609            $return = $this->eq($index)->text();
1610        else {
1611            $return = array();
1612            for($i = 0; $i < $this->size(); $i++) {
1613                $return[] = $this->eq($i)->text();
1614            }
1615        }
1616        // pass thou callbacks
1617        $args = func_get_args();
1618        $args = array_slice($args, 1);
1619        foreach($args as $callback) {
1620            $return = phpQuery::callbackRun($callback, array($return));
1621        }
1622        return $return;
1623    }
1624    /**
1625     * Return matched DOM nodes.
1626     * jQuery difference.
1627     *
1628     * @param int $index
1629     * @return array|string Returns string if $index != null
1630     * @todo implement callbacks
1631     * @todo return only arrays ?
1632     * @todo maybe other name...
1633     */
1634    public function getStrings($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1635        if ($index)
1636            $return = $this->eq($index)->text();
1637        else {
1638            $return = array();
1639            for($i = 0; $i < $this->size(); $i++) {
1640                $return[] = $this->eq($i)->text();
1641            }
1642            // pass thou callbacks
1643            $args = func_get_args();
1644            $args = array_slice($args, 1);
1645        }
1646        foreach($args as $callback) {
1647            if (is_array($return))
1648                foreach($return as $k => $v)
1649                    $return[$k] = phpQuery::callbackRun($callback, array($v));
1650            else
1651                $return = phpQuery::callbackRun($callback, array($return));
1652        }
1653        return $return;
1654    }
1655    /**
1656     * Returns new instance of actual class.
1657     *
1658     * @param array $newStack Optional. Will replace old stack with new and move old one to history.c
1659     */
1660    public function newInstance($newStack = null) {
1661        $class = get_class($this);
1662        // support inheritance by passing old object to overloaded constructor
1663        $new = $class != 'phpQuery'
1664            ? new $class($this, $this->getDocumentID())
1665            : new phpQueryObject($this->getDocumentID());
1666        $new->previous = $this;
1667        if (is_null($newStack)) {
1668            $new->elements = $this->elements;
1669            if ($this->elementsBackup)
1670                $this->elements = $this->elementsBackup;
1671        } else if (is_string($newStack)) {
1672            $new->elements = phpQuery::pq($newStack, $this->getDocumentID())->stack();
1673        } else {
1674            $new->elements = $newStack;
1675        }
1676        return $new;
1677    }
1678    /**
1679     * Enter description here...
1680     *
1681     * In the future, when PHP will support XLS 2.0, then we would do that this way:
1682     * contains(tokenize(@class, '\s'), "something")
1683     * @param unknown_type $class
1684     * @param unknown_type $node
1685     * @return boolean
1686     * @access private
1687     */
1688    protected function matchClasses($class, $node) {
1689        // multi-class
1690        if ( mb_strpos($class, '.', 1)) {
1691            $classes = explode('.', substr($class, 1));
1692            $classesCount = count( $classes );
1693            $nodeClasses = explode(' ', $node->getAttribute('class') );
1694            $nodeClassesCount = count( $nodeClasses );
1695            if ( $classesCount > $nodeClassesCount )
1696                return false;
1697            $diff = count(
1698                array_diff(
1699                    $classes,
1700                    $nodeClasses
1701                )
1702            );
1703            if (! $diff )
1704                return true;
1705        // single-class
1706        } else {
1707            return in_array(
1708                // strip leading dot from class name
1709                substr($class, 1),
1710                // get classes for element as array
1711                explode(' ', $node->getAttribute('class') )
1712            );
1713        }
1714    }
1715    /**
1716     * @access private
1717     */
1718    protected function runQuery($XQuery, $selector = null, $compare = null) {
1719        if ($compare && ! method_exists($this, $compare))
1720            return false;
1721        $stack = array();
1722        if (! $this->elements)
1723            $this->debug('Stack empty, skipping...');
1724//      var_dump($this->elements[0]->nodeType);
1725        // element, document
1726        foreach($this->stack(array(1, 9, 13)) as $k => $stackNode) {
1727            $detachAfter = false;
1728            // to work on detached nodes we need temporary place them somewhere
1729            // thats because context xpath queries sucks ;]
1730            $testNode = $stackNode;
1731            while ($testNode) {
1732                if (! $testNode->parentNode && ! $this->isRoot($testNode)) {
1733                    $this->root->appendChild($testNode);
1734                    $detachAfter = $testNode;
1735                    break;
1736                }
1737                $testNode = isset($testNode->parentNode)
1738                    ? $testNode->parentNode
1739                    : null;
1740            }
1741            // XXX tmp ?
1742            $xpath = $this->documentWrapper->isXHTML
1743                ? $this->getNodeXpath($stackNode, 'html')
1744                : $this->getNodeXpath($stackNode);
1745            // FIXME pseudoclasses-only query, support XML
1746            $query = $XQuery == '//' && $xpath == '/html[1]'
1747                ? '//*'
1748                : $xpath.$XQuery;
1749            $this->debug("XPATH: {$query}");
1750            // run query, get elements
1751            $nodes = $this->xpath->query($query);
1752            $this->debug("QUERY FETCHED");
1753            if (! $nodes->length )
1754                $this->debug('Nothing found');
1755            $debug = array();
1756            foreach($nodes as $node) {
1757                $matched = false;
1758                if ( $compare) {
1759                    phpQuery::$debug ?
1760                        $this->debug("Found: ".$this->whois( $node ).", comparing with {$compare}()")
1761                        : null;
1762                    $phpQueryDebug = phpQuery::$debug;
1763                    phpQuery::$debug = false;
1764                    // TODO ??? use phpQuery::callbackRun()
1765                    if (call_user_func_array(array($this, $compare), array($selector, $node)))
1766                        $matched = true;
1767                    phpQuery::$debug = $phpQueryDebug;
1768                } else {
1769                    $matched = true;
1770                }
1771                if ( $matched) {
1772                    if (phpQuery::$debug)
1773                        $debug[] = $this->whois( $node );
1774                    $stack[] = $node;
1775                }
1776            }
1777            if (phpQuery::$debug) {
1778                $this->debug("Matched ".count($debug).": ".implode(', ', $debug));
1779            }
1780            if ($detachAfter)
1781                $this->root->removeChild($detachAfter);
1782        }
1783        $this->elements = $stack;
1784    }
1785    /**
1786     * Enter description here...
1787     *
1788     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1789     */
1790    public function find($selectors, $context = null, $noHistory = false) {
1791        if (!$noHistory)
1792            // backup last stack /for end()/
1793            $this->elementsBackup = $this->elements;
1794        // allow to define context
1795        // TODO combine code below with phpQuery::pq() context guessing code
1796        //   as generic function
1797        if ($context) {
1798            if (! is_array($context) && $context instanceof DOMELEMENT)
1799                $this->elements = array($context);
1800            else if (is_array($context)) {
1801                $this->elements = array();
1802                foreach ($context as $c)
1803                    if ($c instanceof DOMELEMENT)
1804                        $this->elements[] = $c;
1805            } else if ( $context instanceof self )
1806                $this->elements = $context->elements;
1807        }
1808        $queries = $this->parseSelector($selectors);
1809        $this->debug(array('FIND', $selectors, $queries));
1810        $XQuery = '';
1811        // remember stack state because of multi-queries
1812        $oldStack = $this->elements;
1813        // here we will be keeping found elements
1814        $stack = array();
1815        foreach($queries as $selector) {
1816            $this->elements = $oldStack;
1817            $delimiterBefore = false;
1818            foreach($selector as $s) {
1819                // TAG
1820                $isTag = extension_loaded('mbstring') && phpQuery::$mbstringSupport
1821                    ? mb_ereg_match('^[\w|\||-]+$', $s) || $s == '*'
1822                    : preg_match('@^[\w|\||-]+$@', $s) || $s == '*';
1823                if ($isTag) {
1824                    if ($this->isXML()) {
1825                        // namespace support
1826                        if (mb_strpos($s, '|') !== false) {
1827                            $ns = $tag = null;
1828                            list($ns, $tag) = explode('|', $s);
1829                            $XQuery .= "$ns:$tag";
1830                        } else if ($s == '*') {
1831                            $XQuery .= "*";
1832                        } else {
1833                            $XQuery .= "*[local-name()='$s']";
1834                        }
1835                    } else {
1836                        $XQuery .= $s;
1837                    }
1838                // ID
1839                } else if ($s[0] == '#') {
1840                    if ($delimiterBefore)
1841                        $XQuery .= '*';
1842                    $XQuery .= "[@id='".substr($s, 1)."']";
1843                // ATTRIBUTES
1844                } else if ($s[0] == '[') {
1845                    if ($delimiterBefore)
1846                        $XQuery .= '*';
1847                    // strip side brackets
1848                    $attr = trim($s, '][');
1849                    $execute = false;
1850                    // attr with specifed value
1851                    if (mb_strpos($s, '=')) {
1852                        $value = null;
1853                        list($attr, $value) = explode('=', $attr);
1854                        $value = trim($value, "'\"");
1855                        if ($this->isRegexp($attr)) {
1856                            // cut regexp character
1857                            $attr = substr($attr, 0, -1);
1858                            $execute = true;
1859                            $XQuery .= "[@{$attr}]";
1860                        } else {
1861                            $XQuery .= "[@{$attr}='{$value}']";
1862                        }
1863                    // attr without specified value
1864                    } else {
1865                        $XQuery .= "[@{$attr}]";
1866                    }
1867                    if ($execute) {
1868                        $this->runQuery($XQuery, $s, 'is');
1869                        $XQuery = '';
1870                        if (! $this->length())
1871                            break;
1872                    }
1873                // CLASSES
1874                } else if ($s[0] == '.') {
1875                    // TODO use return $this->find("./self::*[contains(concat(\" \",@class,\" \"), \" $class \")]");
1876                    // thx wizDom ;)
1877                    if ($delimiterBefore)
1878                        $XQuery .= '*';
1879                    $XQuery .= '[@class]';
1880                    $this->runQuery($XQuery, $s, 'matchClasses');
1881                    $XQuery = '';
1882                    if (! $this->length() )
1883                        break;
1884                // ~ General Sibling Selector
1885                } else if ($s[0] == '~') {
1886                    $this->runQuery($XQuery);
1887                    $XQuery = '';
1888                    $this->elements = $this
1889                        ->siblings(
1890                            substr($s, 1)
1891                        )->elements;
1892                    if (! $this->length() )
1893                        break;
1894                // + Adjacent sibling selectors
1895                } else if ($s[0] == '+') {
1896                    // TODO /following-sibling::
1897                    $this->runQuery($XQuery);
1898                    $XQuery = '';
1899                    $subSelector = substr($s, 1);
1900                    $subElements = $this->elements;
1901                    $this->elements = array();
1902                    foreach($subElements as $node) {
1903                        // search first DOMElement sibling
1904                        $test = $node->nextSibling;
1905                        while($test && ! ($test instanceof DOMELEMENT))
1906                            $test = $test->nextSibling;
1907                        if ($test && $this->is($subSelector, $test))
1908                            $this->elements[] = $test;
1909                    }
1910                    if (! $this->length() )
1911                        break;
1912                // PSEUDO CLASSES
1913                } else if ($s[0] == ':') {
1914                    // TODO optimization for :first :last
1915                    if ($XQuery) {
1916                        $this->runQuery($XQuery);
1917                        $XQuery = '';
1918                    }
1919                    if (! $this->length())
1920                        break;
1921                    $this->pseudoClasses($s);
1922                    if (! $this->length())
1923                        break;
1924                // DIRECT DESCENDANDS
1925                } else if ($s == '>') {
1926                    $XQuery .= '/';
1927                    $delimiterBefore = 2;
1928                // ALL DESCENDANDS
1929                } else if ($s == ' ') {
1930                    $XQuery .= '//';
1931                    $delimiterBefore = 2;
1932                // ERRORS
1933                } else {
1934                    phpQuery::debug("Unrecognized token '$s'");
1935                }
1936                $delimiterBefore = $delimiterBefore === 2;
1937            }
1938            // run query if any
1939            if ($XQuery && $XQuery != '//') {
1940                $this->runQuery($XQuery);
1941                $XQuery = '';
1942            }
1943            foreach($this->elements as $node)
1944                if (! $this->elementsContainsNode($node, $stack))
1945                    $stack[] = $node;
1946        }
1947        $this->elements = $stack;
1948        return $this->newInstance();
1949    }
1950    /**
1951     * @todo create API for classes with pseudoselectors
1952     * @access private
1953     */
1954    protected function pseudoClasses($class) {
1955        // TODO clean args parsing ?
1956        $class = ltrim($class, ':');
1957        $haveArgs = mb_strpos($class, '(');
1958        if ($haveArgs !== false) {
1959            $args = substr($class, $haveArgs+1, -1);
1960            $class = substr($class, 0, $haveArgs);
1961        }
1962        switch($class) {
1963            case 'even':
1964            case 'odd':
1965                $stack = array();
1966                foreach($this->elements as $i => $node) {
1967                    if ($class == 'even' && ($i%2) == 0)
1968                        $stack[] = $node;
1969                    else if ( $class == 'odd' && $i % 2 )
1970                        $stack[] = $node;
1971                }
1972                $this->elements = $stack;
1973                break;
1974            case 'eq':
1975                $k = intval($args);
1976                $this->elements = isset( $this->elements[$k] )
1977                    ? array( $this->elements[$k] )
1978                    : array();
1979                break;
1980            case 'gt':
1981                $this->elements = array_slice($this->elements, $args+1);
1982                break;
1983            case 'lt':
1984                $this->elements = array_slice($this->elements, 0, $args+1);
1985                break;
1986            case 'first':
1987                if (isset($this->elements[0]))
1988                    $this->elements = array($this->elements[0]);
1989                break;
1990            case 'last':
1991                if ($this->elements)
1992                    $this->elements = array($this->elements[count($this->elements)-1]);
1993                break;
1994            /*case 'parent':
1995                $stack = array();
1996                foreach($this->elements as $node) {
1997                    if ( $node->childNodes->length )
1998                        $stack[] = $node;
1999                }
2000                $this->elements = $stack;
2001                break;*/
2002            case 'contains':
2003                $text = trim($args, "\"'");
2004                $stack = array();
2005                foreach($this->elements as $node) {
2006                    if (mb_stripos($node->textContent, $text) === false)
2007                        continue;
2008                    $stack[] = $node;
2009                }
2010                $this->elements = $stack;
2011                break;
2012            case 'not':
2013                $selector = self::unQuote($args);
2014                $this->elements = $this->not($selector)->stack();
2015                break;
2016            case 'slice':
2017                // TODO jQuery difference ?
2018                $args = explode(',',
2019                    str_replace(', ', ',', trim($args, "\"'"))
2020                );
2021                $start = $args[0];
2022                $end = isset($args[1])
2023                    ? $args[1]
2024                    : null;
2025                if ($end > 0)
2026                    $end = $end-$start;
2027                $this->elements = array_slice($this->elements, $start, $end);
2028                break;
2029            case 'has':
2030                $selector = trim($args, "\"'");
2031                $stack = array();
2032                foreach($this->stack(1) as $el) {
2033                    if ($this->find($selector, $el, true)->length)
2034                        $stack[] = $el;
2035                }
2036                $this->elements = $stack;
2037                break;
2038            case 'submit':
2039            case 'reset':
2040                $this->elements = phpQuery::merge(
2041                    $this->map(array($this, 'is'),
2042                        "input[type=$class]", new CallbackParam()
2043                    ),
2044                    $this->map(array($this, 'is'),
2045                        "button[type=$class]", new CallbackParam()
2046                    )
2047                );
2048            break;
2049//              $stack = array();
2050//              foreach($this->elements as $node)
2051//                  if ($node->is('input[type=submit]') || $node->is('button[type=submit]'))
2052//                      $stack[] = $el;
2053//              $this->elements = $stack;
2054            case 'input':
2055                $this->elements = $this->map(
2056                    array($this, 'is'),
2057                    'input', new CallbackParam()
2058                )->elements;
2059            break;
2060            case 'password':
2061            case 'checkbox':
2062            case 'radio':
2063            case 'hidden':
2064            case 'image':
2065            case 'file':
2066                $this->elements = $this->map(
2067                    array($this, 'is'),
2068                    "input[type=$class]", new CallbackParam()
2069                )->elements;
2070            break;
2071            case 'parent':
2072                $this->elements = $this->map(
2073                    function ($node) {
2074                        return $node instanceof DOMELEMENT && $node->childNodes->length
2075                            ? $node : null;
2076                    }
2077                )->elements;
2078            break;
2079            case 'empty':
2080                $this->elements = $this->map(
2081                    function ($node) {
2082                        return $node instanceof DOMELEMENT && $node->childNodes->length
2083                            ? null : $node;
2084                    }
2085                )->elements;
2086            break;
2087            case 'disabled':
2088            case 'selected':
2089            case 'checked':
2090                $this->elements = $this->map(
2091                    array($this, 'is'),
2092                    "[$class]", new CallbackParam()
2093                )->elements;
2094            break;
2095            case 'enabled':
2096                $this->elements = $this->map(
2097                    function ($node) {
2098                        return pq($node)->not(":disabled") ? $node : null;
2099                    }
2100                )->elements;
2101            break;
2102            case 'header':
2103                $this->elements = $this->map(
2104                    function ($node) {
2105                        $isHeader = isset($node->tagName) && in_array($node->tagName, array(
2106                                "h1", "h2", "h3", "h4", "h5", "h6", "h7"
2107                            ));
2108                        return $isHeader
2109                            ? $node
2110                            : null;
2111                    }
2112                )->elements;
2113//              $this->elements = $this->map(
2114//                  create_function('$node', '$node = pq($node);
2115//                      return $node->is("h1")
2116//                          || $node->is("h2")
2117//                          || $node->is("h3")
2118//                          || $node->is("h4")
2119//                          || $node->is("h5")
2120//                          || $node->is("h6")
2121//                          || $node->is("h7")
2122//                          ? $node
2123//                          : null;')
2124//              )->elements;
2125            break;
2126            case 'only-child':
2127                $this->elements = $this->map(
2128                    function ($node) {
2129                        return pq($node)->siblings()->size() == 0 ? $node : null;
2130                    }
2131                )->elements;
2132            break;
2133            case 'first-child':
2134                $this->elements = $this->map(
2135                    function ($node) {
2136                        return pq($node)->prevAll()->size() == 0 ? $node : null;
2137                    }
2138                )->elements;
2139            break;
2140            case 'last-child':
2141                $this->elements = $this->map(
2142                    function ($node) {
2143                        return pq($node)->nextAll()->size() == 0 ? $node : null;
2144                    }
2145                )->elements;
2146            break;
2147            case 'nth-child':
2148                $param = trim($args, "\"'");
2149                if (! $param)
2150                    break;
2151                    // nth-child(n+b) to nth-child(1n+b)
2152                if ($param[0] == 'n')
2153                    $param = '1'.$param;
2154                // :nth-child(index/even/odd/equation)
2155                if ($param == 'even' || $param == 'odd')
2156                    $mapped = $this->map(
2157                        function ($node, $param) {
2158                            $index = pq($node)->prevAll()->size() + 1;
2159                            if ($param == "even" && ($index % 2) == 0)
2160                                return $node;
2161                            else if ($param == "odd" && $index % 2 == 1)
2162                                return $node;
2163                            else
2164                                return null;
2165                        }, new CallbackParam(), $param
2166                    );
2167                else if (mb_strlen($param) > 1 && $param[1] == 'n')
2168                    // an+b
2169                    $mapped = $this->map(
2170                        function ($node, $param) {
2171                            $prevs = pq($node)->prevAll()->size();
2172                            $index = 1 + $prevs;
2173                            $b = mb_strlen($param) > 3
2174                                ? $param[3]
2175                                : 0;
2176                            $a = $param[0];
2177                            if ($b && $param[2] == "-")
2178                                $b = -$b;
2179                            if ($a > 0) {
2180                                return ($index - $b) % $a == 0
2181                                    ? $node
2182                                    : null;
2183                                phpQuery::debug($a . "*" . floor($index / $a) . "+$b-1 == " . ($a * floor($index / $a) + $b - 1) . " ?= $prevs");
2184                                return $a * floor($index / $a) + $b - 1 == $prevs
2185                                    ? $node
2186                                    : null;
2187                            } else if ($a == 0) {
2188                                return $index == $b
2189                                    ? $node
2190                                    : null;
2191                            } else {
2192                                // negative value
2193                                return $index <= $b
2194                                    ? $node
2195                                    : null;
2196                            }
2197//                          if (! $b)
2198//                              return $index%$a == 0
2199//                                  ? $node
2200//                                  : null;
2201//                          else
2202//                              return ($index-$b)%$a == 0
2203//                                  ? $node
2204//                                  : null;
2205                        },
2206                        new CallbackParam(), $param
2207                    );
2208                else
2209                    // index
2210                    $mapped = $this->map(
2211                        function ($node, $index) {
2212                            $prevs = pq($node)->prevAll()->size();
2213                            if ($prevs && $prevs == $index - 1)
2214                                return $node;
2215                            else if (!$prevs && $index == 1)
2216                                return $node;
2217                            else
2218                                return null;
2219                        },
2220                        new CallbackParam(), $param
2221                    );
2222                $this->elements = $mapped->elements;
2223            break;
2224            default:
2225                $this->debug("Unknown pseudoclass '{$class}', skipping...");
2226        }
2227    }
2228    /**
2229     * @access private
2230     */
2231    protected function pseudoClassParam($paramsString) {
2232        // TODO;
2233    }
2234    /**
2235     * Enter description here...
2236     *
2237     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2238     */
2239    public function is($selector, $nodes = null) {
2240        phpQuery::debug(array("Is:", $selector));
2241        if (! $selector)
2242            return false;
2243        $oldStack = $this->elements;
2244        $returnArray = false;
2245        if ($nodes && is_array($nodes)) {
2246            $this->elements = $nodes;
2247        } else if ($nodes)
2248            $this->elements = array($nodes);
2249        $this->filter($selector, true);
2250        $stack = $this->elements;
2251        $this->elements = $oldStack;
2252        if ($nodes)
2253            return $stack ? $stack : null;
2254        return (bool)count($stack);
2255    }
2256    /**
2257     * Enter description here...
2258     * jQuery difference.
2259     *
2260     * Callback:
2261     * - $index int
2262     * - $node DOMNode
2263     *
2264     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2265     * @link http://docs.jquery.com/Traversing/filter
2266     */
2267    public function filterCallback($callback, $_skipHistory = false) {
2268        if (! $_skipHistory) {
2269            $this->elementsBackup = $this->elements;
2270            $this->debug("Filtering by callback");
2271        }
2272        $newStack = array();
2273        foreach($this->elements as $index => $node) {
2274            $result = phpQuery::callbackRun($callback, array($index, $node));
2275            if (is_null($result) || (! is_null($result) && $result))
2276                $newStack[] = $node;
2277        }
2278        $this->elements = $newStack;
2279        return $_skipHistory
2280            ? $this
2281            : $this->newInstance();
2282    }
2283    /**
2284     * Enter description here...
2285     *
2286     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2287     * @link http://docs.jquery.com/Traversing/filter
2288     */
2289    public function filter($selectors, $_skipHistory = false) {
2290        if ($selectors instanceof Callback OR $selectors instanceof Closure)
2291            return $this->filterCallback($selectors, $_skipHistory);
2292        if (! $_skipHistory)
2293            $this->elementsBackup = $this->elements;
2294        $notSimpleSelector = array(' ', '>', '~', '+', '/');
2295        if (! is_array($selectors))
2296            $selectors = $this->parseSelector($selectors);
2297        if (! $_skipHistory)
2298            $this->debug(array("Filtering:", $selectors));
2299        $finalStack = array();
2300        foreach($selectors as $selector) {
2301            $stack = array();
2302            if (! $selector)
2303                break;
2304            // avoid first space or /
2305            if (in_array($selector[0], $notSimpleSelector))
2306                $selector = array_slice($selector, 1);
2307            // PER NODE selector chunks
2308            foreach($this->stack() as $node) {
2309                $break = false;
2310                foreach($selector as $s) {
2311                    if (!($node instanceof DOMELEMENT)) {
2312                        // all besides DOMElement
2313                        if ( $s[0] == '[') {
2314                            $attr = trim($s, '[]');
2315                            if ( mb_strpos($attr, '=')) {
2316                                list( $attr, $val ) = explode('=', $attr);
2317                                if ($attr == 'nodeType' && $node->nodeType != $val)
2318                                    $break = true;
2319                            }
2320                        } else
2321                            $break = true;
2322                    } else {
2323                        // DOMElement only
2324                        // ID
2325                        if ( $s[0] == '#') {
2326                            if ( $node->getAttribute('id') != substr($s, 1) )
2327                                $break = true;
2328                        // CLASSES
2329                        } else if ( $s[0] == '.') {
2330                            if (! $this->matchClasses( $s, $node ) )
2331                                $break = true;
2332                        // ATTRS
2333                        } else if ( $s[0] == '[') {
2334                            // strip side brackets
2335                            $attr = trim($s, '[]');
2336                            if (mb_strpos($attr, '=')) {
2337                                list($attr, $val) = explode('=', $attr);
2338                                $val = self::unQuote($val);
2339                                if ($attr == 'nodeType') {
2340                                    if ($val != $node->nodeType)
2341                                        $break = true;
2342                                } else if ($this->isRegexp($attr)) {
2343                                    $val = extension_loaded('mbstring') && phpQuery::$mbstringSupport
2344                                        ? quotemeta(trim($val, '"\''))
2345                                        : preg_quote(trim($val, '"\''), '@');
2346                                    // switch last character
2347                                    switch( substr($attr, -1)) {
2348                                        // quotemeta used insted of preg_quote
2349                                        // http://code.google.com/p/phpquery/issues/detail?id=76
2350                                        case '^':
2351                                            $pattern = '^'.$val;
2352                                            break;
2353                                        case '*':
2354                                            $pattern = '.*'.$val.'.*';
2355                                            break;
2356                                        case '$':
2357                                            $pattern = '.*'.$val.'$';
2358                                            break;
2359                                    }
2360                                    // cut last character
2361                                    $attr = substr($attr, 0, -1);
2362                                    $isMatch = extension_loaded('mbstring') && phpQuery::$mbstringSupport
2363                                        ? mb_ereg_match($pattern, $node->getAttribute($attr))
2364                                        : preg_match("@{$pattern}@", $node->getAttribute($attr));
2365                                    if (! $isMatch)
2366                                        $break = true;
2367                                } else if ($node->getAttribute($attr) != $val)
2368                                    $break = true;
2369                            } else if (! $node->hasAttribute($attr))
2370                                $break = true;
2371                        // PSEUDO CLASSES
2372                        } else if ( $s[0] == ':') {
2373                            // skip
2374                        // TAG
2375                        } else if (trim($s)) {
2376                            if ($s != '*') {
2377                                // TODO namespaces
2378                                if (isset($node->tagName)) {
2379                                    if ($node->tagName != $s)
2380                                        $break = true;
2381                                } else if ($s == 'html' && ! $this->isRoot($node))
2382                                    $break = true;
2383                            }
2384                        // AVOID NON-SIMPLE SELECTORS
2385                        } else if (in_array($s, $notSimpleSelector)) {
2386                            $break = true;
2387                            $this->debug(array('Skipping non simple selector', $selector));
2388                        }
2389                    }
2390                    if ($break)
2391                        break;
2392                }
2393                // if element passed all chunks of selector - add it to new stack
2394                if (! $break )
2395                    $stack[] = $node;
2396            }
2397            $tmpStack = $this->elements;
2398            $this->elements = $stack;
2399            // PER ALL NODES selector chunks
2400            foreach($selector as $s)
2401                // PSEUDO CLASSES
2402                if ($s[0] == ':')
2403                    $this->pseudoClasses($s);
2404            foreach($this->elements as $node)
2405                // XXX it should be merged without duplicates
2406                // but jQuery doesnt do that
2407                $finalStack[] = $node;
2408            $this->elements = $tmpStack;
2409        }
2410        $this->elements = $finalStack;
2411        if ($_skipHistory) {
2412            return $this;
2413        } else {
2414            $this->debug("Stack length after filter(): ".count($finalStack));
2415            return $this->newInstance();
2416        }
2417    }
2418    /**
2419     *
2420     * @param $value
2421     * @return unknown_type
2422     * @TODO implement in all methods using passed parameters
2423     */
2424    protected static function unQuote($value) {
2425        return $value[0] == '\'' || $value[0] == '"'
2426            ? substr($value, 1, -1)
2427            : $value;
2428    }
2429    /**
2430     * Enter description here...
2431     *
2432     * @link http://docs.jquery.com/Ajax/load
2433     * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2434     * @todo Support $selector
2435     */
2436    public function load($url, $data = null, $callback = null) {
2437        if ($data && ! is_array($data)) {
2438            $callback = $data;
2439            $data = null;
2440        }
2441        if (mb_strpos($url, ' ') !== false) {
2442            $matches = null;
2443            if (extension_loaded('mbstring') && phpQuery::$mbstringSupport)
2444                mb_ereg('^([^ ]+) (.*)$', $url, $matches);
2445            else
2446                preg_match('^([^ ]+) (.*)$', $url, $matches);
2447            $url = $matches[1];
2448            $selector = $matches[2];
2449            // FIXME this sucks, pass as callback param
2450            $this->_loadSelector = $selector;
2451        }
2452        $ajax = array(
2453            'url' => $url,
2454            'type' => $data ? 'POST' : 'GET',
2455            'data' => $data,
2456            'complete' => $callback,
2457            'success' => array($this, 'loadSuccess')
2458        );
2459        phpQuery::ajax($ajax);
2460        return $this;
2461    }
2462    /**
2463     * @access private
2464     * @param $html
2465     * @return unknown_type
2466     */
2467    public function loadSuccess($html) {
2468        if ($this->_loadSelector) {
2469            $html = phpQuery::newDocument($html)->find($this->_loadSelector);
2470            unset($this->_loadSelector);
2471        }
2472        foreach($this->stack(1) as $node) {
2473            phpQuery::pq($node, $this->getDocumentID())
2474                ->markup($html);
2475        }
2476    }
2477    /**
2478     * Enter description here...
2479     *
2480     * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2481     * @todo
2482     */
2483    public function css() {
2484        // TODO
2485        return $this;
2486    }
2487    /**
2488     * @todo
2489     *
2490     */
2491    public function show(){
2492        // TODO
2493        return $this;
2494    }
2495    /**
2496     * @todo
2497     *
2498     */
2499    public function hide(){
2500        // TODO
2501        return $this;
2502    }
2503    /**
2504     * Trigger a type of event on every matched element.
2505     *
2506     * @param unknown_type $type
2507     * @param unknown_type $data
2508     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2509     * @TODO support more than event in $type (space-separated)
2510     */
2511    public function trigger($type, $data = array()) {
2512        foreach($this->elements as $node)
2513            phpQueryEvents::trigger($this->getDocumentID(), $type, $data, $node);
2514        return $this;
2515    }
2516    /**
2517     * This particular method triggers all bound event handlers on an element (for a specific event type) WITHOUT executing the browsers default actions.
2518     *
2519     * @param unknown_type $type
2520     * @param unknown_type $data
2521     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2522     * @TODO
2523     */
2524    public function triggerHandler($type, $data = array()) {
2525        // TODO;
2526    }
2527    /**
2528     * Binds a handler to one or more events (like click) for each matched element.
2529     * Can also bind custom events.
2530     *
2531     * @param unknown_type $type
2532     * @param unknown_type $data Optional
2533     * @param unknown_type $callback
2534     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2535     * @TODO support '!' (exclusive) events
2536     * @TODO support more than event in $type (space-separated)
2537     */
2538    public function bind($type, $data, $callback = null) {
2539        // TODO check if $data is callable, not using is_callable
2540        if (! isset($callback)) {
2541            $callback = $data;
2542            $data = null;
2543        }
2544        foreach($this->elements as $node)
2545            phpQueryEvents::add($this->getDocumentID(), $node, $type, $data, $callback);
2546        return $this;
2547    }
2548    /**
2549     * Enter description here...
2550     *
2551     * @param unknown_type $type
2552     * @param unknown_type $callback
2553     * @return unknown
2554     * @TODO namespace events
2555     * @TODO support more than event in $type (space-separated)
2556     */
2557    public function unbind($type = null, $callback = null) {
2558        foreach($this->elements as $node)
2559            phpQueryEvents::remove($this->getDocumentID(), $node, $type, $callback);
2560        return $this;
2561    }
2562    /**
2563     * Enter description here...
2564     *
2565     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2566     */
2567    public function change($callback = null) {
2568        if ($callback)
2569            return $this->bind('change', $callback);
2570        return $this->trigger('change');
2571    }
2572    /**
2573     * Enter description here...
2574     *
2575     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2576     */
2577    public function submit($callback = null) {
2578        if ($callback)
2579            return $this->bind('submit', $callback);
2580        return $this->trigger('submit');
2581    }
2582    /**
2583     * Enter description here...
2584     *
2585     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2586     */
2587    public function click($callback = null) {
2588        if ($callback)
2589            return $this->bind('click', $callback);
2590        return $this->trigger('click');
2591    }
2592    /**
2593     * Enter description here...
2594     *
2595     * @param String|phpQuery
2596     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2597     */
2598    public function wrapAllOld($wrapper) {
2599        $wrapper = pq($wrapper)->_clone();
2600        if (! $wrapper->length() || ! $this->length() )
2601            return $this;
2602        $wrapper->insertBefore($this->elements[0]);
2603        $deepest = $wrapper->elements[0];
2604        while($deepest->firstChild && $deepest->firstChild instanceof DOMELEMENT)
2605            $deepest = $deepest->firstChild;
2606        pq($deepest)->append($this);
2607        return $this;
2608    }
2609    /**
2610     * Enter description here...
2611     *
2612     * TODO testme...
2613     * @param String|phpQuery
2614     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2615     */
2616    public function wrapAll($wrapper) {
2617        if (! $this->length())
2618            return $this;
2619        return phpQuery::pq($wrapper, $this->getDocumentID())
2620            ->clone()
2621            ->insertBefore($this->get(0))
2622            ->map(array($this, '___wrapAllCallback'))
2623            ->append($this);
2624    }
2625  /**
2626   *
2627     * @param $node
2628     * @return unknown_type
2629     * @access private
2630   */
2631    public function ___wrapAllCallback($node) {
2632        $deepest = $node;
2633        while($deepest->firstChild && $deepest->firstChild instanceof DOMELEMENT)
2634            $deepest = $deepest->firstChild;
2635        return $deepest;
2636    }
2637    /**
2638     * Enter description here...
2639     * NON JQUERY METHOD
2640     *
2641     * @param String|phpQuery
2642     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2643     */
2644    public function wrapAllPHP($codeBefore, $codeAfter) {
2645        return $this
2646            ->slice(0, 1)
2647                ->beforePHP($codeBefore)
2648            ->end()
2649            ->slice(-1)
2650                ->afterPHP($codeAfter)
2651            ->end();
2652    }
2653    /**
2654     * Enter description here...
2655     *
2656     * @param String|phpQuery
2657     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2658     */
2659    public function wrap($wrapper) {
2660        foreach($this->stack() as $node)
2661            phpQuery::pq($node, $this->getDocumentID())->wrapAll($wrapper);
2662        return $this;
2663    }
2664    /**
2665     * Enter description here...
2666     *
2667     * @param String|phpQuery
2668     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2669     */
2670    public function wrapPHP($codeBefore, $codeAfter) {
2671        foreach($this->stack() as $node)
2672            phpQuery::pq($node, $this->getDocumentID())->wrapAllPHP($codeBefore, $codeAfter);
2673        return $this;
2674    }
2675    /**
2676     * Enter description here...
2677     *
2678     * @param String|phpQuery
2679     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2680     */
2681    public function wrapInner($wrapper) {
2682        foreach($this->stack() as $node)
2683            phpQuery::pq($node, $this->getDocumentID())->contents()->wrapAll($wrapper);
2684        return $this;
2685    }
2686    /**
2687     * Enter description here...
2688     *
2689     * @param String|phpQuery
2690     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2691     */
2692    public function wrapInnerPHP($codeBefore, $codeAfter) {
2693        foreach($this->stack(1) as $node)
2694            phpQuery::pq($node, $this->getDocumentID())->contents()
2695                ->wrapAllPHP($codeBefore, $codeAfter);
2696        return $this;
2697    }
2698    /**
2699     * Enter description here...
2700     *
2701     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2702     * @testme Support for text nodes
2703     */
2704    public function contents() {
2705        $stack = array();
2706        foreach($this->stack(1) as $el) {
2707            // FIXME (fixed) http://code.google.com/p/phpquery/issues/detail?id=56
2708//          if (! isset($el->childNodes))
2709//              continue;
2710            foreach($el->childNodes as $node) {
2711                $stack[] = $node;
2712            }
2713        }
2714        return $this->newInstance($stack);
2715    }
2716    /**
2717     * Enter description here...
2718     *
2719     * jQuery difference.
2720     *
2721     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2722     */
2723    public function contentsUnwrap() {
2724        foreach($this->stack(1) as $node) {
2725            if (! $node->parentNode )
2726                continue;
2727            $childNodes = array();
2728            // any modification in DOM tree breaks childNodes iteration, so cache them first
2729            foreach($node->childNodes as $chNode )
2730                $childNodes[] = $chNode;
2731            foreach($childNodes as $chNode )
2732//              $node->parentNode->appendChild($chNode);
2733                $node->parentNode->insertBefore($chNode, $node);
2734            $node->parentNode->removeChild($node);
2735        }
2736        return $this;
2737    }
2738    /**
2739     * Enter description here...
2740     *
2741     * jQuery difference.
2742     */
2743    public function switchWith($markup) {
2744        $markup = pq($markup, $this->getDocumentID());
2745        $content = null;
2746        foreach($this->stack(1) as $node) {
2747            pq($node)
2748                ->contents()->toReference($content)->end()
2749                ->replaceWith($markup->clone()->append($content));
2750        }
2751        return $this;
2752    }
2753    /**
2754     * Enter description here...
2755     *
2756     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2757     */
2758    public function eq($num) {
2759        $oldStack = $this->elements;
2760        $this->elementsBackup = $this->elements;
2761        $this->elements = array();
2762        if ( isset($oldStack[$num]) )
2763            $this->elements[] = $oldStack[$num];
2764        return $this->newInstance();
2765    }
2766    /**
2767     * Enter description here...
2768     *
2769     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2770     */
2771    public function size() {
2772        return count($this->elements);
2773    }
2774    /**
2775     * Enter description here...
2776     *
2777     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2778     * @deprecated Use length as attribute
2779     */
2780    public function length() {
2781        return $this->size();
2782    }
2783    public function count() {
2784        return $this->size();
2785    }
2786    /**
2787     * Enter description here...
2788     *
2789     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2790     * @todo $level
2791     */
2792    public function end($level = 1) {
2793//      $this->elements = array_pop( $this->history );
2794//      return $this;
2795//      $this->previous->DOM = $this->DOM;
2796//      $this->previous->XPath = $this->XPath;
2797        return $this->previous
2798            ? $this->previous
2799            : $this;
2800    }
2801    /**
2802     * Enter description here...
2803     * Normal use ->clone() .
2804     *
2805     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2806     * @access private
2807     */
2808    public function _clone() {
2809        $newStack = array();
2810        //pr(array('copy... ', $this->whois()));
2811        //$this->dumpHistory('copy');
2812        $this->elementsBackup = $this->elements;
2813        foreach($this->elements as $node) {
2814            $newStack[] = $node->cloneNode(true);
2815        }
2816        $this->elements = $newStack;
2817        return $this->newInstance();
2818    }
2819    /**
2820     * Enter description here...
2821     *
2822     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2823     */
2824    public function replaceWithPHP($code) {
2825        return $this->replaceWith(phpQuery::php($code));
2826    }
2827    /**
2828     * Enter description here...
2829     *
2830     * @param String|phpQuery $content
2831     * @link http://docs.jquery.com/Manipulation/replaceWith#content
2832     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2833     */
2834    public function replaceWith($content) {
2835        return $this->after($content)->remove();
2836    }
2837    /**
2838     * Enter description here...
2839     *
2840     * @param String $selector
2841     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2842     * @todo this works ?
2843     */
2844    public function replaceAll($selector) {
2845        foreach(phpQuery::pq($selector, $this->getDocumentID()) as $node)
2846            phpQuery::pq($node, $this->getDocumentID())
2847                ->after($this->_clone())
2848                ->remove();
2849        return $this;
2850    }
2851    /**
2852     * Enter description here...
2853     *
2854     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2855     */
2856    public function remove($selector = null) {
2857        $loop = $selector
2858            ? $this->filter($selector)->elements
2859            : $this->elements;
2860        foreach($loop as $node) {
2861            if (! $node->parentNode )
2862                continue;
2863            if (isset($node->tagName))
2864                $this->debug("Removing '{$node->tagName}'");
2865            $node->parentNode->removeChild($node);
2866            // Mutation event
2867            $event = new DOMEvent(array(
2868                'target' => $node,
2869                'type' => 'DOMNodeRemoved'
2870            ));
2871            phpQueryEvents::trigger($this->getDocumentID(),
2872                $event->type, array($event), $node
2873            );
2874        }
2875        return $this;
2876    }
2877    protected function markupEvents($newMarkup, $oldMarkup, $node) {
2878        if ($node->tagName == 'textarea' && $newMarkup != $oldMarkup) {
2879            $event = new DOMEvent(array(
2880                'target' => $node,
2881                'type' => 'change'
2882            ));
2883            phpQueryEvents::trigger($this->getDocumentID(),
2884                $event->type, array($event), $node
2885            );
2886        }
2887    }
2888    /**
2889     * jQuey difference
2890     *
2891     * @param $markup
2892     * @return unknown_type
2893     * @TODO trigger change event for textarea
2894     */
2895    public function markup($markup = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2896        $args = func_get_args();
2897        if ($this->documentWrapper->isXML)
2898            return call_user_func_array(array($this, 'xml'), $args);
2899        else
2900            return call_user_func_array(array($this, 'html'), $args);
2901    }
2902    /**
2903     * jQuey difference
2904     *
2905     * @param $markup
2906     * @return unknown_type
2907     */
2908    public function markupOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2909        $args = func_get_args();
2910        if ($this->documentWrapper->isXML)
2911            return call_user_func_array(array($this, 'xmlOuter'), $args);
2912        else
2913            return call_user_func_array(array($this, 'htmlOuter'), $args);
2914    }
2915    /**
2916     * Enter description here...
2917     *
2918     * @param unknown_type $html
2919     * @return string|phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2920     * @TODO force html result
2921     */
2922    public function html($html = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2923        if (isset($html)) {
2924            // INSERT
2925            $nodes = $this->documentWrapper->import($html);
2926            $this->empty();
2927            foreach($this->stack(1) as $alreadyAdded => $node) {
2928                // for now, limit events for textarea
2929                if (($this->isXHTML() || $this->isHTML()) && $node->tagName == 'textarea')
2930                    $oldHtml = pq($node, $this->getDocumentID())->markup();
2931                foreach($nodes as $newNode) {
2932                    $node->appendChild($alreadyAdded
2933                        ? $newNode->cloneNode(true)
2934                        : $newNode
2935                    );
2936                }
2937                // for now, limit events for textarea
2938                if (($this->isXHTML() || $this->isHTML()) && $node->tagName == 'textarea')
2939                    $this->markupEvents($html, $oldHtml, $node);
2940            }
2941            return $this;
2942        } else {
2943            // FETCH
2944            $return = $this->documentWrapper->markup($this->elements, true);
2945            $args = func_get_args();
2946            foreach(array_slice($args, 1) as $callback) {
2947                $return = phpQuery::callbackRun($callback, array($return));
2948            }
2949            return $return;
2950        }
2951    }
2952    /**
2953     * @TODO force xml result
2954     */
2955    public function xml($xml = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2956        $args = func_get_args();
2957        return call_user_func_array(array($this, 'html'), $args);
2958    }
2959    /**
2960     * Enter description here...
2961     * @TODO force html result
2962     *
2963     * @return String
2964     */
2965    public function htmlOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2966        $markup = $this->documentWrapper->markup($this->elements);
2967        // pass thou callbacks
2968        $args = func_get_args();
2969        foreach($args as $callback) {
2970            $markup = phpQuery::callbackRun($callback, array($markup));
2971        }
2972        return $markup;
2973    }
2974    /**
2975     * @TODO force xml result
2976     */
2977    public function xmlOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2978        $args = func_get_args();
2979        return call_user_func_array(array($this, 'htmlOuter'), $args);
2980    }
2981    public function __toString() {
2982        return $this->markupOuter();
2983    }
2984    /**
2985     * Just like html(), but returns markup with VALID (dangerous) PHP tags.
2986     *
2987     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2988     * @todo support returning markup with PHP tags when called without param
2989     */
2990    public function php($code = null) {
2991        return $this->markupPHP($code);
2992    }
2993    /**
2994     * Enter description here...
2995     *
2996     * @param $code
2997     * @return unknown_type
2998     */
2999    public function markupPHP($code = null) {
3000        return isset($code)
3001            ? $this->markup(phpQuery::php($code))
3002            : phpQuery::markupToPHP($this->markup());
3003    }
3004    /**
3005     * Enter description here...
3006     *
3007     * @param $code
3008     * @return unknown_type
3009     */
3010    public function markupOuterPHP() {
3011        return phpQuery::markupToPHP($this->markupOuter());
3012    }
3013    /**
3014     * Enter description here...
3015     *
3016     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3017     */
3018    public function children($selector = null) {
3019        $stack = array();
3020        foreach($this->stack(1) as $node) {
3021//          foreach($node->getElementsByTagName('*') as $newNode) {
3022            foreach($node->childNodes as $newNode) {
3023                if ($newNode->nodeType != 1)
3024                    continue;
3025                if ($selector && ! $this->is($selector, $newNode))
3026                    continue;
3027                if ($this->elementsContainsNode($newNode, $stack))
3028                    continue;
3029                $stack[] = $newNode;
3030            }
3031        }
3032        $this->elementsBackup = $this->elements;
3033        $this->elements = $stack;
3034        return $this->newInstance();
3035    }
3036    /**
3037     * Enter description here...
3038     *
3039     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3040     */
3041    public function ancestors($selector = null) {
3042        return $this->children( $selector );
3043    }
3044    /**
3045     * Enter description here...
3046     *
3047     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3048     */
3049    public function append( $content) {
3050        return $this->insert($content, __FUNCTION__);
3051    }
3052    /**
3053     * Enter description here...
3054     *
3055     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3056     */
3057    public function appendPHP( $content) {
3058        return $this->insert("<php><!-- {$content} --></php>", 'append');
3059    }
3060    /**
3061     * Enter description here...
3062     *
3063     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3064     */
3065    public function appendTo( $seletor) {
3066        return $this->insert($seletor, __FUNCTION__);
3067    }
3068    /**
3069     * Enter description here...
3070     *
3071     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3072     */
3073    public function prepend( $content) {
3074        return $this->insert($content, __FUNCTION__);
3075    }
3076    /**
3077     * Enter description here...
3078     *
3079     * @todo accept many arguments, which are joined, arrays maybe also
3080     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3081     */
3082    public function prependPHP( $content) {
3083        return $this->insert("<php><!-- {$content} --></php>", 'prepend');
3084    }
3085    /**
3086     * Enter description here...
3087     *
3088     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3089     */
3090    public function prependTo( $seletor) {
3091        return $this->insert($seletor, __FUNCTION__);
3092    }
3093    /**
3094     * Enter description here...
3095     *
3096     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3097     */
3098    public function before($content) {
3099        return $this->insert($content, __FUNCTION__);
3100    }
3101    /**
3102     * Enter description here...
3103     *
3104     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3105     */
3106    public function beforePHP( $content) {
3107        return $this->insert("<php><!-- {$content} --></php>", 'before');
3108    }
3109    /**
3110     * Enter description here...
3111     *
3112     * @param String|phpQuery
3113     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3114     */
3115    public function insertBefore( $seletor) {
3116        return $this->insert($seletor, __FUNCTION__);
3117    }
3118    /**
3119     * Enter description here...
3120     *
3121     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3122     */
3123    public function after( $content) {
3124        return $this->insert($content, __FUNCTION__);
3125    }
3126    /**
3127     * Enter description here...
3128     *
3129     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3130     */
3131    public function afterPHP( $content) {
3132        return $this->insert("<php><!-- {$content} --></php>", 'after');
3133    }
3134    /**
3135     * Enter description here...
3136     *
3137     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3138     */
3139    public function insertAfter( $seletor) {
3140        return $this->insert($seletor, __FUNCTION__);
3141    }
3142    /**
3143     * Internal insert method. Don't use it.
3144     *
3145     * @param unknown_type $target
3146     * @param unknown_type $type
3147     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3148     * @access private
3149     */
3150    public function insert($target, $type) {
3151        $this->debug("Inserting data with '{$type}'");
3152        $to = false;
3153        switch( $type) {
3154            case 'appendTo':
3155            case 'prependTo':
3156            case 'insertBefore':
3157            case 'insertAfter':
3158                $to = true;
3159        }
3160        switch(gettype($target)) {
3161            case 'string':
3162                $insertFrom = $insertTo = array();
3163                if ($to) {
3164                    // INSERT TO
3165                    $insertFrom = $this->elements;
3166                    if (phpQuery::isMarkup($target)) {
3167                        // $target is new markup, import it
3168                        $insertTo = $this->documentWrapper->import($target);
3169                    // insert into selected element
3170                    } else {
3171                        // $tagret is a selector
3172                        $thisStack = $this->elements;
3173                        $this->toRoot();
3174                        $insertTo = $this->find($target)->elements;
3175                        $this->elements = $thisStack;
3176                    }
3177                } else {
3178                    // INSERT FROM
3179                    $insertTo = $this->elements;
3180                    $insertFrom = $this->documentWrapper->import($target);
3181                }
3182                break;
3183            case 'object':
3184                $insertFrom = $insertTo = array();
3185                // phpQuery
3186                if ($target instanceof self) {
3187                    if ($to) {
3188                        $insertTo = $target->elements;
3189                        if ($this->documentFragment && $this->stackIsRoot())
3190                            // get all body children
3191//                          $loop = $this->find('body > *')->elements;
3192                            // TODO test it, test it hard...
3193//                          $loop = $this->newInstance($this->root)->find('> *')->elements;
3194                            $loop = $this->root->childNodes;
3195                        else
3196                            $loop = $this->elements;
3197                        // import nodes if needed
3198                        $insertFrom = $this->getDocumentID() == $target->getDocumentID()
3199                            ? $loop
3200                            : $target->documentWrapper->import($loop);
3201                    } else {
3202                        $insertTo = $this->elements;
3203                        if ( $target->documentFragment && $target->stackIsRoot() )
3204                            // get all body children
3205//                          $loop = $target->find('body > *')->elements;
3206                            $loop = $target->root->childNodes;
3207                        else
3208                            $loop = $target->elements;
3209                        // import nodes if needed
3210                        $insertFrom = $this->getDocumentID() == $target->getDocumentID()
3211                            ? $loop
3212                            : $this->documentWrapper->import($loop);
3213                    }
3214                // DOMNODE
3215                } elseif ($target instanceof DOMNODE) {
3216                    // import node if needed
3217//                  if ( $target->ownerDocument != $this->DOM )
3218//                      $target = $this->DOM->importNode($target, true);
3219                    if ( $to) {
3220                        $insertTo = array($target);
3221                        if ($this->documentFragment && $this->stackIsRoot())
3222                            // get all body children
3223                            $loop = $this->root->childNodes;
3224//                          $loop = $this->find('body > *')->elements;
3225                        else
3226                            $loop = $this->elements;
3227                        foreach($loop as $fromNode)
3228                            // import nodes if needed
3229                            $insertFrom[] = ! $fromNode->ownerDocument->isSameNode($target->ownerDocument)
3230                                ? $target->ownerDocument->importNode($fromNode, true)
3231                                : $fromNode;
3232                    } else {
3233                        // import node if needed
3234                        if (! $target->ownerDocument->isSameNode($this->document))
3235                            $target = $this->document->importNode($target, true);
3236                        $insertTo = $this->elements;
3237                        $insertFrom[] = $target;
3238                    }
3239                }
3240                break;
3241        }
3242        phpQuery::debug("From ".count($insertFrom)."; To ".count($insertTo)." nodes");
3243        foreach($insertTo as $insertNumber => $toNode) {
3244            // we need static relative elements in some cases
3245            switch( $type) {
3246                case 'prependTo':
3247                case 'prepend':
3248                    $firstChild = $toNode->firstChild;
3249                    break;
3250                case 'insertAfter':
3251                case 'after':
3252                    $nextSibling = $toNode->nextSibling;
3253                    break;
3254            }
3255            foreach($insertFrom as $fromNode) {
3256                // clone if inserted already before
3257                $insert = $insertNumber
3258                    ? $fromNode->cloneNode(true)
3259                    : $fromNode;
3260                switch($type) {
3261                    case 'appendTo':
3262                    case 'append':
3263//                      $toNode->insertBefore(
3264//                          $fromNode,
3265//                          $toNode->lastChild->nextSibling
3266//                      );
3267                        $toNode->appendChild($insert);
3268                        $eventTarget = $insert;
3269                        break;
3270                    case 'prependTo':
3271                    case 'prepend':
3272                        $toNode->insertBefore(
3273                            $insert,
3274                            $firstChild
3275                        );
3276                        break;
3277                    case 'insertBefore':
3278                    case 'before':
3279                        if (! $toNode->parentNode)
3280                            throw new Exception("No parentNode, can't do {$type}()");
3281                        else
3282                            $toNode->parentNode->insertBefore(
3283                                $insert,
3284                                $toNode
3285                            );
3286                        break;
3287                    case 'insertAfter':
3288                    case 'after':
3289                        if (! $toNode->parentNode)
3290                            throw new Exception("No parentNode, can't do {$type}()");
3291                        else
3292                            $toNode->parentNode->insertBefore(
3293                                $insert,
3294                                $nextSibling
3295                            );
3296                        break;
3297                }
3298                // Mutation event
3299                $event = new DOMEvent(array(
3300                    'target' => $insert,
3301                    'type' => 'DOMNodeInserted'
3302                ));
3303                phpQueryEvents::trigger($this->getDocumentID(),
3304                    $event->type, array($event), $insert
3305                );
3306            }
3307        }
3308        return $this;
3309    }
3310    /**
3311     * Enter description here...
3312     *
3313     * @return Int
3314     */
3315    public function index($subject) {
3316        $index = -1;
3317        $subject = $subject instanceof phpQueryObject
3318            ? $subject->elements[0]
3319            : $subject;
3320        foreach($this->newInstance() as $k => $node) {
3321            if ($node->isSameNode($subject))
3322                $index = $k;
3323        }
3324        return $index;
3325    }
3326    /**
3327     * Enter description here...
3328     *
3329     * @param unknown_type $start
3330     * @param unknown_type $end
3331     *
3332     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3333     * @testme
3334     */
3335    public function slice($start, $end = null) {
3336//      $last = count($this->elements)-1;
3337//      $end = $end
3338//          ? min($end, $last)
3339//          : $last;
3340//      if ($start < 0)
3341//          $start = $last+$start;
3342//      if ($start > $last)
3343//          return array();
3344        if ($end > 0)
3345            $end = $end-$start;
3346        return $this->newInstance(
3347            array_slice($this->elements, $start, $end)
3348        );
3349    }
3350    /**
3351     * Enter description here...
3352     *
3353     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3354     */
3355    public function reverse() {
3356        $this->elementsBackup = $this->elements;
3357        $this->elements = array_reverse($this->elements);
3358        return $this->newInstance();
3359    }
3360    /**
3361     * Return joined text content.
3362     * @return String
3363     */
3364    public function text($text = null, $callback1 = null, $callback2 = null, $callback3 = null) {
3365        if (isset($text))
3366            return $this->html(htmlspecialchars($text));
3367        $args = func_get_args();
3368        $args = array_slice($args, 1);
3369        $return = '';
3370        foreach($this->elements as $node) {
3371            $text = $node->textContent;
3372            if (count($this->elements) > 1 && $text)
3373                $text .= "\n";
3374            foreach($args as $callback) {
3375                $text = phpQuery::callbackRun($callback, array($text));
3376            }
3377            $return .= $text;
3378        }
3379        return $return;
3380    }
3381    /**
3382     * Enter description here...
3383     *
3384     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3385     */
3386    public function plugin($class, $file = null) {
3387        phpQuery::plugin($class, $file);
3388        return $this;
3389    }
3390    /**
3391     * Deprecated, use $pq->plugin() instead.
3392     *
3393     * @deprecated
3394     * @param $class
3395     * @param $file
3396     * @return unknown_type
3397     */
3398    public static function extend($class, $file = null) {
3399        return $this->plugin($class, $file);
3400    }
3401    /**
3402     *
3403     * @access private
3404     * @param $method
3405     * @param $args
3406     * @return unknown_type
3407     */
3408    public function __call($method, $args) {
3409        $aliasMethods = array('clone', 'empty');
3410        if (isset(phpQuery::$extendMethods[$method])) {
3411            array_unshift($args, $this);
3412            return phpQuery::callbackRun(
3413                phpQuery::$extendMethods[$method], $args
3414            );
3415        } else if (isset(phpQuery::$pluginsMethods[$method])) {
3416            array_unshift($args, $this);
3417            $class = phpQuery::$pluginsMethods[$method];
3418            $realClass = "phpQueryObjectPlugin_$class";
3419            $return = call_user_func_array(
3420                array($realClass, $method),
3421                $args
3422            );
3423            // XXX deprecate ?
3424            return is_null($return)
3425                ? $this
3426                : $return;
3427        } else if (in_array($method, $aliasMethods)) {
3428            return call_user_func_array(array($this, '_'.$method), $args);
3429        } else
3430            throw new Exception("Method '{$method}' doesnt exist");
3431    }
3432    /**
3433     * Safe rename of next().
3434     *
3435     * Use it ONLY when need to call next() on an iterated object (in same time).
3436     * Normaly there is no need to do such thing ;)
3437     *
3438     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3439     * @access private
3440     */
3441    public function _next($selector = null) {
3442        return $this->newInstance(
3443            $this->getElementSiblings('nextSibling', $selector, true)
3444        );
3445    }
3446    /**
3447     * Use prev() and next().
3448     *
3449     * @deprecated
3450     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3451     * @access private
3452     */
3453    public function _prev($selector = null) {
3454        return $this->prev($selector);
3455    }
3456    /**
3457     * Enter description here...
3458     *
3459     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3460     */
3461    public function prev($selector = null) {
3462        return $this->newInstance(
3463            $this->getElementSiblings('previousSibling', $selector, true)
3464        );
3465    }
3466    /**
3467     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3468     * @todo
3469     */
3470    public function prevAll($selector = null) {
3471        return $this->newInstance(
3472            $this->getElementSiblings('previousSibling', $selector)
3473        );
3474    }
3475    /**
3476     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3477     * @todo FIXME: returns source elements insted of next siblings
3478     */
3479    public function nextAll($selector = null) {
3480        return $this->newInstance(
3481            $this->getElementSiblings('nextSibling', $selector)
3482        );
3483    }
3484    /**
3485     * @access private
3486     */
3487    protected function getElementSiblings($direction, $selector = null, $limitToOne = false) {
3488        $stack = array();
3489        $count = 0;
3490        foreach($this->stack() as $node) {
3491            $test = $node;
3492            while( isset($test->{$direction}) && $test->{$direction}) {
3493                $test = $test->{$direction};
3494                if (! $test instanceof DOMELEMENT)
3495                    continue;
3496                $stack[] = $test;
3497                if ($limitToOne)
3498                    break;
3499            }
3500        }
3501        if ($selector) {
3502            $stackOld = $this->elements;
3503            $this->elements = $stack;
3504            $stack = $this->filter($selector, true)->stack();
3505            $this->elements = $stackOld;
3506        }
3507        return $stack;
3508    }
3509    /**
3510     * Enter description here...
3511     *
3512     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3513     */
3514    public function siblings($selector = null) {
3515        $stack = array();
3516        $siblings = array_merge(
3517            $this->getElementSiblings('previousSibling', $selector),
3518            $this->getElementSiblings('nextSibling', $selector)
3519        );
3520        foreach($siblings as $node) {
3521            if (! $this->elementsContainsNode($node, $stack))
3522                $stack[] = $node;
3523        }
3524        return $this->newInstance($stack);
3525    }
3526    /**
3527     * Enter description here...
3528     *
3529     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3530     */
3531    public function not($selector = null) {
3532        if (is_string($selector))
3533            phpQuery::debug(array('not', $selector));
3534        else
3535            phpQuery::debug('not');
3536        $stack = array();
3537        if ($selector instanceof self || $selector instanceof DOMNODE) {
3538            foreach($this->stack() as $node) {
3539                if ($selector instanceof self) {
3540                    $matchFound = false;
3541                    foreach($selector->stack() as $notNode) {
3542                        if ($notNode->isSameNode($node))
3543                            $matchFound = true;
3544                    }
3545                    if (! $matchFound)
3546                        $stack[] = $node;
3547                } else if ($selector instanceof DOMNODE) {
3548                    if (! $selector->isSameNode($node))
3549                        $stack[] = $node;
3550                } else {
3551                    if (! $this->is($selector))
3552                        $stack[] = $node;
3553                }
3554            }
3555        } else {
3556            $orgStack = $this->stack();
3557            $matched = $this->filter($selector, true)->stack();
3558//          $matched = array();
3559//          // simulate OR in filter() instead of AND 5y
3560//          foreach($this->parseSelector($selector) as $s) {
3561//              $matched = array_merge($matched,
3562//                  $this->filter(array($s))->stack()
3563//              );
3564//          }
3565            foreach($orgStack as $node)
3566                if (! $this->elementsContainsNode($node, $matched))
3567                    $stack[] = $node;
3568        }
3569        return $this->newInstance($stack);
3570    }
3571    /**
3572     * Enter description here...
3573     *
3574     * @param string|phpQueryObject
3575     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3576     */
3577    public function add($selector = null) {
3578        if (! $selector)
3579            return $this;
3580        $stack = array();
3581        $this->elementsBackup = $this->elements;
3582        $found = phpQuery::pq($selector, $this->getDocumentID());
3583        $this->merge($found->elements);
3584        return $this->newInstance();
3585    }
3586    /**
3587     * @access private
3588     */
3589    protected function merge() {
3590        foreach(func_get_args() as $nodes)
3591            foreach($nodes as $newNode )
3592                if (! $this->elementsContainsNode($newNode) )
3593                    $this->elements[] = $newNode;
3594    }
3595    /**
3596     * @access private
3597     * TODO refactor to stackContainsNode
3598     */
3599    protected function elementsContainsNode($nodeToCheck, $elementsStack = null) {
3600        $loop = ! is_null($elementsStack)
3601            ? $elementsStack
3602            : $this->elements;
3603        foreach($loop as $node) {
3604            if ( $node->isSameNode( $nodeToCheck ) )
3605                return true;
3606        }
3607        return false;
3608    }
3609    /**
3610     * Enter description here...
3611     *
3612     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3613     */
3614    public function parent($selector = null) {
3615        $stack = array();
3616        foreach($this->elements as $node )
3617            if ( $node->parentNode && ! $this->elementsContainsNode($node->parentNode, $stack) )
3618                $stack[] = $node->parentNode;
3619        $this->elementsBackup = $this->elements;
3620        $this->elements = $stack;
3621        if ( $selector )
3622            $this->filter($selector, true);
3623        return $this->newInstance();
3624    }
3625    /**
3626     * Enter description here...
3627     *
3628     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3629     */
3630    public function parents($selector = null) {
3631        $stack = array();
3632        if (! $this->elements )
3633            $this->debug('parents() - stack empty');
3634        foreach($this->elements as $node) {
3635            $test = $node;
3636            while( $test->parentNode) {
3637                $test = $test->parentNode;
3638                if ($this->isRoot($test))
3639                    break;
3640                if (! $this->elementsContainsNode($test, $stack)) {
3641                    $stack[] = $test;
3642                    continue;
3643                }
3644            }
3645        }
3646        $this->elementsBackup = $this->elements;
3647        $this->elements = $stack;
3648        if ( $selector )
3649            $this->filter($selector, true);
3650        return $this->newInstance();
3651    }
3652    /**
3653     * Internal stack iterator.
3654     *
3655     * @access private
3656     */
3657    public function stack($nodeTypes = null) {
3658        if (!isset($nodeTypes))
3659            return $this->elements;
3660        if (!is_array($nodeTypes))
3661            $nodeTypes = array($nodeTypes);
3662        $return = array();
3663        foreach($this->elements as $node) {
3664            if (in_array($node->nodeType, $nodeTypes))
3665                $return[] = $node;
3666        }
3667        return $return;
3668    }
3669    // TODO phpdoc; $oldAttr is result of hasAttribute, before any changes
3670    protected function attrEvents($attr, $oldAttr, $oldValue, $node) {
3671        // skip events for XML documents
3672        if (! $this->isXHTML() && ! $this->isHTML())
3673            return;
3674        $event = null;
3675        // identify
3676        $isInputValue = $node->tagName == 'input'
3677            && (
3678                in_array($node->getAttribute('type'),
3679                    array('text', 'password', 'hidden'))
3680                || !$node->getAttribute('type')
3681                 );
3682        $isRadio = $node->tagName == 'input'
3683            && $node->getAttribute('type') == 'radio';
3684        $isCheckbox = $node->tagName == 'input'
3685            && $node->getAttribute('type') == 'checkbox';
3686        $isOption = $node->tagName == 'option';
3687        if ($isInputValue && $attr == 'value' && $oldValue != $node->getAttribute($attr)) {
3688            $event = new DOMEvent(array(
3689                'target' => $node,
3690                'type' => 'change'
3691            ));
3692        } else if (($isRadio || $isCheckbox) && $attr == 'checked' && (
3693                // check
3694                (! $oldAttr && $node->hasAttribute($attr))
3695                // un-check
3696                || (! $node->hasAttribute($attr) && $oldAttr)
3697            )) {
3698            $event = new DOMEvent(array(
3699                'target' => $node,
3700                'type' => 'change'
3701            ));
3702        } else if ($isOption && $node->parentNode && $attr == 'selected' && (
3703                // select
3704                (! $oldAttr && $node->hasAttribute($attr))
3705                // un-select
3706                || (! $node->hasAttribute($attr) && $oldAttr)
3707            )) {
3708            $event = new DOMEvent(array(
3709                'target' => $node->parentNode,
3710                'type' => 'change'
3711            ));
3712        }
3713        if ($event) {
3714            phpQueryEvents::trigger($this->getDocumentID(),
3715                $event->type, array($event), $node
3716            );
3717        }
3718    }
3719    public function attr($attr = null, $value = null) {
3720        foreach($this->stack(1) as $node) {
3721            if (! is_null($value)) {
3722                $loop = $attr == '*'
3723                    ? $this->getNodeAttrs($node)
3724                    : array($attr);
3725                foreach($loop as $a) {
3726                    $oldValue = $node->getAttribute($a);
3727                    $oldAttr = $node->hasAttribute($a);
3728                    // TODO raises an error when charset other than UTF-8
3729                    // while document's charset is also not UTF-8
3730                    @$node->setAttribute($a, $value);
3731                    $this->attrEvents($a, $oldAttr, $oldValue, $node);
3732                }
3733            } else if ($attr == '*') {
3734                // jQuery difference
3735                $return = array();
3736                foreach($node->attributes as $n => $v)
3737                    $return[$n] = $v->value;
3738                return $return;
3739            } else
3740                return $node->hasAttribute($attr)
3741                    ? $node->getAttribute($attr)
3742                    : null;
3743        }
3744        return is_null($value)
3745            ? '' : $this;
3746    }
3747    /**
3748     * @access private
3749     */
3750    protected function getNodeAttrs($node) {
3751        $return = array();
3752        foreach($node->attributes as $n => $o)
3753            $return[] = $n;
3754        return $return;
3755    }
3756    /**
3757     * Enter description here...
3758     *
3759     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3760     * @todo check CDATA ???
3761     */
3762    public function attrPHP($attr, $code) {
3763        if (! is_null($code)) {
3764            $value = '<'.'?php '.$code.' ?'.'>';
3765            // TODO tempolary solution
3766            // http://code.google.com/p/phpquery/issues/detail?id=17
3767//          if (function_exists('mb_detect_encoding') && mb_detect_encoding($value) == 'ASCII')
3768//              $value  = mb_convert_encoding($value, 'UTF-8', 'HTML-ENTITIES');
3769        }
3770        foreach($this->stack(1) as $node) {
3771            if (! is_null($code)) {
3772//              $attrNode = $this->DOM->createAttribute($attr);
3773                $node->setAttribute($attr, $value);
3774//              $attrNode->value = $value;
3775//              $node->appendChild($attrNode);
3776            } else if ( $attr == '*') {
3777                // jQuery diff
3778                $return = array();
3779                foreach($node->attributes as $n => $v)
3780                    $return[$n] = $v->value;
3781                return $return;
3782            } else
3783                return $node->getAttribute($attr);
3784        }
3785        return $this;
3786    }
3787    /**
3788     * Enter description here...
3789     *
3790     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3791     */
3792    public function removeAttr($attr) {
3793        foreach($this->stack(1) as $node) {
3794            $loop = $attr == '*'
3795                ? $this->getNodeAttrs($node)
3796                : array($attr);
3797            foreach($loop as $a) {
3798                $oldValue = $node->getAttribute($a);
3799                $node->removeAttribute($a);
3800                $this->attrEvents($a, $oldValue, null, $node);
3801            }
3802        }
3803        return $this;
3804    }
3805    /**
3806     * Return form element value.
3807     *
3808     * @return String Fields value.
3809     */
3810    public function val($val = null) {
3811        if (! isset($val)) {
3812            if ($this->eq(0)->is('select')) {
3813                    $selected = $this->eq(0)->find('option[selected=selected]');
3814                    if ($selected->is('[value]'))
3815                        return $selected->attr('value');
3816                    else
3817                        return $selected->text();
3818            } else if ($this->eq(0)->is('textarea'))
3819                    return $this->eq(0)->markup();
3820                else
3821                    return $this->eq(0)->attr('value');
3822        } else {
3823            $_val = null;
3824            foreach($this->stack(1) as $node) {
3825                $node = pq($node, $this->getDocumentID());
3826                if (is_array($val) && in_array($node->attr('type'), array('checkbox', 'radio'))) {
3827                    $isChecked = in_array($node->attr('value'), $val)
3828                            || in_array($node->attr('name'), $val);
3829                    if ($isChecked)
3830                        $node->attr('checked', 'checked');
3831                    else
3832                        $node->removeAttr('checked');
3833                } else if ($node->get(0)->tagName == 'select') {
3834                    if (! isset($_val)) {
3835                        $_val = array();
3836                        if (! is_array($val))
3837                            $_val = array((string)$val);
3838                        else
3839                            foreach($val as $v)
3840                                $_val[] = $v;
3841                    }
3842                    foreach($node['option']->stack(1) as $option) {
3843                        $option = pq($option, $this->getDocumentID());
3844                        $selected = false;
3845                        // XXX: workaround for string comparsion, see issue #96
3846                        // http://code.google.com/p/phpquery/issues/detail?id=96
3847                        $selected = is_null($option->attr('value'))
3848                            ? in_array($option->markup(), $_val)
3849                            : in_array($option->attr('value'), $_val);
3850//                      $optionValue = $option->attr('value');
3851//                      $optionText = $option->text();
3852//                      $optionTextLenght = mb_strlen($optionText);
3853//                      foreach($_val as $v)
3854//                          if ($optionValue == $v)
3855//                              $selected = true;
3856//                          else if ($optionText == $v && $optionTextLenght == mb_strlen($v))
3857//                              $selected = true;
3858                        if ($selected)
3859                            $option->attr('selected', 'selected');
3860                        else
3861                            $option->removeAttr('selected');
3862                    }
3863                } else if ($node->get(0)->tagName == 'textarea')
3864                    $node->markup($val);
3865                else
3866                    $node->attr('value', $val);
3867            }
3868        }
3869        return $this;
3870    }
3871    /**
3872     * Enter description here...
3873     *
3874     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3875     */
3876    public function andSelf() {
3877        if ( $this->previous )
3878            $this->elements = array_merge($this->elements, $this->previous->elements);
3879        return $this;
3880    }
3881    /**
3882     * Enter description here...
3883     *
3884     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3885     */
3886    public function addClass( $className) {
3887        if (! $className)
3888            return $this;
3889        foreach($this->stack(1) as $node) {
3890            if (! $this->is(".$className", $node))
3891                $node->setAttribute(
3892                    'class',
3893                    trim($node->getAttribute('class').' '.$className)
3894                );
3895        }
3896        return $this;
3897    }
3898    /**
3899     * Enter description here...
3900     *
3901     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3902     */
3903    public function addClassPHP( $className) {
3904        foreach($this->stack(1) as $node) {
3905                $classes = $node->getAttribute('class');
3906                $newValue = $classes
3907                    ? $classes.' <'.'?php '.$className.' ?'.'>'
3908                    : '<'.'?php '.$className.' ?'.'>';
3909                $node->setAttribute('class', $newValue);
3910        }
3911        return $this;
3912    }
3913    /**
3914     * Enter description here...
3915     *
3916     * @param   string  $className
3917     * @return  bool
3918     */
3919    public function hasClass($className) {
3920        foreach($this->stack(1) as $node) {
3921            if ( $this->is(".$className", $node))
3922                return true;
3923        }
3924        return false;
3925    }
3926    /**
3927     * Enter description here...
3928     *
3929     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3930     */
3931    public function removeClass($className) {
3932        foreach($this->stack(1) as $node) {
3933            $classes = explode( ' ', $node->getAttribute('class'));
3934            if ( in_array($className, $classes)) {
3935                $classes = array_diff($classes, array($className));
3936                if ( $classes )
3937                    $node->setAttribute('class', implode(' ', $classes));
3938                else
3939                    $node->removeAttribute('class');
3940            }
3941        }
3942        return $this;
3943    }
3944    /**
3945     * Enter description here...
3946     *
3947     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3948     */
3949    public function toggleClass($className) {
3950        foreach($this->stack(1) as $node) {
3951            if ( $this->is( $node, '.'.$className ))
3952                $this->removeClass($className);
3953            else
3954                $this->addClass($className);
3955        }
3956        return $this;
3957    }
3958    /**
3959     * Proper name without underscore (just ->empty()) also works.
3960     *
3961     * Removes all child nodes from the set of matched elements.
3962     *
3963     * Example:
3964     * pq("p")._empty()
3965     *
3966     * HTML:
3967     * <p>Hello, <span>Person</span> <a href="#">and person</a></p>
3968     *
3969     * Result:
3970     * [ <p></p> ]
3971     *
3972     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3973     * @access private
3974     */
3975    public function _empty() {
3976        foreach($this->stack(1) as $node) {
3977            // thx to 'dave at dgx dot cz'
3978            $node->nodeValue = '';
3979        }
3980        return $this;
3981    }
3982    /**
3983     * Enter description here...
3984     *
3985     * @param array|string $callback Expects $node as first param, $index as second
3986     * @param array $scope External variables passed to callback. Use compact('varName1', 'varName2'...) and extract($scope)
3987     * @param array $arg1 Will ba passed as third and futher args to callback.
3988     * @param array $arg2 Will ba passed as fourth and futher args to callback, and so on...
3989     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3990     */
3991    public function each($callback, $param1 = null, $param2 = null, $param3 = null) {
3992        $paramStructure = null;
3993        if (func_num_args() > 1) {
3994            $paramStructure = func_get_args();
3995            $paramStructure = array_slice($paramStructure, 1);
3996        }
3997        foreach($this->elements as $v)
3998            phpQuery::callbackRun($callback, array($v), $paramStructure);
3999        return $this;
4000    }
4001    /**
4002     * Run callback on actual object.
4003     *
4004     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4005     */
4006    public function callback($callback, $param1 = null, $param2 = null, $param3 = null) {
4007        $params = func_get_args();
4008        $params[0] = $this;
4009        phpQuery::callbackRun($callback, $params);
4010        return $this;
4011    }
4012    /**
4013     * Enter description here...
4014     *
4015     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4016     * @todo add $scope and $args as in each() ???
4017     */
4018    public function map($callback, $param1 = null, $param2 = null, $param3 = null) {
4019//      $stack = array();
4020////        foreach($this->newInstance() as $node) {
4021//      foreach($this->newInstance() as $node) {
4022//          $result = call_user_func($callback, $node);
4023//          if ($result)
4024//              $stack[] = $result;
4025//      }
4026        $params = func_get_args();
4027        array_unshift($params, $this->elements);
4028        return $this->newInstance(
4029            call_user_func_array(array('phpQuery', 'map'), $params)
4030//          phpQuery::map($this->elements, $callback)
4031        );
4032    }
4033    /**
4034     * Enter description here...
4035     *
4036     * @param <type> $key
4037     * @param <type> $value
4038     */
4039    public function data($key, $value = null) {
4040        if (! isset($value)) {
4041            // TODO? implement specific jQuery behavior od returning parent values
4042            // is child which we look up doesn't exist
4043            return phpQuery::data($this->get(0), $key, $value, $this->getDocumentID());
4044        } else {
4045            foreach($this as $node)
4046                phpQuery::data($node, $key, $value, $this->getDocumentID());
4047            return $this;
4048        }
4049    }
4050    /**
4051     * Enter description here...
4052     *
4053     * @param <type> $key
4054     */
4055    public function removeData($key) {
4056        foreach($this as $node)
4057            phpQuery::removeData($node, $key, $this->getDocumentID());
4058        return $this;
4059    }
4060    // INTERFACE IMPLEMENTATIONS
4061
4062    // ITERATOR INTERFACE
4063    /**
4064   * @access private
4065     */
4066    public function rewind(){
4067        $this->debug('iterating foreach');
4068//      phpQuery::selectDocument($this->getDocumentID());
4069        $this->elementsBackup = $this->elements;
4070        $this->elementsInterator = $this->elements;
4071        $this->valid = isset( $this->elements[0] )
4072            ? 1 : 0;
4073//      $this->elements = $this->valid
4074//          ? array($this->elements[0])
4075//          : array();
4076        $this->current = 0;
4077    }
4078    /**
4079   * @access private
4080     */
4081    public function current(){
4082        return $this->elementsInterator[ $this->current ];
4083    }
4084    /**
4085   * @access private
4086     */
4087    public function key(){
4088        return $this->current;
4089    }
4090    /**
4091     * Double-function method.
4092     *
4093     * First: main iterator interface method.
4094     * Second: Returning next sibling, alias for _next().
4095     *
4096     * Proper functionality is choosed automagicaly.
4097     *
4098     * @see phpQueryObject::_next()
4099     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4100     */
4101    public function next($cssSelector = null){
4102//      if ($cssSelector || $this->valid)
4103//          return $this->_next($cssSelector);
4104        $this->valid = isset( $this->elementsInterator[ $this->current+1 ] )
4105            ? true
4106            : false;
4107        if (! $this->valid && $this->elementsInterator) {
4108            $this->elementsInterator = null;
4109        } else if ($this->valid) {
4110            $this->current++;
4111        } else {
4112            return $this->_next($cssSelector);
4113        }
4114    }
4115    /**
4116   * @access private
4117     */
4118    public function valid(){
4119        return $this->valid;
4120    }
4121    // ITERATOR INTERFACE END
4122    // ARRAYACCESS INTERFACE
4123    /**
4124   * @access private
4125     */
4126    public function offsetExists($offset) {
4127        return $this->find($offset)->size() > 0;
4128    }
4129    /**
4130   * @access private
4131     */
4132    public function offsetGet($offset) {
4133        return $this->find($offset);
4134    }
4135    /**
4136   * @access private
4137     */
4138    public function offsetSet($offset, $value) {
4139//      $this->find($offset)->replaceWith($value);
4140        $this->find($offset)->html($value);
4141    }
4142    /**
4143   * @access private
4144     */
4145    public function offsetUnset($offset) {
4146        // empty
4147        throw new Exception("Can't do unset, use array interface only for calling queries and replacing HTML.");
4148    }
4149    // ARRAYACCESS INTERFACE END
4150    /**
4151     * Returns node's XPath.
4152     *
4153     * @param unknown_type $oneNode
4154     * @return string
4155     * @TODO use native getNodePath is avaible
4156     * @access private
4157     */
4158    protected function getNodeXpath($oneNode = null, $namespace = null) {
4159        $return = array();
4160        $loop = $oneNode
4161            ? array($oneNode)
4162            : $this->elements;
4163//      if ($namespace)
4164//          $namespace .= ':';
4165        foreach($loop as $node) {
4166            if ($node instanceof DOMDOCUMENT) {
4167                $return[] = '';
4168                continue;
4169            }
4170            $xpath = array();
4171            while(! ($node instanceof DOMDOCUMENT)) {
4172                $i = 1;
4173                $sibling = $node;
4174                while($sibling->previousSibling) {
4175                    $sibling = $sibling->previousSibling;
4176                    $isElement = $sibling instanceof DOMELEMENT;
4177                    if ($isElement && $sibling->tagName == $node->tagName)
4178                        $i++;
4179                }
4180                $xpath[] = $this->isXML()
4181                    ? "*[local-name()='{$node->tagName}'][{$i}]"
4182                    : "{$node->tagName}[{$i}]";
4183                $node = $node->parentNode;
4184            }
4185            $xpath = join('/', array_reverse($xpath));
4186            $return[] = '/'.$xpath;
4187        }
4188        return $oneNode
4189            ? $return[0]
4190            : $return;
4191    }
4192    // HELPERS
4193    public function whois($oneNode = null) {
4194        $return = array();
4195        $loop = $oneNode
4196            ? array( $oneNode )
4197            : $this->elements;
4198        foreach($loop as $node) {
4199            if (isset($node->tagName)) {
4200                $tag = in_array($node->tagName, array('php', 'js'))
4201                    ? strtoupper($node->tagName)
4202                    : $node->tagName;
4203                $return[] = $tag
4204                    .($node->getAttribute('id')
4205                        ? '#'.$node->getAttribute('id'):'')
4206                    .($node->getAttribute('class')
4207                        ? '.'.join('.', explode(' ', $node->getAttribute('class'))):'')
4208                    .($node->getAttribute('name')
4209                        ? '[name="'.$node->getAttribute('name').'"]':'')
4210                    .($node->getAttribute('value') && strpos($node->getAttribute('value'), '<'.'?php') === false
4211                        ? '[value="'.substr(str_replace("\n", '', $node->getAttribute('value')), 0, 15).'"]':'')
4212                    .($node->getAttribute('value') && strpos($node->getAttribute('value'), '<'.'?php') !== false
4213                        ? '[value=PHP]':'')
4214                    .($node->getAttribute('selected')
4215                        ? '[selected]':'')
4216                    .($node->getAttribute('checked')
4217                        ? '[checked]':'')
4218                ;
4219            } else if ($node instanceof DOMTEXT) {
4220                if (trim($node->textContent))
4221                    $return[] = 'Text:'.substr(str_replace("\n", ' ', $node->textContent), 0, 15);
4222            } else {
4223
4224            }
4225        }
4226        return $oneNode && isset($return[0])
4227            ? $return[0]
4228            : $return;
4229    }
4230    /**
4231     * Dump htmlOuter and preserve chain. Usefull for debugging.
4232     *
4233     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4234     *
4235     */
4236    public function dump() {
4237        print 'DUMP #'.(phpQuery::$dumpCount++).' ';
4238        $debug = phpQuery::$debug;
4239        phpQuery::$debug = false;
4240//      print __FILE__.':'.__LINE__."\n";
4241        var_dump($this->htmlOuter());
4242        return $this;
4243    }
4244    public function dumpWhois() {
4245        print 'DUMP #'.(phpQuery::$dumpCount++).' ';
4246        $debug = phpQuery::$debug;
4247        phpQuery::$debug = false;
4248//      print __FILE__.':'.__LINE__."\n";
4249        var_dump('whois', $this->whois());
4250        phpQuery::$debug = $debug;
4251        return $this;
4252    }
4253    public function dumpLength() {
4254        print 'DUMP #'.(phpQuery::$dumpCount++).' ';
4255        $debug = phpQuery::$debug;
4256        phpQuery::$debug = false;
4257//      print __FILE__.':'.__LINE__."\n";
4258        var_dump('length', $this->length());
4259        phpQuery::$debug = $debug;
4260        return $this;
4261    }
4262    public function dumpTree($html = true, $title = true) {
4263        $output = $title
4264            ? 'DUMP #'.(phpQuery::$dumpCount++)." \n" : '';
4265        $debug = phpQuery::$debug;
4266        phpQuery::$debug = false;
4267        foreach($this->stack() as $node)
4268            $output .= $this->dumpTreeInternal($node);
4269        phpQuery::$debug = $debug;
4270        print $html
4271            ? nl2br(str_replace(' ', '&nbsp;', $output))
4272            : $output;
4273        return $this;
4274    }
4275    private function dumpTreeInternal($node, $intend = 0) {
4276        $whois = $this->whois($node);
4277        $return = '';
4278        if ($whois)
4279            $return .= str_repeat(' - ', $intend).$whois."\n";
4280        if (isset($node->childNodes))
4281            foreach($node->childNodes as $chNode)
4282                $return .= $this->dumpTreeInternal($chNode, $intend+1);
4283        return $return;
4284    }
4285    /**
4286     * Dump htmlOuter and stop script execution. Usefull for debugging.
4287     *
4288     */
4289    public function dumpDie() {
4290        print __FILE__.':'.__LINE__;
4291        var_dump($this->htmlOuter());
4292        die();
4293    }
4294}
4295
4296
4297// -- Multibyte Compatibility functions ---------------------------------------
4298// http://svn.iphonewebdev.com/lace/lib/mb_compat.php
4299
4300/**
4301 *  mb_internal_encoding()
4302 *
4303 *  Included for mbstring pseudo-compatability.
4304 */
4305if (!function_exists('mb_internal_encoding'))
4306{
4307    function mb_internal_encoding($enc) {return true; }
4308}
4309
4310/**
4311 *  mb_regex_encoding()
4312 *
4313 *  Included for mbstring pseudo-compatability.
4314 */
4315if (!function_exists('mb_regex_encoding'))
4316{
4317    function mb_regex_encoding($enc) {return true; }
4318}
4319
4320/**
4321 *  mb_strlen()
4322 *
4323 *  Included for mbstring pseudo-compatability.
4324 */
4325if (!function_exists('mb_strlen'))
4326{
4327    function mb_strlen($str)
4328    {
4329        return strlen($str);
4330    }
4331}
4332
4333/**
4334 *  mb_strpos()
4335 *
4336 *  Included for mbstring pseudo-compatability.
4337 */
4338if (!function_exists('mb_strpos'))
4339{
4340    function mb_strpos($haystack, $needle, $offset=0)
4341    {
4342        return strpos($haystack, $needle, $offset);
4343    }
4344}
4345/**
4346 *  mb_stripos()
4347 *
4348 *  Included for mbstring pseudo-compatability.
4349 */
4350if (!function_exists('mb_stripos'))
4351{
4352    function mb_stripos($haystack, $needle, $offset=0)
4353    {
4354        return stripos($haystack, $needle, $offset);
4355    }
4356}
4357
4358/**
4359 *  mb_substr()
4360 *
4361 *  Included for mbstring pseudo-compatability.
4362 */
4363if (!function_exists('mb_substr'))
4364{
4365    function mb_substr($str, $start, $length=0)
4366    {
4367        return substr($str, $start, $length);
4368    }
4369}
4370
4371/**
4372 *  mb_substr_count()
4373 *
4374 *  Included for mbstring pseudo-compatability.
4375 */
4376if (!function_exists('mb_substr_count'))
4377{
4378    function mb_substr_count($haystack, $needle)
4379    {
4380        return substr_count($haystack, $needle);
4381    }
4382}
4383
4384
4385/**
4386 * Static namespace for phpQuery functions.
4387 *
4388 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
4389 * @package phpQuery
4390 */
4391abstract class phpQuery {
4392    /**
4393     * XXX: Workaround for mbstring problems
4394     *
4395     * @var bool
4396     */
4397    public static $mbstringSupport = true;
4398    public static $debug = false;
4399    public static $documents = array();
4400    public static $defaultDocumentID = null;
4401//  public static $defaultDoctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"';
4402    /**
4403     * Applies only to HTML.
4404     *
4405     * @var unknown_type
4406     */
4407    public static $defaultDoctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
4408"http://www.w3.org/TR/html4/loose.dtd">';
4409    public static $defaultCharset = 'UTF-8';
4410    /**
4411     * Static namespace for plugins.
4412     *
4413     * @var object
4414     */
4415    public static $plugins = array();
4416    /**
4417     * List of loaded plugins.
4418     *
4419     * @var unknown_type
4420     */
4421    public static $pluginsLoaded = array();
4422    public static $pluginsMethods = array();
4423    public static $pluginsStaticMethods = array();
4424    public static $extendMethods = array();
4425    /**
4426     * @TODO implement
4427     */
4428    public static $extendStaticMethods = array();
4429    /**
4430     * Hosts allowed for AJAX connections.
4431     * Dot '.' means $_SERVER['HTTP_HOST'] (if any).
4432     *
4433     * @var array
4434     */
4435    public static $ajaxAllowedHosts = array(
4436        '.'
4437    );
4438    /**
4439     * AJAX settings.
4440     *
4441     * @var array
4442     * XXX should it be static or not ?
4443     */
4444    public static $ajaxSettings = array(
4445        'url' => '',//TODO
4446        'global' => true,
4447        'type' => "GET",
4448        'timeout' => null,
4449        'contentType' => "application/x-www-form-urlencoded",
4450        'processData' => true,
4451//      'async' => true,
4452        'data' => null,
4453        'username' => null,
4454        'password' => null,
4455        'accepts' => array(
4456            'xml' => "application/xml, text/xml",
4457            'html' => "text/html",
4458            'script' => "text/javascript, application/javascript",
4459            'json' => "application/json, text/javascript",
4460            'text' => "text/plain",
4461            '_default' => "*/*"
4462        )
4463    );
4464    public static $lastModified = null;
4465    public static $active = 0;
4466    public static $dumpCount = 0;
4467    /**
4468     * Multi-purpose function.
4469     * Use pq() as shortcut.
4470     *
4471     * In below examples, $pq is any result of pq(); function.
4472     *
4473     * 1. Import markup into existing document (without any attaching):
4474     * - Import into selected document:
4475     *   pq('<div/>')               // DOESNT accept text nodes at beginning of input string !
4476     * - Import into document with ID from $pq->getDocumentID():
4477     *   pq('<div/>', $pq->getDocumentID())
4478     * - Import into same document as DOMNode belongs to:
4479     *   pq('<div/>', DOMNode)
4480     * - Import into document from phpQuery object:
4481     *   pq('<div/>', $pq)
4482     *
4483     * 2. Run query:
4484     * - Run query on last selected document:
4485     *   pq('div.myClass')
4486     * - Run query on document with ID from $pq->getDocumentID():
4487     *   pq('div.myClass', $pq->getDocumentID())
4488     * - Run query on same document as DOMNode belongs to and use node(s)as root for query:
4489     *   pq('div.myClass', DOMNode)
4490     * - Run query on document from phpQuery object
4491     *   and use object's stack as root node(s) for query:
4492     *   pq('div.myClass', $pq)
4493     *
4494     * @param string|DOMNode|DOMNodeList|array  $arg1   HTML markup, CSS Selector, DOMNode or array of DOMNodes
4495     * @param string|phpQueryObject|DOMNode $context    DOM ID from $pq->getDocumentID(), phpQuery object (determines also query root) or DOMNode (determines also query root)
4496     *
4497     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery|QueryTemplatesPhpQuery|false
4498   * phpQuery object or false in case of error.
4499     */
4500    public static function pq($arg1, $context = null) {
4501        if ($arg1 instanceof DOMNODE && ! isset($context)) {
4502            foreach(phpQuery::$documents as $documentWrapper) {
4503                $compare = $arg1 instanceof DOMDocument
4504                    ? $arg1 : $arg1->ownerDocument;
4505                if ($documentWrapper->document->isSameNode($compare))
4506                    $context = $documentWrapper->id;
4507            }
4508        }
4509        if (! $context) {
4510            $domId = self::$defaultDocumentID;
4511            if (! $domId)
4512                throw new Exception("Can't use last created DOM, because there isn't any. Use phpQuery::newDocument() first.");
4513//      } else if (is_object($context) && ($context instanceof PHPQUERY || is_subclass_of($context, 'phpQueryObject')))
4514        } else if (is_object($context) && $context instanceof phpQueryObject)
4515            $domId = $context->getDocumentID();
4516        else if ($context instanceof DOMDOCUMENT) {
4517            $domId = self::getDocumentID($context);
4518            if (! $domId) {
4519                //throw new Exception('Orphaned DOMDocument');
4520                $domId = self::newDocument($context)->getDocumentID();
4521            }
4522        } else if ($context instanceof DOMNODE) {
4523            $domId = self::getDocumentID($context);
4524            if (! $domId) {
4525                throw new Exception('Orphaned DOMNode');
4526//              $domId = self::newDocument($context->ownerDocument);
4527            }
4528        } else
4529            $domId = $context;
4530        if ($arg1 instanceof phpQueryObject) {
4531//      if (is_object($arg1) && (get_class($arg1) == 'phpQueryObject' || $arg1 instanceof PHPQUERY || is_subclass_of($arg1, 'phpQueryObject'))) {
4532            /**
4533             * Return $arg1 or import $arg1 stack if document differs:
4534             * pq(pq('<div/>'))
4535             */
4536            if ($arg1->getDocumentID() == $domId)
4537                return $arg1;
4538            $class = get_class($arg1);
4539            // support inheritance by passing old object to overloaded constructor
4540            $phpQuery = $class != 'phpQuery'
4541                ? new $class($arg1, $domId)
4542                : new phpQueryObject($domId);
4543            $phpQuery->elements = array();
4544            foreach($arg1->elements as $node)
4545                $phpQuery->elements[] = $phpQuery->document->importNode($node, true);
4546            return $phpQuery;
4547        } else if ($arg1 instanceof DOMNODE || (is_array($arg1) && isset($arg1[0]) && $arg1[0] instanceof DOMNODE)) {
4548            /*
4549             * Wrap DOM nodes with phpQuery object, import into document when needed:
4550             * pq(array($domNode1, $domNode2))
4551             */
4552            $phpQuery = new phpQueryObject($domId);
4553            if (!($arg1 instanceof DOMNODELIST) && ! is_array($arg1))
4554                $arg1 = array($arg1);
4555            $phpQuery->elements = array();
4556            foreach($arg1 as $node) {
4557                $sameDocument = $node->ownerDocument instanceof DOMDOCUMENT
4558                    && ! $node->ownerDocument->isSameNode($phpQuery->document);
4559                $phpQuery->elements[] = $sameDocument
4560                    ? $phpQuery->document->importNode($node, true)
4561                    : $node;
4562            }
4563            return $phpQuery;
4564        } else if (self::isMarkup($arg1)) {
4565            /**
4566             * Import HTML:
4567             * pq('<div/>')
4568             */
4569            $phpQuery = new phpQueryObject($domId);
4570            return $phpQuery->newInstance(
4571                $phpQuery->documentWrapper->import($arg1)
4572            );
4573        } else {
4574            /**
4575             * Run CSS query:
4576             * pq('div.myClass')
4577             */
4578            $phpQuery = new phpQueryObject($domId);
4579//          if ($context && ($context instanceof PHPQUERY || is_subclass_of($context, 'phpQueryObject')))
4580            if ($context && $context instanceof phpQueryObject)
4581                $phpQuery->elements = $context->elements;
4582            else if ($context && $context instanceof DOMNODELIST) {
4583                $phpQuery->elements = array();
4584                foreach($context as $node)
4585                    $phpQuery->elements[] = $node;
4586            } else if ($context && $context instanceof DOMNODE)
4587                $phpQuery->elements = array($context);
4588            return $phpQuery->find($arg1);
4589        }
4590    }
4591    /**
4592     * Sets default document to $id. Document has to be loaded prior
4593     * to using this method.
4594     * $id can be retrived via getDocumentID() or getDocumentIDRef().
4595     *
4596     * @param unknown_type $id
4597     */
4598    public static function selectDocument($id) {
4599        $id = self::getDocumentID($id);
4600        self::debug("Selecting document '$id' as default one");
4601        self::$defaultDocumentID = self::getDocumentID($id);
4602    }
4603    /**
4604     * Returns document with id $id or last used as phpQueryObject.
4605     * $id can be retrived via getDocumentID() or getDocumentIDRef().
4606     * Chainable.
4607     *
4608     * @see phpQuery::selectDocument()
4609     * @param unknown_type $id
4610     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4611     */
4612    public static function getDocument($id = null) {
4613        if ($id)
4614            phpQuery::selectDocument($id);
4615        else
4616            $id = phpQuery::$defaultDocumentID;
4617        return new phpQueryObject($id);
4618    }
4619    /**
4620     * Creates new document from markup.
4621     * Chainable.
4622     *
4623     * @param unknown_type $markup
4624     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4625     */
4626    public static function newDocument($markup = null, $contentType = null) {
4627        if (! $markup)
4628            $markup = '';
4629        $documentID = phpQuery::createDocumentWrapper($markup, $contentType);
4630        return new phpQueryObject($documentID);
4631    }
4632    /**
4633     * Creates new document from markup.
4634     * Chainable.
4635     *
4636     * @param unknown_type $markup
4637     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4638     */
4639    public static function newDocumentHTML($markup = null, $charset = null) {
4640        $contentType = $charset
4641            ? ";charset=$charset"
4642            : '';
4643        return self::newDocument($markup, "text/html{$contentType}");
4644    }
4645    /**
4646     * Creates new document from markup.
4647     * Chainable.
4648     *
4649     * @param unknown_type $markup
4650     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4651     */
4652    public static function newDocumentXML($markup = null, $charset = null) {
4653        $contentType = $charset
4654            ? ";charset=$charset"
4655            : '';
4656        return self::newDocument($markup, "text/xml{$contentType}");
4657    }
4658    /**
4659     * Creates new document from markup.
4660     * Chainable.
4661     *
4662     * @param unknown_type $markup
4663     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4664     */
4665    public static function newDocumentXHTML($markup = null, $charset = null) {
4666        $contentType = $charset
4667            ? ";charset=$charset"
4668            : '';
4669        return self::newDocument($markup, "application/xhtml+xml{$contentType}");
4670    }
4671    /**
4672     * Creates new document from markup.
4673     * Chainable.
4674     *
4675     * @param unknown_type $markup
4676     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4677     */
4678    public static function newDocumentPHP($markup = null, $contentType = "text/html") {
4679        // TODO pass charset to phpToMarkup if possible (use DOMDocumentWrapper function)
4680        $markup = phpQuery::phpToMarkup($markup, self::$defaultCharset);
4681        return self::newDocument($markup, $contentType);
4682    }
4683    public static function phpToMarkup($php, $charset = 'utf-8') {
4684        $regexes = array(
4685            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(\')([^\']*)<'.'?php?(.*?)(?:\\?>)([^\']*)\'@s',
4686            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(")([^"]*)<'.'?php?(.*?)(?:\\?>)([^"]*)"@s',
4687        );
4688        foreach($regexes as $regex)
4689            while (preg_match($regex, $php, $matches)) {
4690                $php = preg_replace_callback(
4691                    $regex,
4692//                  create_function('$m, $charset = "'.$charset.'"',
4693//                      'return $m[1].$m[2]
4694//                          .htmlspecialchars("<"."?php".$m[4]."?".">", ENT_QUOTES|ENT_NOQUOTES, $charset)
4695//                          .$m[5].$m[2];'
4696//                  ),
4697                    array('phpQuery', '_phpToMarkupCallback'),
4698                    $php
4699                );
4700            }
4701        $regex = '@(^|>[^<]*)+?(<\?php(.*?)(\?>))@s';
4702//preg_match_all($regex, $php, $matches);
4703//var_dump($matches);
4704        $php = preg_replace($regex, '\\1<php><!-- \\3 --></php>', $php);
4705        return $php;
4706    }
4707    public static function _phpToMarkupCallback($php, $charset = 'utf-8') {
4708        return $m[1].$m[2]
4709            .htmlspecialchars("<"."?php".$m[4]."?".">", ENT_QUOTES|ENT_NOQUOTES, $charset)
4710            .$m[5].$m[2];
4711    }
4712    public static function _markupToPHPCallback($m) {
4713        return "<"."?php ".htmlspecialchars_decode($m[1])." ?".">";
4714    }
4715    /**
4716     * Converts document markup containing PHP code generated by phpQuery::php()
4717     * into valid (executable) PHP code syntax.
4718     *
4719     * @param string|phpQueryObject $content
4720     * @return string PHP code.
4721     */
4722    public static function markupToPHP($content) {
4723        if ($content instanceof phpQueryObject)
4724            $content = $content->markupOuter();
4725        /* <php>...</php> to <?php...? > */
4726        $content = preg_replace_callback(
4727            '@<php>\s*<!--(.*?)-->\s*</php>@s',
4728//          create_function('$m',
4729//              'return "<'.'?php ".htmlspecialchars_decode($m[1])." ?'.'>";'
4730//          ),
4731            array('phpQuery', '_markupToPHPCallback'),
4732            $content
4733        );
4734        /* <node attr='< ?php ? >'> extra space added to save highlighters */
4735        $regexes = array(
4736            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(\')([^\']*)(?:&lt;|%3C)\\?(?:php)?(.*?)(?:\\?(?:&gt;|%3E))([^\']*)\'@s',
4737            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(")([^"]*)(?:&lt;|%3C)\\?(?:php)?(.*?)(?:\\?(?:&gt;|%3E))([^"]*)"@s',
4738        );
4739        foreach($regexes as $regex)
4740            while (preg_match($regex, $content))
4741                $content = preg_replace_callback(
4742                    $regex,
4743                    function ($m) {
4744                        return $m[1] . $m[2] . $m[3] . '<?php '
4745                            . str_replace(
4746                                array("%20", "%3E", "%09", "&#10;", "&#9;", "%7B", "%24", "%7D", "%22", "%5B", "%5D"),
4747                                array(" ", ">", "   ", "\n", "  ", "{", "$", "}", '"', "[", "]"),
4748                                htmlspecialchars_decode($m[4])
4749                            )
4750                            . " ?>" . $m[5] . $m[2];
4751                    },
4752                    $content
4753                );
4754        return $content;
4755    }
4756    /**
4757     * Creates new document from file $file.
4758     * Chainable.
4759     *
4760     * @param string $file URLs allowed. See File wrapper page at php.net for more supported sources.
4761     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4762     */
4763    public static function newDocumentFile($file, $contentType = null) {
4764        $documentID = self::createDocumentWrapper(
4765            file_get_contents($file), $contentType
4766        );
4767        return new phpQueryObject($documentID);
4768    }
4769    /**
4770     * Creates new document from markup.
4771     * Chainable.
4772     *
4773     * @param unknown_type $markup
4774     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4775     */
4776    public static function newDocumentFileHTML($file, $charset = null) {
4777        $contentType = $charset
4778            ? ";charset=$charset"
4779            : '';
4780        return self::newDocumentFile($file, "text/html{$contentType}");
4781    }
4782    /**
4783     * Creates new document from markup.
4784     * Chainable.
4785     *
4786     * @param unknown_type $markup
4787     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4788     */
4789    public static function newDocumentFileXML($file, $charset = null) {
4790        $contentType = $charset
4791            ? ";charset=$charset"
4792            : '';
4793        return self::newDocumentFile($file, "text/xml{$contentType}");
4794    }
4795    /**
4796     * Creates new document from markup.
4797     * Chainable.
4798     *
4799     * @param unknown_type $markup
4800     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4801     */
4802    public static function newDocumentFileXHTML($file, $charset = null) {
4803        $contentType = $charset
4804            ? ";charset=$charset"
4805            : '';
4806        return self::newDocumentFile($file, "application/xhtml+xml{$contentType}");
4807    }
4808    /**
4809     * Creates new document from markup.
4810     * Chainable.
4811     *
4812     * @param unknown_type $markup
4813     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4814     */
4815    public static function newDocumentFilePHP($file, $contentType = null) {
4816        return self::newDocumentPHP(file_get_contents($file), $contentType);
4817    }
4818    /**
4819     * Reuses existing DOMDocument object.
4820     * Chainable.
4821     *
4822     * @param $document DOMDocument
4823     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
4824     * @TODO support DOMDocument
4825     */
4826    public static function loadDocument($document) {
4827        // TODO
4828        die('TODO loadDocument');
4829    }
4830    /**
4831     * Enter description here...
4832     *
4833     * @param unknown_type $html
4834     * @param unknown_type $domId
4835     * @return unknown New DOM ID
4836     * @todo support PHP tags in input
4837     * @todo support passing DOMDocument object from self::loadDocument
4838     */
4839    protected static function createDocumentWrapper($html, $contentType = null, $documentID = null) {
4840        if (function_exists('domxml_open_mem'))
4841            throw new Exception("Old PHP4 DOM XML extension detected. phpQuery won't work until this extension is enabled.");
4842//      $id = $documentID
4843//          ? $documentID
4844//          : md5(microtime());
4845        $document = null;
4846        if ($html instanceof DOMDOCUMENT) {
4847            if (self::getDocumentID($html)) {
4848                // document already exists in phpQuery::$documents, make a copy
4849                $document = clone $html;
4850            } else {
4851                // new document, add it to phpQuery::$documents
4852                $wrapper = new DOMDocumentWrapper($html, $contentType, $documentID);
4853            }
4854        } else {
4855            $wrapper = new DOMDocumentWrapper($html, $contentType, $documentID);
4856        }
4857//      $wrapper->id = $id;
4858        // bind document
4859        phpQuery::$documents[$wrapper->id] = $wrapper;
4860        // remember last loaded document
4861        phpQuery::selectDocument($wrapper->id);
4862        return $wrapper->id;
4863    }
4864    /**
4865     * Extend class namespace.
4866     *
4867     * @param string|array $target
4868     * @param array $source
4869     * @TODO support string $source
4870     * @return unknown_type
4871     */
4872    public static function extend($target, $source) {
4873        switch($target) {
4874            case 'phpQueryObject':
4875                $targetRef = &self::$extendMethods;
4876                $targetRef2 = &self::$pluginsMethods;
4877                break;
4878            case 'phpQuery':
4879                $targetRef = &self::$extendStaticMethods;
4880                $targetRef2 = &self::$pluginsStaticMethods;
4881                break;
4882            default:
4883                throw new Exception("Unsupported \$target type");
4884        }
4885        if (is_string($source))
4886            $source = array($source => $source);
4887        foreach($source as $method => $callback) {
4888            if (isset($targetRef[$method])) {
4889//              throw new Exception
4890                self::debug("Duplicate method '{$method}', can\'t extend '{$target}'");
4891                continue;
4892            }
4893            if (isset($targetRef2[$method])) {
4894//              throw new Exception
4895                self::debug("Duplicate method '{$method}' from plugin '{$targetRef2[$method]}',"
4896                    ." can\'t extend '{$target}'");
4897                continue;
4898            }
4899            $targetRef[$method] = $callback;
4900        }
4901        return true;
4902    }
4903    /**
4904     * Extend phpQuery with $class from $file.
4905     *
4906     * @param string $class Extending class name. Real class name can be prepended phpQuery_.
4907     * @param string $file Filename to include. Defaults to "{$class}.php".
4908     */
4909    public static function plugin($class, $file = null) {
4910        // TODO $class checked agains phpQuery_$class
4911//      if (strpos($class, 'phpQuery') === 0)
4912//          $class = substr($class, 8);
4913        if (in_array($class, self::$pluginsLoaded))
4914            return true;
4915        if (! $file)
4916            $file = $class.'.php';
4917        $objectClassExists = class_exists('phpQueryObjectPlugin_'.$class);
4918        $staticClassExists = class_exists('phpQueryPlugin_'.$class);
4919        if (! $objectClassExists && ! $staticClassExists)
4920            require_once($file);
4921        self::$pluginsLoaded[] = $class;
4922        // static methods
4923        if (class_exists('phpQueryPlugin_'.$class)) {
4924            $realClass = 'phpQueryPlugin_'.$class;
4925            $vars = get_class_vars($realClass);
4926            $loop = isset($vars['phpQueryMethods'])
4927                && ! is_null($vars['phpQueryMethods'])
4928                ? $vars['phpQueryMethods']
4929                : get_class_methods($realClass);
4930            foreach($loop as $method) {
4931                if ($method == '__initialize')
4932                    continue;
4933                if (! is_callable(array($realClass, $method)))
4934                    continue;
4935                if (isset(self::$pluginsStaticMethods[$method])) {
4936                    throw new Exception("Duplicate method '{$method}' from plugin '{$c}' conflicts with same method from plugin '".self::$pluginsStaticMethods[$method]."'");
4937                    return;
4938                }
4939                self::$pluginsStaticMethods[$method] = $class;
4940            }
4941            if (method_exists($realClass, '__initialize'))
4942                call_user_func_array(array($realClass, '__initialize'), array());
4943        }
4944        // object methods
4945        if (class_exists('phpQueryObjectPlugin_'.$class)) {
4946            $realClass = 'phpQueryObjectPlugin_'.$class;
4947            $vars = get_class_vars($realClass);
4948            $loop = isset($vars['phpQueryMethods'])
4949                && ! is_null($vars['phpQueryMethods'])
4950                ? $vars['phpQueryMethods']
4951                : get_class_methods($realClass);
4952            foreach($loop as $method) {
4953                if (! is_callable(array($realClass, $method)))
4954                    continue;
4955                if (isset(self::$pluginsMethods[$method])) {
4956                    throw new Exception("Duplicate method '{$method}' from plugin '{$c}' conflicts with same method from plugin '".self::$pluginsMethods[$method]."'");
4957                    continue;
4958                }
4959                self::$pluginsMethods[$method] = $class;
4960            }
4961        }
4962        return true;
4963    }
4964    /**
4965     * Unloades all or specified document from memory.
4966     *
4967     * @param mixed $documentID @see phpQuery::getDocumentID() for supported types.
4968     */
4969    public static function unloadDocuments($id = null) {
4970        if (isset($id)) {
4971            if ($id = self::getDocumentID($id))
4972                unset(phpQuery::$documents[$id]);
4973        } else {
4974            foreach(phpQuery::$documents as $k => $v) {
4975                unset(phpQuery::$documents[$k]);
4976            }
4977        }
4978    }
4979    /**
4980     * Parses phpQuery object or HTML result against PHP tags and makes them active.
4981     *
4982     * @param phpQuery|string $content
4983     * @deprecated
4984     * @return string
4985     */
4986    public static function unsafePHPTags($content) {
4987        return self::markupToPHP($content);
4988    }
4989    public static function DOMNodeListToArray($DOMNodeList) {
4990        $array = array();
4991        if (! $DOMNodeList)
4992            return $array;
4993        foreach($DOMNodeList as $node)
4994            $array[] = $node;
4995        return $array;
4996    }
4997    /**
4998     * Checks if $input is HTML string, which has to start with '<'.
4999     *
5000     * @deprecated
5001     * @param String $input
5002     * @return Bool
5003     * @todo still used ?
5004     */
5005    public static function isMarkup($input) {
5006        return ! is_array($input) && substr(trim($input), 0, 1) == '<';
5007    }
5008    public static function debug($text) {
5009        if (self::$debug)
5010            print var_dump($text);
5011    }
5012    /**
5013     * Make an AJAX request.
5014     *
5015     * @param array See $options http://docs.jquery.com/Ajax/jQuery.ajax#toptions
5016     * Additional options are:
5017     * 'document' - document for global events, @see phpQuery::getDocumentID()
5018     * 'referer' - implemented
5019     * 'requested_with' - TODO; not implemented (X-Requested-With)
5020     * @return Zend_Http_Client
5021     * @link http://docs.jquery.com/Ajax/jQuery.ajax
5022     *
5023     * @TODO $options['cache']
5024     * @TODO $options['processData']
5025     * @TODO $options['xhr']
5026     * @TODO $options['data'] as string
5027     * @TODO XHR interface
5028     */
5029    public static function ajax($options = array(), $xhr = null) {
5030        $options = array_merge(
5031            self::$ajaxSettings, $options
5032        );
5033        $documentID = isset($options['document'])
5034            ? self::getDocumentID($options['document'])
5035            : null;
5036        if ($xhr) {
5037            // reuse existing XHR object, but clean it up
5038            $client = $xhr;
5039//          $client->setParameterPost(null);
5040//          $client->setParameterGet(null);
5041            $client->setAuth(false);
5042            $client->setHeaders("If-Modified-Since", null);
5043            $client->setHeaders("Referer", null);
5044            $client->resetParameters();
5045        } else {
5046            // create new XHR object
5047            require_once('Zend/Http/Client.php');
5048            $client = new Zend_Http_Client();
5049            $client->setCookieJar();
5050        }
5051        if (isset($options['timeout']))
5052            $client->setConfig(array(
5053                'timeout'      => $options['timeout'],
5054            ));
5055//          'maxredirects' => 0,
5056        foreach(self::$ajaxAllowedHosts as $k => $host)
5057            if ($host == '.' && isset($_SERVER['HTTP_HOST']))
5058                self::$ajaxAllowedHosts[$k] = $_SERVER['HTTP_HOST'];
5059        $host = parse_url($options['url'], PHP_URL_HOST);
5060        if (! in_array($host, self::$ajaxAllowedHosts)) {
5061            throw new Exception("Request not permitted, host '$host' not present in "
5062                ."phpQuery::\$ajaxAllowedHosts");
5063        }
5064        // JSONP
5065        $jsre = "/=\\?(&|$)/";
5066        if (isset($options['dataType']) && $options['dataType'] == 'jsonp') {
5067            $jsonpCallbackParam = $options['jsonp']
5068                ? $options['jsonp'] : 'callback';
5069            if (strtolower($options['type']) == 'get') {
5070                if (! preg_match($jsre, $options['url'])) {
5071                    $sep = strpos($options['url'], '?')
5072                        ? '&' : '?';
5073                    $options['url'] .= "$sep$jsonpCallbackParam=?";
5074                }
5075            } else if ($options['data']) {
5076                $jsonp = false;
5077                foreach($options['data'] as $n => $v) {
5078                    if ($v == '?')
5079                        $jsonp = true;
5080                }
5081                if (! $jsonp) {
5082                    $options['data'][$jsonpCallbackParam] = '?';
5083                }
5084            }
5085            $options['dataType'] = 'json';
5086        }
5087        if (isset($options['dataType']) && $options['dataType'] == 'json') {
5088            $jsonpCallback = 'json_'.md5(microtime());
5089            $jsonpData = $jsonpUrl = false;
5090            if ($options['data']) {
5091                foreach($options['data'] as $n => $v) {
5092                    if ($v == '?')
5093                        $jsonpData = $n;
5094                }
5095            }
5096            if (preg_match($jsre, $options['url']))
5097                $jsonpUrl = true;
5098            if ($jsonpData !== false || $jsonpUrl) {
5099                // remember callback name for httpData()
5100                $options['_jsonp'] = $jsonpCallback;
5101                if ($jsonpData !== false)
5102                    $options['data'][$jsonpData] = $jsonpCallback;
5103                if ($jsonpUrl)
5104                    $options['url'] = preg_replace($jsre, "=$jsonpCallback\\1", $options['url']);
5105            }
5106        }
5107        $client->setUri($options['url']);
5108        $client->setMethod(strtoupper($options['type']));
5109        if (isset($options['referer']) && $options['referer'])
5110            $client->setHeaders('Referer', $options['referer']);
5111        $client->setHeaders(array(
5112//          'content-type' => $options['contentType'],
5113            'User-Agent' => 'Mozilla/5.0 (X11; U; Linux x86; en-US; rv:1.9.0.5) Gecko'
5114                 .'/2008122010 Firefox/3.0.5',
5115            // TODO custom charset
5116            'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
5117//          'Connection' => 'keep-alive',
5118//          'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
5119            'Accept-Language' => 'en-us,en;q=0.5',
5120        ));
5121        if ($options['username'])
5122            $client->setAuth($options['username'], $options['password']);
5123        if (isset($options['ifModified']) && $options['ifModified'])
5124            $client->setHeaders("If-Modified-Since",
5125                self::$lastModified
5126                    ? self::$lastModified
5127                    : "Thu, 01 Jan 1970 00:00:00 GMT"
5128            );
5129        $client->setHeaders("Accept",
5130            isset($options['dataType'])
5131            && isset(self::$ajaxSettings['accepts'][ $options['dataType'] ])
5132                ? self::$ajaxSettings['accepts'][ $options['dataType'] ].", */*"
5133                : self::$ajaxSettings['accepts']['_default']
5134        );
5135        // TODO $options['processData']
5136        if ($options['data'] instanceof phpQueryObject) {
5137            $serialized = $options['data']->serializeArray($options['data']);
5138            $options['data'] = array();
5139            foreach($serialized as $r)
5140                $options['data'][ $r['name'] ] = $r['value'];
5141        }
5142        if (strtolower($options['type']) == 'get') {
5143            $client->setParameterGet($options['data']);
5144        } else if (strtolower($options['type']) == 'post') {
5145            $client->setEncType($options['contentType']);
5146            $client->setParameterPost($options['data']);
5147        }
5148        if (self::$active == 0 && $options['global'])
5149            phpQueryEvents::trigger($documentID, 'ajaxStart');
5150        self::$active++;
5151        // beforeSend callback
5152        if (isset($options['beforeSend']) && $options['beforeSend'])
5153            phpQuery::callbackRun($options['beforeSend'], array($client));
5154        // ajaxSend event
5155        if ($options['global'])
5156            phpQueryEvents::trigger($documentID, 'ajaxSend', array($client, $options));
5157        if (phpQuery::$debug) {
5158            self::debug("{$options['type']}: {$options['url']}\n");
5159            self::debug("Options: <pre>".var_export($options, true)."</pre>\n");
5160//          if ($client->getCookieJar())
5161//              self::debug("Cookies: <pre>".var_export($client->getCookieJar()->getMatchingCookies($options['url']), true)."</pre>\n");
5162        }
5163        // request
5164        $response = $client->request();
5165        if (phpQuery::$debug) {
5166            self::debug('Status: '.$response->getStatus().' / '.$response->getMessage());
5167            self::debug($client->getLastRequest());
5168            self::debug($response->getHeaders());
5169        }
5170        if ($response->isSuccessful()) {
5171            // XXX tempolary
5172            self::$lastModified = $response->getHeader('Last-Modified');
5173            $data = self::httpData($response->getBody(), $options['dataType'], $options);
5174            if (isset($options['success']) && $options['success'])
5175                phpQuery::callbackRun($options['success'], array($data, $response->getStatus(), $options));
5176            if ($options['global'])
5177                phpQueryEvents::trigger($documentID, 'ajaxSuccess', array($client, $options));
5178        } else {
5179            if (isset($options['error']) && $options['error'])
5180                phpQuery::callbackRun($options['error'], array($client, $response->getStatus(), $response->getMessage()));
5181            if ($options['global'])
5182                phpQueryEvents::trigger($documentID, 'ajaxError', array($client, /*$response->getStatus(),*/$response->getMessage(), $options));
5183        }
5184        if (isset($options['complete']) && $options['complete'])
5185            phpQuery::callbackRun($options['complete'], array($client, $response->getStatus()));
5186        if ($options['global'])
5187            phpQueryEvents::trigger($documentID, 'ajaxComplete', array($client, $options));
5188        if ($options['global'] && ! --self::$active)
5189            phpQueryEvents::trigger($documentID, 'ajaxStop');
5190        return $client;
5191//      if (is_null($domId))
5192//          $domId = self::$defaultDocumentID ? self::$defaultDocumentID : false;
5193//      return new phpQueryAjaxResponse($response, $domId);
5194    }
5195    protected static function httpData($data, $type, $options) {
5196        if (isset($options['dataFilter']) && $options['dataFilter'])
5197            $data = self::callbackRun($options['dataFilter'], array($data, $type));
5198        if (is_string($data)) {
5199            if ($type == "json") {
5200                if (isset($options['_jsonp']) && $options['_jsonp']) {
5201                    $data = preg_replace('/^\s*\w+\((.*)\)\s*$/s', '$1', $data);
5202                }
5203                $data = self::parseJSON($data);
5204            }
5205        }
5206        return $data;
5207    }
5208    /**
5209     * Enter description here...
5210     *
5211     * @param array|phpQuery $data
5212     *
5213     */
5214    public static function param($data) {
5215        return http_build_query($data, null, '&');
5216    }
5217    public static function get($url, $data = null, $callback = null, $type = null) {
5218        if (!is_array($data)) {
5219            $callback = $data;
5220            $data = null;
5221        }
5222        // TODO some array_values on this shit
5223        return phpQuery::ajax(array(
5224            'type' => 'GET',
5225            'url' => $url,
5226            'data' => $data,
5227            'success' => $callback,
5228            'dataType' => $type,
5229        ));
5230    }
5231    public static function post($url, $data = null, $callback = null, $type = null) {
5232        if (!is_array($data)) {
5233            $callback = $data;
5234            $data = null;
5235        }
5236        return phpQuery::ajax(array(
5237            'type' => 'POST',
5238            'url' => $url,
5239            'data' => $data,
5240            'success' => $callback,
5241            'dataType' => $type,
5242        ));
5243    }
5244    public static function getJSON($url, $data = null, $callback = null) {
5245        if (!is_array($data)) {
5246            $callback = $data;
5247            $data = null;
5248        }
5249        // TODO some array_values on this shit
5250        return phpQuery::ajax(array(
5251            'type' => 'GET',
5252            'url' => $url,
5253            'data' => $data,
5254            'success' => $callback,
5255            'dataType' => 'json',
5256        ));
5257    }
5258    public static function ajaxSetup($options) {
5259        self::$ajaxSettings = array_merge(
5260            self::$ajaxSettings,
5261            $options
5262        );
5263    }
5264    public static function ajaxAllowHost($host1, $host2 = null, $host3 = null) {
5265        $loop = is_array($host1)
5266            ? $host1
5267            : func_get_args();
5268        foreach($loop as $host) {
5269            if ($host && ! in_array($host, phpQuery::$ajaxAllowedHosts)) {
5270                phpQuery::$ajaxAllowedHosts[] = $host;
5271            }
5272        }
5273    }
5274    public static function ajaxAllowURL($url1, $url2 = null, $url3 = null) {
5275        $loop = is_array($url1)
5276            ? $url1
5277            : func_get_args();
5278        foreach($loop as $url)
5279            phpQuery::ajaxAllowHost(parse_url($url, PHP_URL_HOST));
5280    }
5281    /**
5282     * Returns JSON representation of $data.
5283     *
5284     * @static
5285     * @param mixed $data
5286     * @return string
5287     */
5288    public static function toJSON($data) {
5289        if (function_exists('json_encode'))
5290            return json_encode($data);
5291        require_once('Zend/Json/Encoder.php');
5292        return Zend_Json_Encoder::encode($data);
5293    }
5294    /**
5295     * Parses JSON into proper PHP type.
5296     *
5297     * @static
5298     * @param string $json
5299     * @return mixed
5300     */
5301    public static function parseJSON($json) {
5302        if (function_exists('json_decode')) {
5303            $return = json_decode(trim($json), true);
5304            // json_decode and UTF8 issues
5305            if (isset($return))
5306                return $return;
5307        }
5308        require_once('Zend/Json/Decoder.php');
5309        return Zend_Json_Decoder::decode($json);
5310    }
5311    /**
5312     * Returns source's document ID.
5313     *
5314     * @param $source DOMNode|phpQueryObject
5315     * @return string
5316     */
5317    public static function getDocumentID($source) {
5318        if ($source instanceof DOMDOCUMENT) {
5319            foreach(phpQuery::$documents as $id => $document) {
5320                if ($source->isSameNode($document->document))
5321                    return $id;
5322            }
5323        } else if ($source instanceof DOMNODE) {
5324            foreach(phpQuery::$documents as $id => $document) {
5325                if ($source->ownerDocument->isSameNode($document->document))
5326                    return $id;
5327            }
5328        } else if ($source instanceof phpQueryObject)
5329            return $source->getDocumentID();
5330        else if (is_string($source) && isset(phpQuery::$documents[$source]))
5331            return $source;
5332    }
5333    /**
5334     * Get DOMDocument object related to $source.
5335     * Returns null if such document doesn't exist.
5336     *
5337     * @param $source DOMNode|phpQueryObject|string
5338     * @return string
5339     */
5340    public static function getDOMDocument($source) {
5341        if ($source instanceof DOMDOCUMENT)
5342            return $source;
5343        $source = self::getDocumentID($source);
5344        return $source
5345            ? self::$documents[$id]['document']
5346            : null;
5347    }
5348
5349    // UTILITIES
5350    // http://docs.jquery.com/Utilities
5351
5352    /**
5353     *
5354     * @return unknown_type
5355     * @link http://docs.jquery.com/Utilities/jQuery.makeArray
5356     */
5357    public static function makeArray($obj) {
5358        $array = array();
5359        if (is_object($object) && $object instanceof DOMNODELIST) {
5360            foreach($object as $value)
5361                $array[] = $value;
5362        } else if (is_object($object) && ! ($object instanceof Iterator)) {
5363            foreach(get_object_vars($object) as $name => $value)
5364                $array[0][$name] = $value;
5365        } else {
5366            foreach($object as $name => $value)
5367                $array[0][$name] = $value;
5368        }
5369        return $array;
5370    }
5371    public static function inArray($value, $array) {
5372        return in_array($value, $array);
5373    }
5374    /**
5375     *
5376     * @param $object
5377     * @param $callback
5378     * @return unknown_type
5379     * @link http://docs.jquery.com/Utilities/jQuery.each
5380     */
5381    public static function each($object, $callback, $param1 = null, $param2 = null, $param3 = null) {
5382        $paramStructure = null;
5383        if (func_num_args() > 2) {
5384            $paramStructure = func_get_args();
5385            $paramStructure = array_slice($paramStructure, 2);
5386        }
5387        if (is_object($object) && ! ($object instanceof Iterator)) {
5388            foreach(get_object_vars($object) as $name => $value)
5389                phpQuery::callbackRun($callback, array($name, $value), $paramStructure);
5390        } else {
5391            foreach($object as $name => $value)
5392                phpQuery::callbackRun($callback, array($name, $value), $paramStructure);
5393        }
5394    }
5395    /**
5396     *
5397     * @link http://docs.jquery.com/Utilities/jQuery.map
5398     */
5399    public static function map($array, $callback, $param1 = null, $param2 = null, $param3 = null) {
5400        $result = array();
5401        $paramStructure = null;
5402        if (func_num_args() > 2) {
5403            $paramStructure = func_get_args();
5404            $paramStructure = array_slice($paramStructure, 2);
5405        }
5406        foreach($array as $v) {
5407            $vv = phpQuery::callbackRun($callback, array($v), $paramStructure);
5408//          $callbackArgs = $args;
5409//          foreach($args as $i => $arg) {
5410//              $callbackArgs[$i] = $arg instanceof CallbackParam
5411//                  ? $v
5412//                  : $arg;
5413//          }
5414//          $vv = call_user_func_array($callback, $callbackArgs);
5415            if (is_array($vv))  {
5416                foreach($vv as $vvv)
5417                    $result[] = $vvv;
5418            } else if ($vv !== null) {
5419                $result[] = $vv;
5420            }
5421        }
5422        return $result;
5423    }
5424    /**
5425     *
5426     * @param $callback Callback
5427     * @param $params
5428     * @param $paramStructure
5429     * @return unknown_type
5430     */
5431    public static function callbackRun($callback, $params = array(), $paramStructure = null) {
5432        if (! $callback)
5433            return;
5434        if ($callback instanceof CallbackParameterToReference) {
5435            // TODO support ParamStructure to select which $param push to reference
5436            if (isset($params[0]))
5437                $callback->callback = $params[0];
5438            return true;
5439        }
5440        if ($callback instanceof Callback) {
5441            $paramStructure = $callback->params;
5442            $callback = $callback->callback;
5443        }
5444        if (! $paramStructure)
5445            return call_user_func_array($callback, $params);
5446        $p = 0;
5447        foreach($paramStructure as $i => $v) {
5448            $paramStructure[$i] = $v instanceof CallbackParam
5449                ? $params[$p++]
5450                : $v;
5451        }
5452        return call_user_func_array($callback, $paramStructure);
5453    }
5454    /**
5455     * Merge 2 phpQuery objects.
5456     * @param array $one
5457     * @param array $two
5458     * @protected
5459     * @todo node lists, phpQueryObject
5460     */
5461    public static function merge($one, $two) {
5462        $elements = $one->elements;
5463        foreach($two->elements as $node) {
5464            $exists = false;
5465            foreach($elements as $node2) {
5466                if ($node2->isSameNode($node))
5467                    $exists = true;
5468            }
5469            if (! $exists)
5470                $elements[] = $node;
5471        }
5472        return $elements;
5473//      $one = $one->newInstance();
5474//      $one->elements = $elements;
5475//      return $one;
5476    }
5477    /**
5478     *
5479     * @param $array
5480     * @param $callback
5481     * @param $invert
5482     * @return unknown_type
5483     * @link http://docs.jquery.com/Utilities/jQuery.grep
5484     */
5485    public static function grep($array, $callback, $invert = false) {
5486        $result = array();
5487        foreach($array as $k => $v) {
5488            $r = call_user_func_array($callback, array($v, $k));
5489            if ($r === !(bool)$invert)
5490                $result[] = $v;
5491        }
5492        return $result;
5493    }
5494    public static function unique($array) {
5495        return array_unique($array);
5496    }
5497    /**
5498     *
5499     * @param $function
5500     * @return unknown_type
5501     * @TODO there are problems with non-static methods, second parameter pass it
5502     *  but doesnt verify is method is really callable
5503     */
5504    public static function isFunction($function) {
5505        return is_callable($function);
5506    }
5507    public static function trim($str) {
5508        return trim($str);
5509    }
5510    /* PLUGINS NAMESPACE */
5511    /**
5512     *
5513     * @param $url
5514     * @param $callback
5515     * @param $param1
5516     * @param $param2
5517     * @param $param3
5518     * @return phpQueryObject
5519     */
5520    public static function browserGet($url, $callback, $param1 = null, $param2 = null, $param3 = null) {
5521        if (self::plugin('WebBrowser')) {
5522            $params = func_get_args();
5523            return self::callbackRun(array(self::$plugins, 'browserGet'), $params);
5524        } else {
5525            self::debug('WebBrowser plugin not available...');
5526        }
5527    }
5528    /**
5529     *
5530     * @param $url
5531     * @param $data
5532     * @param $callback
5533     * @param $param1
5534     * @param $param2
5535     * @param $param3
5536     * @return phpQueryObject
5537     */
5538    public static function browserPost($url, $data, $callback, $param1 = null, $param2 = null, $param3 = null) {
5539        if (self::plugin('WebBrowser')) {
5540            $params = func_get_args();
5541            return self::callbackRun(array(self::$plugins, 'browserPost'), $params);
5542        } else {
5543            self::debug('WebBrowser plugin not available...');
5544        }
5545    }
5546    /**
5547     *
5548     * @param $ajaxSettings
5549     * @param $callback
5550     * @param $param1
5551     * @param $param2
5552     * @param $param3
5553     * @return phpQueryObject
5554     */
5555    public static function browser($ajaxSettings, $callback, $param1 = null, $param2 = null, $param3 = null) {
5556        if (self::plugin('WebBrowser')) {
5557            $params = func_get_args();
5558            return self::callbackRun(array(self::$plugins, 'browser'), $params);
5559        } else {
5560            self::debug('WebBrowser plugin not available...');
5561        }
5562    }
5563    /**
5564     *
5565     * @param $code
5566     * @return string
5567     */
5568    public static function php($code) {
5569        return self::code('php', $code);
5570    }
5571    /**
5572     *
5573     * @param $type
5574     * @param $code
5575     * @return string
5576     */
5577    public static function code($type, $code) {
5578        return "<$type><!-- ".trim($code)." --></$type>";
5579    }
5580
5581    public static function __callStatic($method, $params) {
5582        return call_user_func_array(
5583            array(phpQuery::$plugins, $method),
5584            $params
5585        );
5586    }
5587    protected static function dataSetupNode($node, $documentID) {
5588        // search are return if alredy exists
5589        foreach(phpQuery::$documents[$documentID]->dataNodes as $dataNode) {
5590            if ($node->isSameNode($dataNode))
5591                return $dataNode;
5592        }
5593        // if doesn't, add it
5594        phpQuery::$documents[$documentID]->dataNodes[] = $node;
5595        return $node;
5596    }
5597    protected static function dataRemoveNode($node, $documentID) {
5598        // search are return if alredy exists
5599        foreach(phpQuery::$documents[$documentID]->dataNodes as $k => $dataNode) {
5600            if ($node->isSameNode($dataNode)) {
5601                unset(self::$documents[$documentID]->dataNodes[$k]);
5602                unset(self::$documents[$documentID]->data[ $dataNode->dataID ]);
5603            }
5604        }
5605    }
5606    public static function data($node, $name, $data, $documentID = null) {
5607        if (! $documentID)
5608            // TODO check if this works
5609            $documentID = self::getDocumentID($node);
5610        $document = phpQuery::$documents[$documentID];
5611        $node = self::dataSetupNode($node, $documentID);
5612        if (! isset($node->dataID))
5613            $node->dataID = ++phpQuery::$documents[$documentID]->uuid;
5614        $id = $node->dataID;
5615        if (! isset($document->data[$id]))
5616            $document->data[$id] = array();
5617        if (! is_null($data))
5618            $document->data[$id][$name] = $data;
5619        if ($name) {
5620            if (isset($document->data[$id][$name]))
5621                return $document->data[$id][$name];
5622        } else
5623            return $id;
5624    }
5625    public static function removeData($node, $name, $documentID) {
5626        if (! $documentID)
5627            // TODO check if this works
5628            $documentID = self::getDocumentID($node);
5629        $document = phpQuery::$documents[$documentID];
5630        $node = self::dataSetupNode($node, $documentID);
5631        $id = $node->dataID;
5632        if ($name) {
5633            if (isset($document->data[$id][$name]))
5634                unset($document->data[$id][$name]);
5635            $name = null;
5636            foreach($document->data[$id] as $name)
5637                break;
5638            if (! $name)
5639                self::removeData($node, $name, $documentID);
5640        } else {
5641            self::dataRemoveNode($node, $documentID);
5642        }
5643    }
5644}
5645/**
5646 * Plugins static namespace class.
5647 *
5648 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
5649 * @package phpQuery
5650 * @todo move plugin methods here (as statics)
5651 */
5652class phpQueryPlugins {
5653    public function __call($method, $args) {
5654        if (isset(phpQuery::$extendStaticMethods[$method])) {
5655            $return = call_user_func_array(
5656                phpQuery::$extendStaticMethods[$method],
5657                $args
5658            );
5659        } else if (isset(phpQuery::$pluginsStaticMethods[$method])) {
5660            $class = phpQuery::$pluginsStaticMethods[$method];
5661            $realClass = "phpQueryPlugin_$class";
5662            $return = call_user_func_array(
5663                array($realClass, $method),
5664                $args
5665            );
5666            return isset($return)
5667                ? $return
5668                : $this;
5669        } else
5670            throw new Exception("Method '{$method}' doesnt exist");
5671    }
5672}
5673/**
5674 * Shortcut to phpQuery::pq($arg1, $context)
5675 * Chainable.
5676 *
5677 * @see phpQuery::pq()
5678 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
5679 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
5680 * @package phpQuery
5681 */
5682function pq($arg1, $context = null) {
5683    $args = func_get_args();
5684    return call_user_func_array(
5685        array('phpQuery', 'pq'),
5686        $args
5687    );
5688}
5689// add plugins dir and Zend framework to include path
5690set_include_path(
5691    get_include_path()
5692        .PATH_SEPARATOR.dirname(__FILE__).'/phpQuery/'
5693        .PATH_SEPARATOR.dirname(__FILE__).'/phpQuery/plugins/'
5694);
5695// why ? no __call nor __get for statics in php...
5696// XXX __callStatic will be available in PHP 5.3
5697phpQuery::$plugins = new phpQueryPlugins();
5698// include bootstrap file (personal library config)
5699if (file_exists(dirname(__FILE__).'/phpQuery/bootstrap.php'))
5700    require_once dirname(__FILE__).'/phpQuery/bootstrap.php';
5701