[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/_test/core/ -> phpQuery-onefile.php (source)

   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   * @deprecated 2022-10-19
  14   */
  15  
  16  // class names for instanceof
  17  // TODO move them as class constants into phpQuery
  18  define('DOMDOCUMENT', 'DOMDocument');
  19  define('DOMELEMENT', 'DOMElement');
  20  define('DOMNODELIST', 'DOMNodeList');
  21  define('DOMNODE', 'DOMNode');
  22  
  23  /**
  24   * DOMEvent class.
  25   *
  26   * Based on
  27   * @link http://developer.mozilla.org/En/DOM:event
  28   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
  29   * @package phpQuery
  30   * @todo implement ArrayAccess ?
  31   */
  32  class DOMEvent {
  33      /**
  34       * Returns a boolean indicating whether the event bubbles up through the DOM or not.
  35       *
  36       * @var unknown_type
  37       */
  38      public $bubbles = true;
  39      /**
  40       * Returns a boolean indicating whether the event is cancelable.
  41       *
  42       * @var unknown_type
  43       */
  44      public $cancelable = true;
  45      /**
  46       * Returns a reference to the currently registered target for the event.
  47       *
  48       * @var unknown_type
  49       */
  50      public $currentTarget;
  51      /**
  52       * Returns detail about the event, depending on the type of event.
  53       *
  54       * @var unknown_type
  55       * @link http://developer.mozilla.org/en/DOM/event.detail
  56       */
  57      public $detail; // ???
  58      /**
  59       * Used to indicate which phase of the event flow is currently being evaluated.
  60       *
  61       * NOT IMPLEMENTED
  62       *
  63       * @var unknown_type
  64       * @link http://developer.mozilla.org/en/DOM/event.eventPhase
  65       */
  66      public $eventPhase; // ???
  67      /**
  68       * The explicit original target of the event (Mozilla-specific).
  69       *
  70       * NOT IMPLEMENTED
  71       *
  72       * @var unknown_type
  73       */
  74      public $explicitOriginalTarget; // moz only
  75      /**
  76       * The original target of the event, before any retargetings (Mozilla-specific).
  77       *
  78       * NOT IMPLEMENTED
  79       *
  80       * @var unknown_type
  81       */
  82      public $originalTarget; // moz only
  83      /**
  84       * Identifies a secondary target for the event.
  85       *
  86       * @var unknown_type
  87       */
  88      public $relatedTarget;
  89      /**
  90       * Returns a reference to the target to which the event was originally dispatched.
  91       *
  92       * @var unknown_type
  93       */
  94      public $target;
  95      /**
  96       * Returns the time that the event was created.
  97       *
  98       * @var unknown_type
  99       */
 100      public $timeStamp;
 101      /**
 102       * Returns the name of the event (case-insensitive).
 103       */
 104      public $type;
 105      public $runDefault = true;
 106      public $data = null;
 107      public function __construct($data) {
 108          foreach($data as $k => $v) {
 109              $this->$k = $v;
 110          }
 111          if (! $this->timeStamp)
 112              $this->timeStamp = time();
 113      }
 114      /**
 115       * Cancels the event (if it is cancelable).
 116       *
 117       */
 118      public function preventDefault() {
 119          $this->runDefault = false;
 120      }
 121      /**
 122       * Stops the propagation of events further along in the DOM.
 123       *
 124       */
 125      public function stopPropagation() {
 126          $this->bubbles = false;
 127      }
 128  }
 129  
 130  
 131  /**
 132   * DOMDocumentWrapper class simplifies work with DOMDocument.
 133   *
 134   * Know bug:
 135   * - in XHTML fragments, <br /> changes to <br clear="none" />
 136   *
 137   * @todo check XML catalogs compatibility
 138   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 139   * @package phpQuery
 140   */
 141  class DOMDocumentWrapper {
 142      /**
 143       * @var DOMDocument
 144       */
 145      public $document;
 146      public $id;
 147      /**
 148       * @todo Rewrite as method and quess if null.
 149       * @var unknown_type
 150       */
 151      public $contentType = '';
 152      public $xpath;
 153      public $uuid = 0;
 154      public $data = array();
 155      public $dataNodes = array();
 156      public $events = array();
 157      public $eventsNodes = array();
 158      public $eventsGlobal = array();
 159      /**
 160       * @TODO iframes support http://code.google.com/p/phpquery/issues/detail?id=28
 161       * @var unknown_type
 162       */
 163      public $frames = array();
 164      /**
 165       * Document root, by default equals to document itself.
 166       * Used by documentFragments.
 167       *
 168       * @var DOMNode
 169       */
 170      public $root;
 171      public $isDocumentFragment;
 172      public $isXML = false;
 173      public $isXHTML = false;
 174      public $isHTML = false;
 175      public $charset;
 176      public function __construct($markup = null, $contentType = null, $newDocumentID = null) {
 177          if (isset($markup))
 178              $this->load($markup, $contentType, $newDocumentID);
 179          $this->id = $newDocumentID
 180              ? $newDocumentID
 181              : md5(microtime());
 182      }
 183      public function load($markup, $contentType = null, $newDocumentID = null) {
 184  //      phpQuery::$documents[$id] = $this;
 185          $this->contentType = strtolower($contentType);
 186          if ($markup instanceof DOMDOCUMENT) {
 187              $this->document = $markup;
 188              $this->root = $this->document;
 189              $this->charset = $this->document->encoding;
 190              // TODO isDocumentFragment
 191          } else {
 192              $loaded = $this->loadMarkup($markup);
 193          }
 194          if ($loaded) {
 195  //          $this->document->formatOutput = true;
 196              $this->document->preserveWhiteSpace = true;
 197              $this->xpath = new DOMXPath($this->document);
 198              $this->afterMarkupLoad();
 199              return true;
 200              // remember last loaded document
 201  //          return phpQuery::selectDocument($id);
 202          }
 203          return false;
 204      }
 205      protected function afterMarkupLoad() {
 206          if ($this->isXHTML) {
 207              $this->xpath->registerNamespace("html", "http://www.w3.org/1999/xhtml");
 208          }
 209      }
 210      protected function loadMarkup($markup) {
 211          $loaded = false;
 212          if ($this->contentType) {
 213              self::debug("Load markup for content type {$this->contentType}");
 214              // content determined by contentType
 215              list($contentType, $charset) = $this->contentTypeToArray($this->contentType);
 216              switch($contentType) {
 217                  case 'text/html':
 218                      phpQuery::debug("Loading HTML, content type '{$this->contentType}'");
 219                      $loaded = $this->loadMarkupHTML($markup, $charset);
 220                  break;
 221                  case 'text/xml':
 222                  case 'application/xhtml+xml':
 223                      phpQuery::debug("Loading XML, content type '{$this->contentType}'");
 224                      $loaded = $this->loadMarkupXML($markup, $charset);
 225                  break;
 226                  default:
 227                      // for feeds or anything that sometimes doesn't use text/xml
 228                      if (strpos('xml', $this->contentType) !== false) {
 229                          phpQuery::debug("Loading XML, content type '{$this->contentType}'");
 230                          $loaded = $this->loadMarkupXML($markup, $charset);
 231                      } else
 232                          phpQuery::debug("Could not determine document type from content type '{$this->contentType}'");
 233              }
 234          } else {
 235              // content type autodetection
 236              if ($this->isXML($markup)) {
 237                  phpQuery::debug("Loading XML, isXML() == true");
 238                  $loaded = $this->loadMarkupXML($markup);
 239                  if (! $loaded && $this->isXHTML) {
 240                      phpQuery::debug('Loading as XML failed, trying to load as HTML, isXHTML == true');
 241                      $loaded = $this->loadMarkupHTML($markup);
 242                  }
 243              } else {
 244                  phpQuery::debug("Loading HTML, isXML() == false");
 245                  $loaded = $this->loadMarkupHTML($markup);
 246              }
 247          }
 248          return $loaded;
 249      }
 250      protected function loadMarkupReset() {
 251          $this->isXML = $this->isXHTML = $this->isHTML = false;
 252      }
 253      protected function documentCreate($charset, $version = '1.0') {
 254          if (! $version)
 255              $version = '1.0';
 256          $this->document = new DOMDocument($version, $charset);
 257          $this->charset = $this->document->encoding;
 258  //      $this->document->encoding = $charset;
 259          $this->document->formatOutput = true;
 260          $this->document->preserveWhiteSpace = true;
 261      }
 262      protected function loadMarkupHTML($markup, $requestedCharset = null) {
 263          if (phpQuery::$debug)
 264              phpQuery::debug('Full markup load (HTML): '.substr($markup, 0, 250));
 265          $this->loadMarkupReset();
 266          $this->isHTML = true;
 267          if (!isset($this->isDocumentFragment))
 268              $this->isDocumentFragment = self::isDocumentFragmentHTML($markup);
 269          $charset = null;
 270          $documentCharset = $this->charsetFromHTML($markup);
 271          $addDocumentCharset = false;
 272          if ($documentCharset) {
 273              $charset = $documentCharset;
 274              $markup = $this->charsetFixHTML($markup);
 275          } else if ($requestedCharset) {
 276              $charset = $requestedCharset;
 277          }
 278          if (! $charset)
 279              $charset = phpQuery::$defaultCharset;
 280          // HTTP 1.1 says that the default charset is ISO-8859-1
 281          // @see http://www.w3.org/International/O-HTTP-charset
 282          if (! $documentCharset) {
 283              $documentCharset = 'ISO-8859-1';
 284              $addDocumentCharset = true;
 285          }
 286          // Should be careful here, still need 'magic encoding detection' since lots of pages have other 'default encoding'
 287          // Worse, some pages can have mixed encodings... we'll try not to worry about that
 288          $requestedCharset = strtoupper($requestedCharset);
 289          $documentCharset = strtoupper($documentCharset);
 290          phpQuery::debug("DOC: $documentCharset REQ: $requestedCharset");
 291          if ($requestedCharset && $documentCharset && $requestedCharset !== $documentCharset) {
 292              phpQuery::debug("CHARSET CONVERT");
 293              // Document Encoding Conversion
 294              // http://code.google.com/p/phpquery/issues/detail?id=86
 295              if (function_exists('mb_detect_encoding')) {
 296                  $possibleCharsets = array($documentCharset, $requestedCharset, 'AUTO');
 297                  $docEncoding = mb_detect_encoding($markup, implode(', ', $possibleCharsets));
 298                  if (! $docEncoding)
 299                      $docEncoding = $documentCharset; // ok trust the document
 300                  phpQuery::debug("DETECTED '$docEncoding'");
 301                  // Detected does not match what document says...
 302                  if ($docEncoding !== $documentCharset) {
 303                      // Tricky..
 304                  }
 305                  if ($docEncoding !== $requestedCharset) {
 306                      phpQuery::debug("CONVERT $docEncoding => $requestedCharset");
 307                      $markup = mb_convert_encoding($markup, $requestedCharset, $docEncoding);
 308                      $markup = $this->charsetAppendToHTML($markup, $requestedCharset);
 309                      $charset = $requestedCharset;
 310                  }
 311              } else {
 312                  phpQuery::debug("TODO: charset conversion without mbstring...");
 313              }
 314          }
 315          $return = false;
 316          if ($this->isDocumentFragment) {
 317              phpQuery::debug("Full markup load (HTML), DocumentFragment detected, using charset '$charset'");
 318              $return = $this->documentFragmentLoadMarkup($this, $charset, $markup);
 319          } else {
 320              if ($addDocumentCharset) {
 321                  phpQuery::debug("Full markup load (HTML), appending charset: '$charset'");
 322                  $markup = $this->charsetAppendToHTML($markup, $charset);
 323              }
 324              phpQuery::debug("Full markup load (HTML), documentCreate('$charset')");
 325              $this->documentCreate($charset);
 326              $return = phpQuery::$debug === 2
 327                  ? $this->document->loadHTML($markup)
 328                  : @$this->document->loadHTML($markup);
 329              if ($return)
 330                  $this->root = $this->document;
 331          }
 332          if ($return && ! $this->contentType)
 333              $this->contentType = 'text/html';
 334          return $return;
 335      }
 336      protected function loadMarkupXML($markup, $requestedCharset = null) {
 337          if (phpQuery::$debug)
 338              phpQuery::debug('Full markup load (XML): '.substr($markup, 0, 250));
 339          $this->loadMarkupReset();
 340          $this->isXML = true;
 341          // check agains XHTML in contentType or markup
 342          $isContentTypeXHTML = $this->isXHTML();
 343          $isMarkupXHTML = $this->isXHTML($markup);
 344          if ($isContentTypeXHTML || $isMarkupXHTML) {
 345              self::debug('Full markup load (XML), XHTML detected');
 346              $this->isXHTML = true;
 347          }
 348          // determine document fragment
 349          if (! isset($this->isDocumentFragment))
 350              $this->isDocumentFragment = $this->isXHTML
 351                  ? self::isDocumentFragmentXHTML($markup)
 352                  : self::isDocumentFragmentXML($markup);
 353          // this charset will be used
 354          $charset = null;
 355          // charset from XML declaration @var string
 356          $documentCharset = $this->charsetFromXML($markup);
 357          if (! $documentCharset) {
 358              if ($this->isXHTML) {
 359                  // this is XHTML, try to get charset from content-type meta header
 360                  $documentCharset = $this->charsetFromHTML($markup);
 361                  if ($documentCharset) {
 362                      phpQuery::debug("Full markup load (XML), appending XHTML charset '$documentCharset'");
 363                      $this->charsetAppendToXML($markup, $documentCharset);
 364                      $charset = $documentCharset;
 365                  }
 366              }
 367              if (! $documentCharset) {
 368                  // if still no document charset...
 369                  $charset = $requestedCharset;
 370              }
 371          } else if ($requestedCharset) {
 372              $charset = $requestedCharset;
 373          }
 374          if (! $charset) {
 375              $charset = phpQuery::$defaultCharset;
 376          }
 377          if ($requestedCharset && $documentCharset && $requestedCharset != $documentCharset) {
 378              // TODO place for charset conversion
 379  //          $charset = $requestedCharset;
 380          }
 381          $return = false;
 382          if ($this->isDocumentFragment) {
 383              phpQuery::debug("Full markup load (XML), DocumentFragment detected, using charset '$charset'");
 384              $return = $this->documentFragmentLoadMarkup($this, $charset, $markup);
 385          } else {
 386              // FIXME ???
 387              if ($isContentTypeXHTML && ! $isMarkupXHTML)
 388              if (! $documentCharset) {
 389                  phpQuery::debug("Full markup load (XML), appending charset '$charset'");
 390                  $markup = $this->charsetAppendToXML($markup, $charset);
 391              }
 392              // see http://php.net/manual/en/book.dom.php#78929
 393              // LIBXML_DTDLOAD (>= PHP 5.1)
 394              // does XML ctalogues works with LIBXML_NONET
 395      //      $this->document->resolveExternals = true;
 396              // TODO test LIBXML_COMPACT for performance improvement
 397              // create document
 398              $this->documentCreate($charset);
 399              if (phpversion() < 5.1) {
 400                  $this->document->resolveExternals = true;
 401                  $return = phpQuery::$debug === 2
 402                      ? $this->document->loadXML($markup)
 403                      : @$this->document->loadXML($markup);
 404              } else {
 405                  /** @link http://php.net/manual/en/libxml.constants.php */
 406                  $libxmlStatic = phpQuery::$debug === 2
 407                      ? LIBXML_DTDLOAD|LIBXML_DTDATTR|LIBXML_NONET
 408                      : LIBXML_DTDLOAD|LIBXML_DTDATTR|LIBXML_NONET|LIBXML_NOWARNING|LIBXML_NOERROR;
 409                  $return = $this->document->loadXML($markup, $libxmlStatic);
 410  //              if (! $return)
 411  //                  $return = $this->document->loadHTML($markup);
 412              }
 413              if ($return)
 414                  $this->root = $this->document;
 415          }
 416          if ($return) {
 417              if (! $this->contentType) {
 418                  if ($this->isXHTML)
 419                      $this->contentType = 'application/xhtml+xml';
 420                  else
 421                      $this->contentType = 'text/xml';
 422              }
 423              return $return;
 424          } else {
 425              throw new Exception("Error loading XML markup");
 426          }
 427      }
 428      protected function isXHTML($markup = null) {
 429          if (! isset($markup)) {
 430              return strpos($this->contentType, 'xhtml') !== false;
 431          }
 432          // XXX ok ?
 433          return strpos($markup, "<!DOCTYPE html") !== false;
 434  //      return stripos($doctype, 'xhtml') !== false;
 435  //      $doctype = isset($dom->doctype) && is_object($dom->doctype)
 436  //          ? $dom->doctype->publicId
 437  //          : self::$defaultDoctype;
 438      }
 439      protected function isXML($markup) {
 440  //      return strpos($markup, '<?xml') !== false && stripos($markup, 'xhtml') === false;
 441          return strpos(substr($markup, 0, 100), '<'.'?xml') !== false;
 442      }
 443      protected function contentTypeToArray($contentType) {
 444          $matches = explode(';', trim(strtolower($contentType)));
 445          if (isset($matches[1])) {
 446              $matches[1] = explode('=', $matches[1]);
 447              // strip 'charset='
 448              $matches[1] = isset($matches[1][1]) && trim($matches[1][1])
 449                  ? $matches[1][1]
 450                  : $matches[1][0];
 451          } else
 452              $matches[1] = null;
 453          return $matches;
 454      }
 455      /**
 456       *
 457       * @param $markup
 458       * @return array contentType, charset
 459       */
 460      protected function contentTypeFromHTML($markup) {
 461          $matches = array();
 462          // find meta tag
 463          preg_match('@<meta[^>]+http-equiv\\s*=\\s*(["|\'])Content-Type\\1([^>]+?)>@i',
 464              $markup, $matches
 465          );
 466          if (! isset($matches[0]))
 467              return array(null, null);
 468          // get attr 'content'
 469          preg_match('@content\\s*=\\s*(["|\'])(.+?)\\1@', $matches[0], $matches);
 470          if (! isset($matches[0]))
 471              return array(null, null);
 472          return $this->contentTypeToArray($matches[2]);
 473      }
 474      protected function charsetFromHTML($markup) {
 475          $contentType = $this->contentTypeFromHTML($markup);
 476          return $contentType[1];
 477      }
 478      protected function charsetFromXML($markup) {
 479          $matches;
 480          // find declaration
 481          preg_match('@<'.'?xml[^>]+encoding\\s*=\\s*(["|\'])(.*?)\\1@i',
 482              $markup, $matches
 483          );
 484          return isset($matches[2])
 485              ? strtolower($matches[2])
 486              : null;
 487      }
 488      /**
 489       * Repositions meta[type=charset] at the start of head. Bypasses DOMDocument bug.
 490       *
 491       * @link http://code.google.com/p/phpquery/issues/detail?id=80
 492       * @param $html
 493       */
 494      protected function charsetFixHTML($markup) {
 495          $matches = array();
 496          // find meta tag
 497          preg_match('@\s*<meta[^>]+http-equiv\\s*=\\s*(["|\'])Content-Type\\1([^>]+?)>@i',
 498              $markup, $matches, PREG_OFFSET_CAPTURE
 499          );
 500          if (! isset($matches[0]))
 501              return;
 502          $metaContentType = $matches[0][0];
 503          $markup = substr($markup, 0, $matches[0][1])
 504              .substr($markup, $matches[0][1]+strlen($metaContentType));
 505          $headStart = stripos($markup, '<head>');
 506          $markup = substr($markup, 0, $headStart+6).$metaContentType
 507              .substr($markup, $headStart+6);
 508          return $markup;
 509      }
 510      protected function charsetAppendToHTML($html, $charset, $xhtml = false) {
 511          // remove existing meta[type=content-type]
 512          $html = preg_replace('@\s*<meta[^>]+http-equiv\\s*=\\s*(["|\'])Content-Type\\1([^>]+?)>@i', '', $html);
 513          $meta = '<meta http-equiv="Content-Type" content="text/html;charset='
 514              .$charset.'" '
 515              .($xhtml ? '/' : '')
 516              .'>';
 517          if (strpos($html, '<head') === false) {
 518              if (strpos($hltml, '<html') === false) {
 519                  return $meta.$html;
 520              } else {
 521                  return preg_replace(
 522                      '@<html(.*?)(?(?<!\?)>)@s',
 523                      "<html\\1><head>{$meta}</head>",
 524                      $html
 525                  );
 526              }
 527          } else {
 528              return preg_replace(
 529                  '@<head(.*?)(?(?<!\?)>)@s',
 530                  '<head\\1>'.$meta,
 531                  $html
 532              );
 533          }
 534      }
 535      protected function charsetAppendToXML($markup, $charset) {
 536          $declaration = '<'.'?xml version="1.0" encoding="'.$charset.'"?'.'>';
 537          return $declaration.$markup;
 538      }
 539      public static function isDocumentFragmentHTML($markup) {
 540          return stripos($markup, '<html') === false && stripos($markup, '<!doctype') === false;
 541      }
 542      public static function isDocumentFragmentXML($markup) {
 543          return stripos($markup, '<'.'?xml') === false;
 544      }
 545      public static function isDocumentFragmentXHTML($markup) {
 546          return self::isDocumentFragmentHTML($markup);
 547      }
 548      public function importAttr($value) {
 549          // TODO
 550      }
 551      /**
 552       *
 553       * @param $source
 554       * @param $target
 555       * @param $sourceCharset
 556       * @return array Array of imported nodes.
 557       */
 558      public function import($source, $sourceCharset = null) {
 559          // TODO charset conversions
 560          $return = array();
 561          if ($source instanceof DOMNODE && !($source instanceof DOMNODELIST))
 562              $source = array($source);
 563  //      if (is_array($source)) {
 564  //          foreach($source as $node) {
 565  //              if (is_string($node)) {
 566  //                  // string markup
 567  //                  $fake = $this->documentFragmentCreate($node, $sourceCharset);
 568  //                  if ($fake === false)
 569  //                      throw new Exception("Error loading documentFragment markup");
 570  //                  else
 571  //                      $return = array_merge($return,
 572  //                          $this->import($fake->root->childNodes)
 573  //                      );
 574  //              } else {
 575  //                  $return[] = $this->document->importNode($node, true);
 576  //              }
 577  //          }
 578  //          return $return;
 579  //      } else {
 580  //          // string markup
 581  //          $fake = $this->documentFragmentCreate($source, $sourceCharset);
 582  //          if ($fake === false)
 583  //              throw new Exception("Error loading documentFragment markup");
 584  //          else
 585  //              return $this->import($fake->root->childNodes);
 586  //      }
 587          if (is_array($source) || $source instanceof DOMNODELIST) {
 588              // dom nodes
 589              self::debug('Importing nodes to document');
 590              foreach($source as $node)
 591                  $return[] = $this->document->importNode($node, true);
 592          } else {
 593              // string markup
 594              $fake = $this->documentFragmentCreate($source, $sourceCharset);
 595              if ($fake === false)
 596                  throw new Exception("Error loading documentFragment markup");
 597              else
 598                  return $this->import($fake->root->childNodes);
 599          }
 600          return $return;
 601      }
 602      /**
 603       * Creates new document fragment.
 604       *
 605       * @param $source
 606       * @return DOMDocumentWrapper
 607       */
 608      protected function documentFragmentCreate($source, $charset = null) {
 609          $fake = new DOMDocumentWrapper();
 610          $fake->contentType = $this->contentType;
 611          $fake->isXML = $this->isXML;
 612          $fake->isHTML = $this->isHTML;
 613          $fake->isXHTML = $this->isXHTML;
 614          $fake->root = $fake->document;
 615          if (! $charset)
 616              $charset = $this->charset;
 617  //  $fake->documentCreate($this->charset);
 618          if ($source instanceof DOMNODE && !($source instanceof DOMNODELIST))
 619              $source = array($source);
 620          if (is_array($source) || $source instanceof DOMNODELIST) {
 621              // dom nodes
 622              // load fake document
 623              if (! $this->documentFragmentLoadMarkup($fake, $charset))
 624                  return false;
 625              $nodes = $fake->import($source);
 626              foreach($nodes as $node)
 627                  $fake->root->appendChild($node);
 628          } else {
 629              // string markup
 630              $this->documentFragmentLoadMarkup($fake, $charset, $source);
 631          }
 632          return $fake;
 633      }
 634      /**
 635       *
 636       * @param $document DOMDocumentWrapper
 637       * @param $markup
 638       * @return $document
 639       */
 640      private function documentFragmentLoadMarkup($fragment, $charset, $markup = null) {
 641          // TODO error handling
 642          // TODO copy doctype
 643          // tempolary turn off
 644          $fragment->isDocumentFragment = false;
 645          if ($fragment->isXML) {
 646              if ($fragment->isXHTML) {
 647                  // add FAKE element to set default namespace
 648                  $fragment->loadMarkupXML('<?xml version="1.0" encoding="'.$charset.'"?>'
 649                      .'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
 650                      .'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
 651                      .'<fake xmlns="http://www.w3.org/1999/xhtml">'.$markup.'</fake>');
 652                  $fragment->root = $fragment->document->firstChild->nextSibling;
 653              } else {
 654                  $fragment->loadMarkupXML('<?xml version="1.0" encoding="'.$charset.'"?><fake>'.$markup.'</fake>');
 655                  $fragment->root = $fragment->document->firstChild;
 656              }
 657          } else {
 658              $markup2 = phpQuery::$defaultDoctype.'<html><head><meta http-equiv="Content-Type" content="text/html;charset='
 659                  .$charset.'"></head>';
 660              $noBody = strpos($markup, '<body') === false;
 661              if ($noBody)
 662                  $markup2 .= '<body>';
 663              $markup2 .= $markup;
 664              if ($noBody)
 665                  $markup2 .= '</body>';
 666              $markup2 .= '</html>';
 667              $fragment->loadMarkupHTML($markup2);
 668              // TODO resolv body tag merging issue
 669              $fragment->root = $noBody
 670                  ? $fragment->document->firstChild->nextSibling->firstChild->nextSibling
 671                  : $fragment->document->firstChild->nextSibling->firstChild->nextSibling;
 672          }
 673          if (! $fragment->root)
 674              return false;
 675          $fragment->isDocumentFragment = true;
 676          return true;
 677      }
 678      protected function documentFragmentToMarkup($fragment) {
 679          phpQuery::debug('documentFragmentToMarkup');
 680          $tmp = $fragment->isDocumentFragment;
 681          $fragment->isDocumentFragment = false;
 682          $markup = $fragment->markup();
 683          if ($fragment->isXML) {
 684              $markup = substr($markup, 0, strrpos($markup, '</fake>'));
 685              if ($fragment->isXHTML) {
 686                  $markup = substr($markup, strpos($markup, '<fake')+43);
 687              } else {
 688                  $markup = substr($markup, strpos($markup, '<fake>')+6);
 689              }
 690          } else {
 691                  $markup = substr($markup, strpos($markup, '<body>')+6);
 692                  $markup = substr($markup, 0, strrpos($markup, '</body>'));
 693          }
 694          $fragment->isDocumentFragment = $tmp;
 695          if (phpQuery::$debug)
 696              phpQuery::debug('documentFragmentToMarkup: '.substr($markup, 0, 150));
 697          return $markup;
 698      }
 699      /**
 700       * Return document markup, starting with optional $nodes as root.
 701       *
 702       * @param $nodes    DOMNode|DOMNodeList
 703       * @return string
 704       */
 705      public function markup($nodes = null, $innerMarkup = false) {
 706          if (isset($nodes) && count($nodes) == 1 && $nodes[0] instanceof DOMDOCUMENT)
 707              $nodes = null;
 708          if (isset($nodes)) {
 709              $markup = '';
 710              if (!is_array($nodes) && !($nodes instanceof DOMNODELIST) )
 711                  $nodes = array($nodes);
 712              if ($this->isDocumentFragment && ! $innerMarkup)
 713                  foreach($nodes as $i => $node)
 714                      if ($node->isSameNode($this->root)) {
 715                      //  var_dump($node);
 716                          $nodes = array_slice($nodes, 0, $i)
 717                              + phpQuery::DOMNodeListToArray($node->childNodes)
 718                              + array_slice($nodes, $i+1);
 719                          }
 720              if ($this->isXML && ! $innerMarkup) {
 721                  self::debug("Getting outerXML with charset '{$this->charset}'");
 722                  // we need outerXML, so we can benefit from
 723                  // $node param support in saveXML()
 724                  foreach($nodes as $node)
 725                      $markup .= $this->document->saveXML($node);
 726              } else {
 727                  $loop = array();
 728                  if ($innerMarkup)
 729                      foreach($nodes as $node) {
 730                          if ($node->childNodes)
 731                              foreach($node->childNodes as $child)
 732                                  $loop[] = $child;
 733                          else
 734                              $loop[] = $node;
 735                      }
 736                  else
 737                      $loop = $nodes;
 738                  self::debug("Getting markup, moving selected nodes (".count($loop).") to new DocumentFragment");
 739                  $fake = $this->documentFragmentCreate($loop);
 740                  $markup = $this->documentFragmentToMarkup($fake);
 741              }
 742              if ($this->isXHTML) {
 743                  self::debug("Fixing XHTML");
 744                  $markup = self::markupFixXHTML($markup);
 745              }
 746              self::debug("Markup: ".substr($markup, 0, 250));
 747              return $markup;
 748          } else {
 749              if ($this->isDocumentFragment) {
 750                  // documentFragment, html only...
 751                  self::debug("Getting markup, DocumentFragment detected");
 752  //              return $this->markup(
 753  ////                    $this->document->getElementsByTagName('body')->item(0)
 754  //                  $this->document->root, true
 755  //              );
 756                  $markup = $this->documentFragmentToMarkup($this);
 757                  // no need for markupFixXHTML, as it's done thought markup($nodes) method
 758                  return $markup;
 759              } else {
 760                  self::debug("Getting markup (".($this->isXML?'XML':'HTML')."), final with charset '{$this->charset}'");
 761                  $markup = $this->isXML
 762                      ? $this->document->saveXML()
 763                      : $this->document->saveHTML();
 764                  if ($this->isXHTML) {
 765                      self::debug("Fixing XHTML");
 766                      $markup = self::markupFixXHTML($markup);
 767                  }
 768                  self::debug("Markup: ".substr($markup, 0, 250));
 769                  return $markup;
 770              }
 771          }
 772      }
 773      protected static function markupFixXHTML($markup) {
 774          $markup = self::expandEmptyTag('script', $markup);
 775          $markup = self::expandEmptyTag('select', $markup);
 776          $markup = self::expandEmptyTag('textarea', $markup);
 777          return $markup;
 778      }
 779      public static function debug($text) {
 780          phpQuery::debug($text);
 781      }
 782      /**
 783       * expandEmptyTag
 784       *
 785       * @param $tag
 786       * @param $xml
 787       * @return unknown_type
 788       * @author mjaque at ilkebenson dot com
 789       * @link http://php.net/manual/en/domdocument.savehtml.php#81256
 790       */
 791      public static function expandEmptyTag($tag, $xml){
 792          $indice = 0;
 793          while ($indice< strlen($xml)){
 794              $pos = strpos($xml, "<$tag ", $indice);
 795              if ($pos){
 796                  $posCierre = strpos($xml, ">", $pos);
 797                  if ($xml[$posCierre-1] == "/"){
 798                      $xml = substr_replace($xml, "></$tag>", $posCierre-1, 2);
 799                  }
 800                  $indice = $posCierre;
 801              }
 802              else break;
 803          }
 804          return $xml;
 805      }
 806  }
 807  
 808  /**
 809   * Event handling class.
 810   *
 811   * @author Tobiasz Cudnik
 812   * @package phpQuery
 813   * @static
 814   */
 815  abstract class phpQueryEvents {
 816      /**
 817       * Trigger a type of event on every matched element.
 818       *
 819       * @param DOMNode|phpQueryObject|string $document
 820       * @param unknown_type $type
 821       * @param unknown_type $data
 822       *
 823       * @TODO exclusive events (with !)
 824       * @TODO global events (test)
 825       * @TODO support more than event in $type (space-separated)
 826       */
 827      public static function trigger($document, $type, $data = array(), $node = null) {
 828          // trigger: function(type, data, elem, donative, extra) {
 829          $documentID = phpQuery::getDocumentID($document);
 830          $namespace = null;
 831          if (strpos($type, '.') !== false)
 832              list($name, $namespace) = explode('.', $type);
 833          else
 834              $name = $type;
 835          if (! $node) {
 836              if (self::issetGlobal($documentID, $type)) {
 837                  $pq = phpQuery::getDocument($documentID);
 838                  // TODO check add($pq->document)
 839                  $pq->find('*')->add($pq->document)
 840                      ->trigger($type, $data);
 841              }
 842          } else {
 843              if (isset($data[0]) && $data[0] instanceof DOMEvent) {
 844                  $event = $data[0];
 845                  $event->relatedTarget = $event->target;
 846                  $event->target = $node;
 847                  $data = array_slice($data, 1);
 848              } else {
 849                  $event = new DOMEvent(array(
 850                      'type' => $type,
 851                      'target' => $node,
 852                      'timeStamp' => time(),
 853                  ));
 854              }
 855              $i = 0;
 856              while($node) {
 857                  // TODO whois
 858                  phpQuery::debug("Triggering ".($i?"bubbled ":'')."event '{$type}' on "
 859                      ."node \n");//.phpQueryObject::whois($node)."\n");
 860                  $event->currentTarget = $node;
 861                  $eventNode = self::getNode($documentID, $node);
 862                  if (isset($eventNode->eventHandlers)) {
 863                      foreach($eventNode->eventHandlers as $eventType => $handlers) {
 864                          $eventNamespace = null;
 865                          if (strpos($type, '.') !== false)
 866                              list($eventName, $eventNamespace) = explode('.', $eventType);
 867                          else
 868                              $eventName = $eventType;
 869                          if ($name != $eventName)
 870                              continue;
 871                          if ($namespace && $eventNamespace && $namespace != $eventNamespace)
 872                              continue;
 873                          foreach($handlers as $handler) {
 874                              phpQuery::debug("Calling event handler\n");
 875                              $event->data = $handler['data']
 876                                  ? $handler['data']
 877                                  : null;
 878                              $params = array_merge(array($event), $data);
 879                              $return = phpQuery::callbackRun($handler['callback'], $params);
 880                              if ($return === false) {
 881                                  $event->bubbles = false;
 882                              }
 883                          }
 884                      }
 885                  }
 886                  // to bubble or not to bubble...
 887                  if (! $event->bubbles)
 888                      break;
 889                  $node = $node->parentNode;
 890                  $i++;
 891              }
 892          }
 893      }
 894      /**
 895       * Binds a handler to one or more events (like click) for each matched element.
 896       * Can also bind custom events.
 897       *
 898       * @param DOMNode|phpQueryObject|string $document
 899       * @param unknown_type $type
 900       * @param unknown_type $data Optional
 901       * @param unknown_type $callback
 902       *
 903       * @TODO support '!' (exclusive) events
 904       * @TODO support more than event in $type (space-separated)
 905       * @TODO support binding to global events
 906       */
 907      public static function add($document, $node, $type, $data, $callback = null) {
 908          phpQuery::debug("Binding '$type' event");
 909          $documentID = phpQuery::getDocumentID($document);
 910  //      if (is_null($callback) && is_callable($data)) {
 911  //          $callback = $data;
 912  //          $data = null;
 913  //      }
 914          $eventNode = self::getNode($documentID, $node);
 915          if (! $eventNode)
 916              $eventNode = self::setNode($documentID, $node);
 917          if (!isset($eventNode->eventHandlers[$type]))
 918              $eventNode->eventHandlers[$type] = array();
 919          $eventNode->eventHandlers[$type][] = array(
 920              'callback' => $callback,
 921              'data' => $data,
 922          );
 923      }
 924      /**
 925       * Enter description here...
 926       *
 927       * @param DOMNode|phpQueryObject|string $document
 928       * @param unknown_type $type
 929       * @param unknown_type $callback
 930       *
 931       * @TODO namespace events
 932       * @TODO support more than event in $type (space-separated)
 933       */
 934      public static function remove($document, $node, $type = null, $callback = null) {
 935          $documentID = phpQuery::getDocumentID($document);
 936          $eventNode = self::getNode($documentID, $node);
 937          if (is_object($eventNode) && isset($eventNode->eventHandlers[$type])) {
 938              if ($callback) {
 939                  foreach($eventNode->eventHandlers[$type] as $k => $handler)
 940                      if ($handler['callback'] == $callback)
 941                          unset($eventNode->eventHandlers[$type][$k]);
 942              } else {
 943                  unset($eventNode->eventHandlers[$type]);
 944              }
 945          }
 946      }
 947      protected static function getNode($documentID, $node) {
 948          foreach(phpQuery::$documents[$documentID]->eventsNodes as $eventNode) {
 949              if ($node->isSameNode($eventNode))
 950                  return $eventNode;
 951          }
 952      }
 953      protected static function setNode($documentID, $node) {
 954          phpQuery::$documents[$documentID]->eventsNodes[] = $node;
 955          return phpQuery::$documents[$documentID]->eventsNodes[
 956              count(phpQuery::$documents[$documentID]->eventsNodes)-1
 957          ];
 958      }
 959      protected static function issetGlobal($documentID, $type) {
 960          return isset(phpQuery::$documents[$documentID])
 961              ? in_array($type, phpQuery::$documents[$documentID]->eventsGlobal)
 962              : false;
 963      }
 964  }
 965  
 966  
 967  interface ICallbackNamed {
 968      function hasName();
 969      function getName();
 970  }
 971  /**
 972   * Callback class introduces currying-like pattern.
 973   *
 974   * Example:
 975   * function foo($param1, $param2, $param3) {
 976   *   var_dump($param1, $param2, $param3);
 977   * }
 978   * $fooCurried = new Callback('foo',
 979   *   'param1 is now statically set',
 980   *   new CallbackParam, new CallbackParam
 981   * );
 982   * phpQuery::callbackRun($fooCurried,
 983   *  array('param2 value', 'param3 value'
 984   * );
 985   *
 986   * Callback class is supported in all phpQuery methods which accepts callbacks.
 987   *
 988   * @link http://code.google.com/p/phpquery/wiki/Callbacks#Param_Structures
 989   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 990   *
 991   * @TODO??? return fake forwarding function created via create_function
 992   * @TODO honor paramStructure
 993   */
 994  class Callback
 995      implements ICallbackNamed {
 996      public $callback = null;
 997      public $params = null;
 998      protected $name;
 999      public function __construct($callback, $param1 = null, $param2 = null,
1000              $param3 = null) {
1001          $params = func_get_args();
1002          $params = array_slice($params, 1);
1003          if ($callback instanceof Callback) {
1004              // TODO implement recurention
1005          } else {
1006              $this->callback = $callback;
1007              $this->params = $params;
1008          }
1009      }
1010      public function getName() {
1011          return 'Callback: '.$this->name;
1012      }
1013      public function hasName() {
1014          return isset($this->name) && $this->name;
1015      }
1016      public function setName($name) {
1017          $this->name = $name;
1018          return $this;
1019      }
1020      // TODO test me
1021  //  public function addParams() {
1022  //      $params = func_get_args();
1023  //      return new Callback($this->callback, $this->params+$params);
1024  //  }
1025  }
1026  
1027  /**
1028   * Callback type which on execution returns reference passed during creation.
1029   *
1030   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1031   */
1032  class CallbackReturnReference extends Callback
1033      implements ICallbackNamed {
1034      protected $reference;
1035      public function __construct(&$reference, $name = null){
1036          $this->reference =& $reference;
1037          $this->callback = array($this, 'callback');
1038      }
1039      public function callback() {
1040          return $this->reference;
1041      }
1042      public function getName() {
1043          return 'Callback: '.$this->name;
1044      }
1045      public function hasName() {
1046          return isset($this->name) && $this->name;
1047      }
1048  }
1049  /**
1050   * Callback type which on execution returns value passed during creation.
1051   *
1052   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1053   */
1054  class CallbackReturnValue extends Callback
1055      implements ICallbackNamed {
1056      protected $value;
1057      protected $name;
1058      public function __construct($value, $name = null){
1059          $this->value =& $value;
1060          $this->name = $name;
1061          $this->callback = array($this, 'callback');
1062      }
1063      public function callback() {
1064          return $this->value;
1065      }
1066      public function __toString() {
1067          return $this->getName();
1068      }
1069      public function getName() {
1070          return 'Callback: '.$this->name;
1071      }
1072      public function hasName() {
1073          return isset($this->name) && $this->name;
1074      }
1075  }
1076  /**
1077   * CallbackParameterToReference can be used when we don't really want a callback,
1078   * only parameter passed to it. CallbackParameterToReference takes first
1079   * parameter's value and passes it to reference.
1080   *
1081   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1082   */
1083  class CallbackParameterToReference extends Callback {
1084      /**
1085       * @param $reference
1086       * @TODO implement $paramIndex;
1087       * param index choose which callback param will be passed to reference
1088       */
1089      public function __construct(&$reference){
1090          $this->callback =& $reference;
1091      }
1092  }
1093  //class CallbackReference extends Callback {
1094  //  /**
1095  //   *
1096  //   * @param $reference
1097  //   * @param $paramIndex
1098  //   * @todo implement $paramIndex; param index choose which callback param will be passed to reference
1099  //   */
1100  //  public function __construct(&$reference, $name = null){
1101  //      $this->callback =& $reference;
1102  //  }
1103  //}
1104  class CallbackParam {}
1105  
1106  /**
1107   * Class representing phpQuery objects.
1108   *
1109   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1110   * @package phpQuery
1111   * @method phpQueryObject clone() clone()
1112   * @method phpQueryObject empty() empty()
1113   * @method phpQueryObject next() next($selector = null)
1114   * @method phpQueryObject prev() prev($selector = null)
1115   * @property Int $length
1116   */
1117  class phpQueryObject
1118      implements Iterator, Countable, ArrayAccess {
1119      public $documentID = null;
1120      /**
1121       * DOMDocument class.
1122       *
1123       * @var DOMDocument
1124       */
1125      public $document = null;
1126      public $charset = null;
1127      /**
1128       *
1129       * @var DOMDocumentWrapper
1130       */
1131      public $documentWrapper = null;
1132      /**
1133       * XPath interface.
1134       *
1135       * @var DOMXPath
1136       */
1137      public $xpath = null;
1138      /**
1139       * Stack of selected elements.
1140       * @TODO refactor to ->nodes
1141       * @var array
1142       */
1143      public $elements = array();
1144      /**
1145       * @access private
1146       */
1147      protected $elementsBackup = array();
1148      /**
1149       * @access private
1150       */
1151      protected $previous = null;
1152      /**
1153       * @access private
1154       * @TODO deprecate
1155       */
1156      protected $root = array();
1157      /**
1158       * Indicated if doument is just a fragment (no <html> tag).
1159       *
1160       * Every document is realy a full document, so even documentFragments can
1161       * be queried against <html>, but getDocument(id)->htmlOuter() will return
1162       * only contents of <body>.
1163       *
1164       * @var bool
1165       */
1166      public $documentFragment = true;
1167      /**
1168       * Iterator interface helper
1169       * @access private
1170       */
1171      protected $elementsInterator = array();
1172      /**
1173       * Iterator interface helper
1174       * @access private
1175       */
1176      protected $valid = false;
1177      /**
1178       * Iterator interface helper
1179       * @access private
1180       */
1181      protected $current = null;
1182      /**
1183       * Enter description here...
1184       *
1185       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1186       */
1187      public function __construct($documentID) {
1188          dbg_deprecated(\DOMWrap\Document::class);
1189  
1190  //      if ($documentID instanceof self)
1191  //          var_dump($documentID->getDocumentID());
1192          $id = $documentID instanceof self
1193              ? $documentID->getDocumentID()
1194              : $documentID;
1195  //      var_dump($id);
1196          if (! isset(phpQuery::$documents[$id] )) {
1197  //          var_dump(phpQuery::$documents);
1198              throw new Exception("Document with ID '{$id}' isn't loaded. Use phpQuery::newDocument(\$html) or phpQuery::newDocumentFile(\$file) first.");
1199          }
1200          $this->documentID = $id;
1201          $this->documentWrapper =& phpQuery::$documents[$id];
1202          $this->document =& $this->documentWrapper->document;
1203          $this->xpath =& $this->documentWrapper->xpath;
1204          $this->charset =& $this->documentWrapper->charset;
1205          $this->documentFragment =& $this->documentWrapper->isDocumentFragment;
1206          // TODO check $this->DOM->documentElement;
1207  //      $this->root = $this->document->documentElement;
1208          $this->root =& $this->documentWrapper->root;
1209  //      $this->toRoot();
1210          $this->elements = array($this->root);
1211      }
1212      /**
1213       *
1214       * @access private
1215       * @param $attr
1216       * @return unknown_type
1217       */
1218      public function __get($attr) {
1219          switch($attr) {
1220              // FIXME doesnt work at all ?
1221              case 'length':
1222                  return $this->size();
1223              break;
1224              default:
1225                  return $this->$attr;
1226          }
1227      }
1228      /**
1229       * Saves actual object to $var by reference.
1230       * Useful when need to break chain.
1231       * @param phpQueryObject $var
1232       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1233       */
1234      public function toReference(&$var) {
1235          return $var = $this;
1236      }
1237      public function documentFragment($state = null) {
1238          if ($state) {
1239              phpQuery::$documents[$this->getDocumentID()]['documentFragment'] = $state;
1240              return $this;
1241          }
1242          return $this->documentFragment;
1243      }
1244      /**
1245     * @access private
1246     * @TODO documentWrapper
1247       */
1248      protected function isRoot( $node) {
1249  //      return $node instanceof DOMDOCUMENT || $node->tagName == 'html';
1250          return $node instanceof DOMDOCUMENT
1251              || ($node instanceof DOMELEMENT && $node->tagName == 'html')
1252              || $this->root->isSameNode($node);
1253      }
1254      /**
1255     * @access private
1256       */
1257      protected function stackIsRoot() {
1258          return $this->size() == 1 && $this->isRoot($this->elements[0]);
1259      }
1260      /**
1261       * Enter description here...
1262       * NON JQUERY METHOD
1263       *
1264       * Watch out, it doesn't creates new instance, can be reverted with end().
1265       *
1266       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1267       */
1268      public function toRoot() {
1269          $this->elements = array($this->root);
1270          return $this;
1271  //      return $this->newInstance(array($this->root));
1272      }
1273      /**
1274       * Saves object's DocumentID to $var by reference.
1275       * <code>
1276       * $myDocumentId;
1277       * phpQuery::newDocument('<div/>')
1278       *     ->getDocumentIDRef($myDocumentId)
1279       *     ->find('div')->...
1280       * </code>
1281       *
1282       * @param unknown_type $domId
1283       * @see phpQuery::newDocument
1284       * @see phpQuery::newDocumentFile
1285       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1286       */
1287      public function getDocumentIDRef(&$documentID) {
1288          $documentID = $this->getDocumentID();
1289          return $this;
1290      }
1291      /**
1292       * Returns object with stack set to document root.
1293       *
1294       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1295       */
1296      public function getDocument() {
1297          return phpQuery::getDocument($this->getDocumentID());
1298      }
1299      /**
1300       *
1301       * @return DOMDocument
1302       */
1303      public function getDOMDocument() {
1304          return $this->document;
1305      }
1306      /**
1307       * Get object's Document ID.
1308       *
1309       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1310       */
1311      public function getDocumentID() {
1312          return $this->documentID;
1313      }
1314      /**
1315       * Unloads whole document from memory.
1316       * CAUTION! None further operations will be possible on this document.
1317       * All objects refering to it will be useless.
1318       *
1319       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1320       */
1321      public function unloadDocument() {
1322          phpQuery::unloadDocuments($this->getDocumentID());
1323      }
1324      public function isHTML() {
1325          return $this->documentWrapper->isHTML;
1326      }
1327      public function isXHTML() {
1328          return $this->documentWrapper->isXHTML;
1329      }
1330      public function isXML() {
1331          return $this->documentWrapper->isXML;
1332      }
1333      /**
1334       * Enter description here...
1335       *
1336       * @link http://docs.jquery.com/Ajax/serialize
1337       * @return string
1338       */
1339      public function serialize() {
1340          return phpQuery::param($this->serializeArray());
1341      }
1342      /**
1343       * Enter description here...
1344       *
1345       * @link http://docs.jquery.com/Ajax/serializeArray
1346       * @return array
1347       */
1348      public function serializeArray($submit = null) {
1349          $source = $this->filter('form, input, select, textarea')
1350              ->find('input, select, textarea')
1351              ->andSelf()
1352              ->not('form');
1353          $return = array();
1354  //      $source->dumpDie();
1355          foreach($source as $input) {
1356              $input = phpQuery::pq($input);
1357              if ($input->is('[disabled]'))
1358                  continue;
1359              if (!$input->is('[name]'))
1360                  continue;
1361              if ($input->is('[type=checkbox]') && !$input->is('[checked]'))
1362                  continue;
1363              // jquery diff
1364              if ($submit && $input->is('[type=submit]')) {
1365                  if ($submit instanceof DOMELEMENT && ! $input->elements[0]->isSameNode($submit))
1366                      continue;
1367                  else if (is_string($submit) && $input->attr('name') != $submit)
1368                      continue;
1369              }
1370              $return[] = array(
1371                  'name' => $input->attr('name'),
1372                  'value' => $input->val(),
1373              );
1374          }
1375          return $return;
1376      }
1377      /**
1378       * @access private
1379       */
1380      protected function debug($in) {
1381          if (! phpQuery::$debug )
1382              return;
1383          print('<pre>');
1384          print_r($in);
1385          // file debug
1386  //      file_put_contents(dirname(__FILE__).'/phpQuery.log', print_r($in, true)."\n", FILE_APPEND);
1387          // quite handy debug trace
1388  //      if ( is_array($in))
1389  //          print_r(array_slice(debug_backtrace(), 3));
1390          print("</pre>\n");
1391      }
1392      /**
1393       * @access private
1394       */
1395      protected function isRegexp($pattern) {
1396          return in_array(
1397              $pattern[ mb_strlen($pattern)-1 ],
1398              array('^','*','$')
1399          );
1400      }
1401      /**
1402       * Determines if $char is really a char.
1403       *
1404       * @param string $char
1405       * @return bool
1406       * @todo rewrite me to charcode range ! ;)
1407       * @access private
1408       */
1409      protected function isChar($char) {
1410          return extension_loaded('mbstring') && phpQuery::$mbstringSupport
1411              ? mb_eregi('\w', $char)
1412              : preg_match('@\w@', $char);
1413      }
1414      /**
1415       * @access private
1416       */
1417      protected function parseSelector($query) {
1418          // clean spaces
1419          // TODO include this inside parsing ?
1420          $query = trim(
1421              preg_replace('@\s+@', ' ',
1422                  preg_replace('@\s*(>|\\+|~)\s*@', '\\1', $query)
1423              )
1424          );
1425          $queries = array(array());
1426          if (! $query)
1427              return $queries;
1428          $return =& $queries[0];
1429          $specialChars = array('>',' ');
1430  //      $specialCharsMapping = array('/' => '>');
1431          $specialCharsMapping = array();
1432          $strlen = mb_strlen($query);
1433          $classChars = array('.', '-');
1434          $pseudoChars = array('-');
1435          $tagChars = array('*', '|', '-');
1436          // split multibyte string
1437          // http://code.google.com/p/phpquery/issues/detail?id=76
1438          $_query = array();
1439          for ($i=0; $i<$strlen; $i++)
1440              $_query[] = mb_substr($query, $i, 1);
1441          $query = $_query;
1442          // it works, but i dont like it...
1443          $i = 0;
1444          while( $i < $strlen) {
1445              $c = $query[$i];
1446              $tmp = '';
1447              // TAG
1448              if ($this->isChar($c) || in_array($c, $tagChars)) {
1449                  while(isset($query[$i])
1450                      && ($this->isChar($query[$i]) || in_array($query[$i], $tagChars))) {
1451                      $tmp .= $query[$i];
1452                      $i++;
1453                  }
1454                  $return[] = $tmp;
1455              // IDs
1456              } else if ( $c == '#') {
1457                  $i++;
1458                  while( isset($query[$i]) && ($this->isChar($query[$i]) || $query[$i] == '-')) {
1459                      $tmp .= $query[$i];
1460                      $i++;
1461                  }
1462                  $return[] = '#'.$tmp;
1463              // SPECIAL CHARS
1464              } else if (in_array($c, $specialChars)) {
1465                  $return[] = $c;
1466                  $i++;
1467              // MAPPED SPECIAL MULTICHARS
1468  //          } else if ( $c.$query[$i+1] == '//') {
1469  //              $return[] = ' ';
1470  //              $i = $i+2;
1471              // MAPPED SPECIAL CHARS
1472              } else if ( isset($specialCharsMapping[$c])) {
1473                  $return[] = $specialCharsMapping[$c];
1474                  $i++;
1475              // COMMA
1476              } else if ( $c == ',') {
1477                  $queries[] = array();
1478                  $return =& $queries[ count($queries)-1 ];
1479                  $i++;
1480                  while( isset($query[$i]) && $query[$i] == ' ')
1481                      $i++;
1482              // CLASSES
1483              } else if ($c == '.') {
1484                  while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $classChars))) {
1485                      $tmp .= $query[$i];
1486                      $i++;
1487                  }
1488                  $return[] = $tmp;
1489              // ~ General Sibling Selector
1490              } else if ($c == '~') {
1491                  $spaceAllowed = true;
1492                  $tmp .= $query[$i++];
1493                  while( isset($query[$i])
1494                      && ($this->isChar($query[$i])
1495                          || in_array($query[$i], $classChars)
1496                          || $query[$i] == '*'
1497                          || ($query[$i] == ' ' && $spaceAllowed)
1498                      )) {
1499                      if ($query[$i] != ' ')
1500                          $spaceAllowed = false;
1501                      $tmp .= $query[$i];
1502                      $i++;
1503                  }
1504                  $return[] = $tmp;
1505              // + Adjacent sibling selectors
1506              } else if ($c == '+') {
1507                  $spaceAllowed = true;
1508                  $tmp .= $query[$i++];
1509                  while( isset($query[$i])
1510                      && ($this->isChar($query[$i])
1511                          || in_array($query[$i], $classChars)
1512                          || $query[$i] == '*'
1513                          || ($spaceAllowed && $query[$i] == ' ')
1514                      )) {
1515                      if ($query[$i] != ' ')
1516                          $spaceAllowed = false;
1517                      $tmp .= $query[$i];
1518                      $i++;
1519                  }
1520                  $return[] = $tmp;
1521              // ATTRS
1522              } else if ($c == '[') {
1523                  $stack = 1;
1524                  $tmp .= $c;
1525                  while( isset($query[++$i])) {
1526                      $tmp .= $query[$i];
1527                      if ( $query[$i] == '[') {
1528                          $stack++;
1529                      } else if ( $query[$i] == ']') {
1530                          $stack--;
1531                          if (! $stack )
1532                              break;
1533                      }
1534                  }
1535                  $return[] = $tmp;
1536                  $i++;
1537              // PSEUDO CLASSES
1538              } else if ($c == ':') {
1539                  $stack = 1;
1540                  $tmp .= $query[$i++];
1541                  while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $pseudoChars))) {
1542                      $tmp .= $query[$i];
1543                      $i++;
1544                  }
1545                  // with arguments ?
1546                  if ( isset($query[$i]) && $query[$i] == '(') {
1547                      $tmp .= $query[$i];
1548                      $stack = 1;
1549                      while( isset($query[++$i])) {
1550                          $tmp .= $query[$i];
1551                          if ( $query[$i] == '(') {
1552                              $stack++;
1553                          } else if ( $query[$i] == ')') {
1554                              $stack--;
1555                              if (! $stack )
1556                                  break;
1557                          }
1558                      }
1559                      $return[] = $tmp;
1560                      $i++;
1561                  } else {
1562                      $return[] = $tmp;
1563                  }
1564              } else {
1565                  $i++;
1566              }
1567          }
1568          foreach($queries as $k => $q) {
1569              if (isset($q[0])) {
1570                  if (isset($q[0][0]) && $q[0][0] == ':')
1571                      array_unshift($queries[$k], '*');
1572                  if ($q[0] != '>')
1573                      array_unshift($queries[$k], ' ');
1574              }
1575          }
1576          return $queries;
1577      }
1578      /**
1579       * Return matched DOM nodes.
1580       *
1581       * @param int $index
1582       * @return array|DOMElement Single DOMElement or array of DOMElement.
1583       */
1584      public function get($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1585          $return = isset($index)
1586              ? (isset($this->elements[$index]) ? $this->elements[$index] : null)
1587              : $this->elements;
1588          // pass thou callbacks
1589          $args = func_get_args();
1590          $args = array_slice($args, 1);
1591          foreach($args as $callback) {
1592              if (is_array($return))
1593                  foreach($return as $k => $v)
1594                      $return[$k] = phpQuery::callbackRun($callback, array($v));
1595              else
1596                  $return = phpQuery::callbackRun($callback, array($return));
1597          }
1598          return $return;
1599      }
1600      /**
1601       * Return matched DOM nodes.
1602       * jQuery difference.
1603       *
1604       * @param int $index
1605       * @return array|string Returns string if $index != null
1606       * @todo implement callbacks
1607       * @todo return only arrays ?
1608       * @todo maybe other name...
1609       */
1610      public function getString($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1611          if ($index)
1612              $return = $this->eq($index)->text();
1613          else {
1614              $return = array();
1615              for($i = 0; $i < $this->size(); $i++) {
1616                  $return[] = $this->eq($i)->text();
1617              }
1618          }
1619          // pass thou callbacks
1620          $args = func_get_args();
1621          $args = array_slice($args, 1);
1622          foreach($args as $callback) {
1623              $return = phpQuery::callbackRun($callback, array($return));
1624          }
1625          return $return;
1626      }
1627      /**
1628       * Return matched DOM nodes.
1629       * jQuery difference.
1630       *
1631       * @param int $index
1632       * @return array|string Returns string if $index != null
1633       * @todo implement callbacks
1634       * @todo return only arrays ?
1635       * @todo maybe other name...
1636       */
1637      public function getStrings($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1638          if ($index)
1639              $return = $this->eq($index)->text();
1640          else {
1641              $return = array();
1642              for($i = 0; $i < $this->size(); $i++) {
1643                  $return[] = $this->eq($i)->text();
1644              }
1645              // pass thou callbacks
1646              $args = func_get_args();
1647              $args = array_slice($args, 1);
1648          }
1649          foreach($args as $callback) {
1650              if (is_array($return))
1651                  foreach($return as $k => $v)
1652                      $return[$k] = phpQuery::callbackRun($callback, array($v));
1653              else
1654                  $return = phpQuery::callbackRun($callback, array($return));
1655          }
1656          return $return;
1657      }
1658      /**
1659       * Returns new instance of actual class.
1660       *
1661       * @param array $newStack Optional. Will replace old stack with new and move old one to history.c
1662       */
1663      public function newInstance($newStack = null) {
1664          $class = get_class($this);
1665          // support inheritance by passing old object to overloaded constructor
1666          $new = $class != 'phpQuery'
1667              ? new $class($this, $this->getDocumentID())
1668              : new phpQueryObject($this->getDocumentID());
1669          $new->previous = $this;
1670          if (is_null($newStack)) {
1671              $new->elements = $this->elements;
1672              if ($this->elementsBackup)
1673                  $this->elements = $this->elementsBackup;
1674          } else if (is_string($newStack)) {
1675              $new->elements = phpQuery::pq($newStack, $this->getDocumentID())->stack();
1676          } else {
1677              $new->elements = $newStack;
1678          }
1679          return $new;
1680      }
1681      /**
1682       * Enter description here...
1683       *
1684       * In the future, when PHP will support XLS 2.0, then we would do that this way:
1685       * contains(tokenize(@class, '\s'), "something")
1686       * @param unknown_type $class
1687       * @param unknown_type $node
1688       * @return boolean
1689       * @access private
1690       */
1691      protected function matchClasses($class, $node) {
1692          // multi-class
1693          if ( mb_strpos($class, '.', 1)) {
1694              $classes = explode('.', substr($class, 1));
1695              $classesCount = count( $classes );
1696              $nodeClasses = explode(' ', $node->getAttribute('class') );
1697              $nodeClassesCount = count( $nodeClasses );
1698              if ( $classesCount > $nodeClassesCount )
1699                  return false;
1700              $diff = count(
1701                  array_diff(
1702                      $classes,
1703                      $nodeClasses
1704                  )
1705              );
1706              if (! $diff )
1707                  return true;
1708          // single-class
1709          } else {
1710              return in_array(
1711                  // strip leading dot from class name
1712                  substr($class, 1),
1713                  // get classes for element as array
1714                  explode(' ', $node->getAttribute('class') )
1715              );
1716          }
1717      }
1718      /**
1719       * @access private
1720       */
1721      protected function runQuery($XQuery, $selector = null, $compare = null) {
1722          if ($compare && ! method_exists($this, $compare))
1723              return false;
1724          $stack = array();
1725          if (! $this->elements)
1726              $this->debug('Stack empty, skipping...');
1727  //      var_dump($this->elements[0]->nodeType);
1728          // element, document
1729          foreach($this->stack(array(1, 9, 13)) as $k => $stackNode) {
1730              $detachAfter = false;
1731              // to work on detached nodes we need temporary place them somewhere
1732              // thats because context xpath queries sucks ;]
1733              $testNode = $stackNode;
1734              while ($testNode) {
1735                  if (! $testNode->parentNode && ! $this->isRoot($testNode)) {
1736                      $this->root->appendChild($testNode);
1737                      $detachAfter = $testNode;
1738                      break;
1739                  }
1740                  $testNode = isset($testNode->parentNode)
1741                      ? $testNode->parentNode
1742                      : null;
1743              }
1744              // XXX tmp ?
1745              $xpath = $this->documentWrapper->isXHTML
1746                  ? $this->getNodeXpath($stackNode, 'html')
1747                  : $this->getNodeXpath($stackNode);
1748              // FIXME pseudoclasses-only query, support XML
1749              $query = $XQuery == '//' && $xpath == '/html[1]'
1750                  ? '//*'
1751                  : $xpath.$XQuery;
1752              $this->debug("XPATH: {$query}");
1753              // run query, get elements
1754              $nodes = $this->xpath->query($query);
1755              $this->debug("QUERY FETCHED");
1756              if (! $nodes->length )
1757                  $this->debug('Nothing found');
1758              $debug = array();
1759              foreach($nodes as $node) {
1760                  $matched = false;
1761                  if ( $compare) {
1762                      phpQuery::$debug ?
1763                          $this->debug("Found: ".$this->whois( $node ).", comparing with {$compare}()")
1764                          : null;
1765                      $phpQueryDebug = phpQuery::$debug;
1766                      phpQuery::$debug = false;
1767                      // TODO ??? use phpQuery::callbackRun()
1768                      if (call_user_func_array(array($this, $compare), array($selector, $node)))
1769                          $matched = true;
1770                      phpQuery::$debug = $phpQueryDebug;
1771                  } else {
1772                      $matched = true;
1773                  }
1774                  if ( $matched) {
1775                      if (phpQuery::$debug)
1776                          $debug[] = $this->whois( $node );
1777                      $stack[] = $node;
1778                  }
1779              }
1780              if (phpQuery::$debug) {
1781                  $this->debug("Matched ".count($debug).": ".implode(', ', $debug));
1782              }
1783              if ($detachAfter)
1784                  $this->root->removeChild($detachAfter);
1785          }
1786          $this->elements = $stack;
1787      }
1788      /**
1789       * Enter description here...
1790       *
1791       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1792       */
1793      public function find($selectors, $context = null, $noHistory = false) {
1794          if (!$noHistory)
1795              // backup last stack /for end()/
1796              $this->elementsBackup = $this->elements;
1797          // allow to define context
1798          // TODO combine code below with phpQuery::pq() context guessing code
1799          //   as generic function
1800          if ($context) {
1801              if (! is_array($context) && $context instanceof DOMELEMENT)
1802                  $this->elements = array($context);
1803              else if (is_array($context)) {
1804                  $this->elements = array();
1805                  foreach ($context as $c)
1806                      if ($c instanceof DOMELEMENT)
1807                          $this->elements[] = $c;
1808              } else if ( $context instanceof self )
1809                  $this->elements = $context->elements;
1810          }
1811          $queries = $this->parseSelector($selectors);
1812          $this->debug(array('FIND', $selectors, $queries));
1813          $XQuery = '';
1814          // remember stack state because of multi-queries
1815          $oldStack = $this->elements;
1816          // here we will be keeping found elements
1817          $stack = array();
1818          foreach($queries as $selector) {
1819              $this->elements = $oldStack;
1820              $delimiterBefore = false;
1821              foreach($selector as $s) {
1822                  // TAG
1823                  $isTag = extension_loaded('mbstring') && phpQuery::$mbstringSupport
1824                      ? mb_ereg_match('^[\w|\||-]+$', $s) || $s == '*'
1825                      : preg_match('@^[\w|\||-]+$@', $s) || $s == '*';
1826                  if ($isTag) {
1827                      if ($this->isXML()) {
1828                          // namespace support
1829                          if (mb_strpos($s, '|') !== false) {
1830                              $ns = $tag = null;
1831                              list($ns, $tag) = explode('|', $s);
1832                              $XQuery .= "$ns:$tag";
1833                          } else if ($s == '*') {
1834                              $XQuery .= "*";
1835                          } else {
1836                              $XQuery .= "*[local-name()='$s']";
1837                          }
1838                      } else {
1839                          $XQuery .= $s;
1840                      }
1841                  // ID
1842                  } else if ($s[0] == '#') {
1843                      if ($delimiterBefore)
1844                          $XQuery .= '*';
1845                      $XQuery .= "[@id='".substr($s, 1)."']";
1846                  // ATTRIBUTES
1847                  } else if ($s[0] == '[') {
1848                      if ($delimiterBefore)
1849                          $XQuery .= '*';
1850                      // strip side brackets
1851                      $attr = trim($s, '][');
1852                      $execute = false;
1853                      // attr with specifed value
1854                      if (mb_strpos($s, '=')) {
1855                          $value = null;
1856                          list($attr, $value) = explode('=', $attr);
1857                          $value = trim($value, "'\"");
1858                          if ($this->isRegexp($attr)) {
1859                              // cut regexp character
1860                              $attr = substr($attr, 0, -1);
1861                              $execute = true;
1862                              $XQuery .= "[@{$attr}]";
1863                          } else {
1864                              $XQuery .= "[@{$attr}='{$value}']";
1865                          }
1866                      // attr without specified value
1867                      } else {
1868                          $XQuery .= "[@{$attr}]";
1869                      }
1870                      if ($execute) {
1871                          $this->runQuery($XQuery, $s, 'is');
1872                          $XQuery = '';
1873                          if (! $this->length())
1874                              break;
1875                      }
1876                  // CLASSES
1877                  } else if ($s[0] == '.') {
1878                      // TODO use return $this->find("./self::*[contains(concat(\" \",@class,\" \"), \" $class \")]");
1879                      // thx wizDom ;)
1880                      if ($delimiterBefore)
1881                          $XQuery .= '*';
1882                      $XQuery .= '[@class]';
1883                      $this->runQuery($XQuery, $s, 'matchClasses');
1884                      $XQuery = '';
1885                      if (! $this->length() )
1886                          break;
1887                  // ~ General Sibling Selector
1888                  } else if ($s[0] == '~') {
1889                      $this->runQuery($XQuery);
1890                      $XQuery = '';
1891                      $this->elements = $this
1892                          ->siblings(
1893                              substr($s, 1)
1894                          )->elements;
1895                      if (! $this->length() )
1896                          break;
1897                  // + Adjacent sibling selectors
1898                  } else if ($s[0] == '+') {
1899                      // TODO /following-sibling::
1900                      $this->runQuery($XQuery);
1901                      $XQuery = '';
1902                      $subSelector = substr($s, 1);
1903                      $subElements = $this->elements;
1904                      $this->elements = array();
1905                      foreach($subElements as $node) {
1906                          // search first DOMElement sibling
1907                          $test = $node->nextSibling;
1908                          while($test && ! ($test instanceof DOMELEMENT))
1909                              $test = $test->nextSibling;
1910                          if ($test && $this->is($subSelector, $test))
1911                              $this->elements[] = $test;
1912                      }
1913                      if (! $this->length() )
1914                          break;
1915                  // PSEUDO CLASSES
1916                  } else if ($s[0] == ':') {
1917                      // TODO optimization for :first :last
1918                      if ($XQuery) {
1919                          $this->runQuery($XQuery);
1920                          $XQuery = '';
1921                      }
1922                      if (! $this->length())
1923                          break;
1924                      $this->pseudoClasses($s);
1925                      if (! $this->length())
1926                          break;
1927                  // DIRECT DESCENDANDS
1928                  } else if ($s == '>') {
1929                      $XQuery .= '/';
1930                      $delimiterBefore = 2;
1931                  // ALL DESCENDANDS
1932                  } else if ($s == ' ') {
1933                      $XQuery .= '//';
1934                      $delimiterBefore = 2;
1935                  // ERRORS
1936                  } else {
1937                      phpQuery::debug("Unrecognized token '$s'");
1938                  }
1939                  $delimiterBefore = $delimiterBefore === 2;
1940              }
1941              // run query if any
1942              if ($XQuery && $XQuery != '//') {
1943                  $this->runQuery($XQuery);
1944                  $XQuery = '';
1945              }
1946              foreach($this->elements as $node)
1947                  if (! $this->elementsContainsNode($node, $stack))
1948                      $stack[] = $node;
1949          }
1950          $this->elements = $stack;
1951          return $this->newInstance();
1952      }
1953      /**
1954       * @todo create API for classes with pseudoselectors
1955       * @access private
1956       */
1957      protected function pseudoClasses($class) {
1958          // TODO clean args parsing ?
1959          $class = ltrim($class, ':');
1960          $haveArgs = mb_strpos($class, '(');
1961          if ($haveArgs !== false) {
1962              $args = substr($class, $haveArgs+1, -1);
1963              $class = substr($class, 0, $haveArgs);
1964          }
1965          switch($class) {
1966              case 'even':
1967              case 'odd':
1968                  $stack = array();
1969                  foreach($this->elements as $i => $node) {
1970                      if ($class == 'even' && ($i%2) == 0)
1971                          $stack[] = $node;
1972                      else if ( $class == 'odd' && $i % 2 )
1973                          $stack[] = $node;
1974                  }
1975                  $this->elements = $stack;
1976                  break;
1977              case 'eq':
1978                  $k = intval($args);
1979                  $this->elements = isset( $this->elements[$k] )
1980                      ? array( $this->elements[$k] )
1981                      : array();
1982                  break;
1983              case 'gt':
1984                  $this->elements = array_slice($this->elements, $args+1);
1985                  break;
1986              case 'lt':
1987                  $this->elements = array_slice($this->elements, 0, $args+1);
1988                  break;
1989              case 'first':
1990                  if (isset($this->elements[0]))
1991                      $this->elements = array($this->elements[0]);
1992                  break;
1993              case 'last':
1994                  if ($this->elements)
1995                      $this->elements = array($this->elements[count($this->elements)-1]);
1996                  break;
1997              /*case 'parent':
1998                  $stack = array();
1999                  foreach($this->elements as $node) {
2000                      if ( $node->childNodes->length )
2001                          $stack[] = $node;
2002                  }
2003                  $this->elements = $stack;
2004                  break;*/
2005              case 'contains':
2006                  $text = trim($args, "\"'");
2007                  $stack = array();
2008                  foreach($this->elements as $node) {
2009                      if (mb_stripos($node->textContent, $text) === false)
2010                          continue;
2011                      $stack[] = $node;
2012                  }
2013                  $this->elements = $stack;
2014                  break;
2015              case 'not':
2016                  $selector = self::unQuote($args);
2017                  $this->elements = $this->not($selector)->stack();
2018                  break;
2019              case 'slice':
2020                  // TODO jQuery difference ?
2021                  $args = explode(',',
2022                      str_replace(', ', ',', trim($args, "\"'"))
2023                  );
2024                  $start = $args[0];
2025                  $end = isset($args[1])
2026                      ? $args[1]
2027                      : null;
2028                  if ($end > 0)
2029                      $end = $end-$start;
2030                  $this->elements = array_slice($this->elements, $start, $end);
2031                  break;
2032              case 'has':
2033                  $selector = trim($args, "\"'");
2034                  $stack = array();
2035                  foreach($this->stack(1) as $el) {
2036                      if ($this->find($selector, $el, true)->length)
2037                          $stack[] = $el;
2038                  }
2039                  $this->elements = $stack;
2040                  break;
2041              case 'submit':
2042              case 'reset':
2043                  $this->elements = phpQuery::merge(
2044                      $this->map(array($this, 'is'),
2045                          "input[type=$class]", new CallbackParam()
2046                      ),
2047                      $this->map(array($this, 'is'),
2048                          "button[type=$class]", new CallbackParam()
2049                      )
2050                  );
2051              break;
2052  //              $stack = array();
2053  //              foreach($this->elements as $node)
2054  //                  if ($node->is('input[type=submit]') || $node->is('button[type=submit]'))
2055  //                      $stack[] = $el;
2056  //              $this->elements = $stack;
2057              case 'input':
2058                  $this->elements = $this->map(
2059                      array($this, 'is'),
2060                      'input', new CallbackParam()
2061                  )->elements;
2062              break;
2063              case 'password':
2064              case 'checkbox':
2065              case 'radio':
2066              case 'hidden':
2067              case 'image':
2068              case 'file':
2069                  $this->elements = $this->map(
2070                      array($this, 'is'),
2071                      "input[type=$class]", new CallbackParam()
2072                  )->elements;
2073              break;
2074              case 'parent':
2075                  $this->elements = $this->map(
2076                      function ($node) {
2077                          return $node instanceof DOMELEMENT && $node->childNodes->length
2078                              ? $node : null;
2079                      }
2080                  )->elements;
2081              break;
2082              case 'empty':
2083                  $this->elements = $this->map(
2084                      function ($node) {
2085                          return $node instanceof DOMELEMENT && $node->childNodes->length
2086                              ? null : $node;
2087                      }
2088                  )->elements;
2089              break;
2090              case 'disabled':
2091              case 'selected':
2092              case 'checked':
2093                  $this->elements = $this->map(
2094                      array($this, 'is'),
2095                      "[$class]", new CallbackParam()
2096                  )->elements;
2097              break;
2098              case 'enabled':
2099                  $this->elements = $this->map(
2100                      function ($node) {
2101                          return pq($node)->not(":disabled") ? $node : null;
2102                      }
2103                  )->elements;
2104              break;
2105              case 'header':
2106                  $this->elements = $this->map(
2107                      function ($node) {
2108                          $isHeader = isset($node->tagName) && in_array($node->tagName, array(
2109                                  "h1", "h2", "h3", "h4", "h5", "h6", "h7"
2110                              ));
2111                          return $isHeader
2112                              ? $node
2113                              : null;
2114                      }
2115                  )->elements;
2116  //              $this->elements = $this->map(
2117  //                  create_function('$node', '$node = pq($node);
2118  //                      return $node->is("h1")
2119  //                          || $node->is("h2")
2120  //                          || $node->is("h3")
2121  //                          || $node->is("h4")
2122  //                          || $node->is("h5")
2123  //                          || $node->is("h6")
2124  //                          || $node->is("h7")
2125  //                          ? $node
2126  //                          : null;')
2127  //              )->elements;
2128              break;
2129              case 'only-child':
2130                  $this->elements = $this->map(
2131                      function ($node) {
2132                          return pq($node)->siblings()->size() == 0 ? $node : null;
2133                      }
2134                  )->elements;
2135              break;
2136              case 'first-child':
2137                  $this->elements = $this->map(
2138                      function ($node) {
2139                          return pq($node)->prevAll()->size() == 0 ? $node : null;
2140                      }
2141                  )->elements;
2142              break;
2143              case 'last-child':
2144                  $this->elements = $this->map(
2145                      function ($node) {
2146                          return pq($node)->nextAll()->size() == 0 ? $node : null;
2147                      }
2148                  )->elements;
2149              break;
2150              case 'nth-child':
2151                  $param = trim($args, "\"'");
2152                  if (! $param)
2153                      break;
2154                      // nth-child(n+b) to nth-child(1n+b)
2155                  if ($param[0] == 'n')
2156                      $param = '1'.$param;
2157                  // :nth-child(index/even/odd/equation)
2158                  if ($param == 'even' || $param == 'odd')
2159                      $mapped = $this->map(
2160                          function ($node, $param) {
2161                              $index = pq($node)->prevAll()->size() + 1;
2162                              if ($param == "even" && ($index % 2) == 0)
2163                                  return $node;
2164                              else if ($param == "odd" && $index % 2 == 1)
2165                                  return $node;
2166                              else
2167                                  return null;
2168                          }, new CallbackParam(), $param
2169                      );
2170                  else if (mb_strlen($param) > 1 && $param[1] == 'n')
2171                      // an+b
2172                      $mapped = $this->map(
2173                          function ($node, $param) {
2174                              $prevs = pq($node)->prevAll()->size();
2175                              $index = 1 + $prevs;
2176                              $b = mb_strlen($param) > 3
2177                                  ? $param[3]
2178                                  : 0;
2179                              $a = $param[0];
2180                              if ($b && $param[2] == "-")
2181                                  $b = -$b;
2182                              if ($a > 0) {
2183                                  return ($index - $b) % $a == 0
2184                                      ? $node
2185                                      : null;
2186                                  phpQuery::debug($a . "*" . floor($index / $a) . "+$b-1 == " . ($a * floor($index / $a) + $b - 1) . " ?= $prevs");
2187                                  return $a * floor($index / $a) + $b - 1 == $prevs
2188                                      ? $node
2189                                      : null;
2190                              } else if ($a == 0) {
2191                                  return $index == $b
2192                                      ? $node
2193                                      : null;
2194                              } else {
2195                                  // negative value
2196                                  return $index <= $b
2197                                      ? $node
2198                                      : null;
2199                              }
2200  //                          if (! $b)
2201  //                              return $index%$a == 0
2202  //                                  ? $node
2203  //                                  : null;
2204  //                          else
2205  //                              return ($index-$b)%$a == 0
2206  //                                  ? $node
2207  //                                  : null;
2208                          },
2209                          new CallbackParam(), $param
2210                      );
2211                  else
2212                      // index
2213                      $mapped = $this->map(
2214                          function ($node, $index) {
2215                              $prevs = pq($node)->prevAll()->size();
2216                              if ($prevs && $prevs == $index - 1)
2217                                  return $node;
2218                              else if (!$prevs && $index == 1)
2219                                  return $node;
2220                              else
2221                                  return null;
2222                          },
2223                          new CallbackParam(), $param
2224                      );
2225                  $this->elements = $mapped->elements;
2226              break;
2227              default:
2228                  $this->debug("Unknown pseudoclass '{$class}', skipping...");
2229          }
2230      }
2231      /**
2232       * @access private
2233       */
2234      protected function pseudoClassParam($paramsString) {
2235          // TODO;
2236      }
2237      /**
2238       * Enter description here...
2239       *
2240       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2241       */
2242      public function is($selector, $nodes = null) {
2243          phpQuery::debug(array("Is:", $selector));
2244          if (! $selector)
2245              return false;
2246          $oldStack = $this->elements;
2247          $returnArray = false;
2248          if ($nodes && is_array($nodes)) {
2249              $this->elements = $nodes;
2250          } else if ($nodes)
2251              $this->elements = array($nodes);
2252          $this->filter($selector, true);
2253          $stack = $this->elements;
2254          $this->elements = $oldStack;
2255          if ($nodes)
2256              return $stack ? $stack : null;
2257          return (bool)count($stack);
2258      }
2259      /**
2260       * Enter description here...
2261       * jQuery difference.
2262       *
2263       * Callback:
2264       * - $index int
2265       * - $node DOMNode
2266       *
2267       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2268       * @link http://docs.jquery.com/Traversing/filter
2269       */
2270      public function filterCallback($callback, $_skipHistory = false) {
2271          if (! $_skipHistory) {
2272              $this->elementsBackup = $this->elements;
2273              $this->debug("Filtering by callback");
2274          }
2275          $newStack = array();
2276          foreach($this->elements as $index => $node) {
2277              $result = phpQuery::callbackRun($callback, array($index, $node));
2278              if (is_null($result) || (! is_null($result) && $result))
2279                  $newStack[] = $node;
2280          }
2281          $this->elements = $newStack;
2282          return $_skipHistory
2283              ? $this
2284              : $this->newInstance();
2285      }
2286      /**
2287       * Enter description here...
2288       *
2289       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2290       * @link http://docs.jquery.com/Traversing/filter
2291       */
2292      public function filter($selectors, $_skipHistory = false) {
2293          if ($selectors instanceof Callback OR $selectors instanceof Closure)
2294              return $this->filterCallback($selectors, $_skipHistory);
2295          if (! $_skipHistory)
2296              $this->elementsBackup = $this->elements;
2297          $notSimpleSelector = array(' ', '>', '~', '+', '/');
2298          if (! is_array($selectors))
2299              $selectors = $this->parseSelector($selectors);
2300          if (! $_skipHistory)
2301              $this->debug(array("Filtering:", $selectors));
2302          $finalStack = array();
2303          foreach($selectors as $selector) {
2304              $stack = array();
2305              if (! $selector)
2306                  break;
2307              // avoid first space or /
2308              if (in_array($selector[0], $notSimpleSelector))
2309                  $selector = array_slice($selector, 1);
2310              // PER NODE selector chunks
2311              foreach($this->stack() as $node) {
2312                  $break = false;
2313                  foreach($selector as $s) {
2314                      if (!($node instanceof DOMELEMENT)) {
2315                          // all besides DOMElement
2316                          if ( $s[0] == '[') {
2317                              $attr = trim($s, '[]');
2318                              if ( mb_strpos($attr, '=')) {
2319                                  list( $attr, $val ) = explode('=', $attr);
2320                                  if ($attr == 'nodeType' && $node->nodeType != $val)
2321                                      $break = true;
2322                              }
2323                          } else
2324                              $break = true;
2325                      } else {
2326                          // DOMElement only
2327                          // ID
2328                          if ( $s[0] == '#') {
2329                              if ( $node->getAttribute('id') != substr($s, 1) )
2330                                  $break = true;
2331                          // CLASSES
2332                          } else if ( $s[0] == '.') {
2333                              if (! $this->matchClasses( $s, $node ) )
2334                                  $break = true;
2335                          // ATTRS
2336                          } else if ( $s[0] == '[') {
2337                              // strip side brackets
2338                              $attr = trim($s, '[]');
2339                              if (mb_strpos($attr, '=')) {
2340                                  list($attr, $val) = explode('=', $attr);
2341                                  $val = self::unQuote($val);
2342                                  if ($attr == 'nodeType') {
2343                                      if ($val != $node->nodeType)
2344                                          $break = true;
2345                                  } else if ($this->isRegexp($attr)) {
2346                                      $val = extension_loaded('mbstring') && phpQuery::$mbstringSupport
2347                                          ? quotemeta(trim($val, '"\''))
2348                                          : preg_quote(trim($val, '"\''), '@');
2349                                      // switch last character
2350                                      switch( substr($attr, -1)) {
2351                                          // quotemeta used insted of preg_quote
2352                                          // http://code.google.com/p/phpquery/issues/detail?id=76
2353                                          case '^':
2354                                              $pattern = '^'.$val;
2355                                              break;
2356                                          case '*':
2357                                              $pattern = '.*'.$val.'.*';
2358                                              break;
2359                                          case '$':
2360                                              $pattern = '.*'.$val.'$';
2361                                              break;
2362                                      }
2363                                      // cut last character
2364                                      $attr = substr($attr, 0, -1);
2365                                      $isMatch = extension_loaded('mbstring') && phpQuery::$mbstringSupport
2366                                          ? mb_ereg_match($pattern, $node->getAttribute($attr))
2367                                          : preg_match("@{$pattern}@", $node->getAttribute($attr));
2368                                      if (! $isMatch)
2369                                          $break = true;
2370                                  } else if ($node->getAttribute($attr) != $val)
2371                                      $break = true;
2372                              } else if (! $node->hasAttribute($attr))
2373                                  $break = true;
2374                          // PSEUDO CLASSES
2375                          } else if ( $s[0] == ':') {
2376                              // skip
2377                          // TAG
2378                          } else if (trim($s)) {
2379                              if ($s != '*') {
2380                                  // TODO namespaces
2381                                  if (isset($node->tagName)) {
2382                                      if ($node->tagName != $s)
2383                                          $break = true;
2384                                  } else if ($s == 'html' && ! $this->isRoot($node))
2385                                      $break = true;
2386                              }
2387                          // AVOID NON-SIMPLE SELECTORS
2388                          } else if (in_array($s, $notSimpleSelector)) {
2389                              $break = true;
2390                              $this->debug(array('Skipping non simple selector', $selector));
2391                          }
2392                      }
2393                      if ($break)
2394                          break;
2395                  }
2396                  // if element passed all chunks of selector - add it to new stack
2397                  if (! $break )
2398                      $stack[] = $node;
2399              }
2400              $tmpStack = $this->elements;
2401              $this->elements = $stack;
2402              // PER ALL NODES selector chunks
2403              foreach($selector as $s)
2404                  // PSEUDO CLASSES
2405                  if ($s[0] == ':')
2406                      $this->pseudoClasses($s);
2407              foreach($this->elements as $node)
2408                  // XXX it should be merged without duplicates
2409                  // but jQuery doesnt do that
2410                  $finalStack[] = $node;
2411              $this->elements = $tmpStack;
2412          }
2413          $this->elements = $finalStack;
2414          if ($_skipHistory) {
2415              return $this;
2416          } else {
2417              $this->debug("Stack length after filter(): ".count($finalStack));
2418              return $this->newInstance();
2419          }
2420      }
2421      /**
2422       *
2423       * @param $value
2424       * @return unknown_type
2425       * @TODO implement in all methods using passed parameters
2426       */
2427      protected static function unQuote($value) {
2428          return $value[0] == '\'' || $value[0] == '"'
2429              ? substr($value, 1, -1)
2430              : $value;
2431      }
2432      /**
2433       * Enter description here...
2434       *
2435       * @link http://docs.jquery.com/Ajax/load
2436       * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2437       * @todo Support $selector
2438       */
2439      public function load($url, $data = null, $callback = null) {
2440          if ($data && ! is_array($data)) {
2441              $callback = $data;
2442              $data = null;
2443          }
2444          if (mb_strpos($url, ' ') !== false) {
2445              $matches = null;
2446              if (extension_loaded('mbstring') && phpQuery::$mbstringSupport)
2447                  mb_ereg('^([^ ]+) (.*)$', $url, $matches);
2448              else
2449                  preg_match('^([^ ]+) (.*)$', $url, $matches);
2450              $url = $matches[1];
2451              $selector = $matches[2];
2452              // FIXME this sucks, pass as callback param
2453              $this->_loadSelector = $selector;
2454          }
2455          $ajax = array(
2456              'url' => $url,
2457              'type' => $data ? 'POST' : 'GET',
2458              'data' => $data,
2459              'complete' => $callback,
2460              'success' => array($this, 'loadSuccess')
2461          );
2462          phpQuery::ajax($ajax);
2463          return $this;
2464      }
2465      /**
2466       * @access private
2467       * @param $html
2468       * @return unknown_type
2469       */
2470      public function loadSuccess($html) {
2471          if ($this->_loadSelector) {
2472              $html = phpQuery::newDocument($html)->find($this->_loadSelector);
2473              unset($this->_loadSelector);
2474          }
2475          foreach($this->stack(1) as $node) {
2476              phpQuery::pq($node, $this->getDocumentID())
2477                  ->markup($html);
2478          }
2479      }
2480      /**
2481       * Enter description here...
2482       *
2483       * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2484       * @todo
2485       */
2486      public function css() {
2487          // TODO
2488          return $this;
2489      }
2490      /**
2491       * @todo
2492       *
2493       */
2494      public function show(){
2495          // TODO
2496          return $this;
2497      }
2498      /**
2499       * @todo
2500       *
2501       */
2502      public function hide(){
2503          // TODO
2504          return $this;
2505      }
2506      /**
2507       * Trigger a type of event on every matched element.
2508       *
2509       * @param unknown_type $type
2510       * @param unknown_type $data
2511       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2512       * @TODO support more than event in $type (space-separated)
2513       */
2514      public function trigger($type, $data = array()) {
2515          foreach($this->elements as $node)
2516              phpQueryEvents::trigger($this->getDocumentID(), $type, $data, $node);
2517          return $this;
2518      }
2519      /**
2520       * This particular method triggers all bound event handlers on an element (for a specific event type) WITHOUT executing the browsers default actions.
2521       *
2522       * @param unknown_type $type
2523       * @param unknown_type $data
2524       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2525       * @TODO
2526       */
2527      public function triggerHandler($type, $data = array()) {
2528          // TODO;
2529      }
2530      /**
2531       * Binds a handler to one or more events (like click) for each matched element.
2532       * Can also bind custom events.
2533       *
2534       * @param unknown_type $type
2535       * @param unknown_type $data Optional
2536       * @param unknown_type $callback
2537       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2538       * @TODO support '!' (exclusive) events
2539       * @TODO support more than event in $type (space-separated)
2540       */
2541      public function bind($type, $data, $callback = null) {
2542          // TODO check if $data is callable, not using is_callable
2543          if (! isset($callback)) {
2544              $callback = $data;
2545              $data = null;
2546          }
2547          foreach($this->elements as $node)
2548              phpQueryEvents::add($this->getDocumentID(), $node, $type, $data, $callback);
2549          return $this;
2550      }
2551      /**
2552       * Enter description here...
2553       *
2554       * @param unknown_type $type
2555       * @param unknown_type $callback
2556       * @return unknown
2557       * @TODO namespace events
2558       * @TODO support more than event in $type (space-separated)
2559       */
2560      public function unbind($type = null, $callback = null) {
2561          foreach($this->elements as $node)
2562              phpQueryEvents::remove($this->getDocumentID(), $node, $type, $callback);
2563          return $this;
2564      }
2565      /**
2566       * Enter description here...
2567       *
2568       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2569       */
2570      public function change($callback = null) {
2571          if ($callback)
2572              return $this->bind('change', $callback);
2573          return $this->trigger('change');
2574      }
2575      /**
2576       * Enter description here...
2577       *
2578       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2579       */
2580      public function submit($callback = null) {
2581          if ($callback)
2582              return $this->bind('submit', $callback);
2583          return $this->trigger('submit');
2584      }
2585      /**
2586       * Enter description here...
2587       *
2588       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2589       */
2590      public function click($callback = null) {
2591          if ($callback)
2592              return $this->bind('click', $callback);
2593          return $this->trigger('click');
2594      }
2595      /**
2596       * Enter description here...
2597       *
2598       * @param String|phpQuery
2599       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2600       */
2601      public function wrapAllOld($wrapper) {
2602          $wrapper = pq($wrapper)->_clone();
2603          if (! $wrapper->length() || ! $this->length() )
2604              return $this;
2605          $wrapper->insertBefore($this->elements[0]);
2606          $deepest = $wrapper->elements[0];
2607          while($deepest->firstChild && $deepest->firstChild instanceof DOMELEMENT)
2608              $deepest = $deepest->firstChild;
2609          pq($deepest)->append($this);
2610          return $this;
2611      }
2612      /**
2613       * Enter description here...
2614       *
2615       * TODO testme...
2616       * @param String|phpQuery
2617       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2618       */
2619      public function wrapAll($wrapper) {
2620          if (! $this->length())
2621              return $this;
2622          return phpQuery::pq($wrapper, $this->getDocumentID())
2623              ->clone()
2624              ->insertBefore($this->get(0))
2625              ->map(array($this, '___wrapAllCallback'))
2626              ->append($this);
2627      }
2628    /**
2629     *
2630       * @param $node
2631       * @return unknown_type
2632       * @access private
2633     */
2634      public function ___wrapAllCallback($node) {
2635          $deepest = $node;
2636          while($deepest->firstChild && $deepest->firstChild instanceof DOMELEMENT)
2637              $deepest = $deepest->firstChild;
2638          return $deepest;
2639      }
2640      /**
2641       * Enter description here...
2642       * NON JQUERY METHOD
2643       *
2644       * @param String|phpQuery
2645       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2646       */
2647      public function wrapAllPHP($codeBefore, $codeAfter) {
2648          return $this
2649              ->slice(0, 1)
2650                  ->beforePHP($codeBefore)
2651              ->end()
2652              ->slice(-1)
2653                  ->afterPHP($codeAfter)
2654              ->end();
2655      }
2656      /**
2657       * Enter description here...
2658       *
2659       * @param String|phpQuery
2660       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2661       */
2662      public function wrap($wrapper) {
2663          foreach($this->stack() as $node)
2664              phpQuery::pq($node, $this->getDocumentID())->wrapAll($wrapper);
2665          return $this;
2666      }
2667      /**
2668       * Enter description here...
2669       *
2670       * @param String|phpQuery
2671       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2672       */
2673      public function wrapPHP($codeBefore, $codeAfter) {
2674          foreach($this->stack() as $node)
2675              phpQuery::pq($node, $this->getDocumentID())->wrapAllPHP($codeBefore, $codeAfter);
2676          return $this;
2677      }
2678      /**
2679       * Enter description here...
2680       *
2681       * @param String|phpQuery
2682       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2683       */
2684      public function wrapInner($wrapper) {
2685          foreach($this->stack() as $node)
2686              phpQuery::pq($node, $this->getDocumentID())->contents()->wrapAll($wrapper);
2687          return $this;
2688      }
2689      /**
2690       * Enter description here...
2691       *
2692       * @param String|phpQuery
2693       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2694       */
2695      public function wrapInnerPHP($codeBefore, $codeAfter) {
2696          foreach($this->stack(1) as $node)
2697              phpQuery::pq($node, $this->getDocumentID())->contents()
2698                  ->wrapAllPHP($codeBefore, $codeAfter);
2699          return $this;
2700      }
2701      /**
2702       * Enter description here...
2703       *
2704       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2705       * @testme Support for text nodes
2706       */
2707      public function contents() {
2708          $stack = array();
2709          foreach($this->stack(1) as $el) {
2710              // FIXME (fixed) http://code.google.com/p/phpquery/issues/detail?id=56
2711  //          if (! isset($el->childNodes))
2712  //              continue;
2713              foreach($el->childNodes as $node) {
2714                  $stack[] = $node;
2715              }
2716          }
2717          return $this->newInstance($stack);
2718      }
2719      /**
2720       * Enter description here...
2721       *
2722       * jQuery difference.
2723       *
2724       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2725       */
2726      public function contentsUnwrap() {
2727          foreach($this->stack(1) as $node) {
2728              if (! $node->parentNode )
2729                  continue;
2730              $childNodes = array();
2731              // any modification in DOM tree breaks childNodes iteration, so cache them first
2732              foreach($node->childNodes as $chNode )
2733                  $childNodes[] = $chNode;
2734              foreach($childNodes as $chNode )
2735  //              $node->parentNode->appendChild($chNode);
2736                  $node->parentNode->insertBefore($chNode, $node);
2737              $node->parentNode->removeChild($node);
2738          }
2739          return $this;
2740      }
2741      /**
2742       * Enter description here...
2743       *
2744       * jQuery difference.
2745       */
2746      public function switchWith($markup) {
2747          $markup = pq($markup, $this->getDocumentID());
2748          $content = null;
2749          foreach($this->stack(1) as $node) {
2750              pq($node)
2751                  ->contents()->toReference($content)->end()
2752                  ->replaceWith($markup->clone()->append($content));
2753          }
2754          return $this;
2755      }
2756      /**
2757       * Enter description here...
2758       *
2759       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2760       */
2761      public function eq($num) {
2762          $oldStack = $this->elements;
2763          $this->elementsBackup = $this->elements;
2764          $this->elements = array();
2765          if ( isset($oldStack[$num]) )
2766              $this->elements[] = $oldStack[$num];
2767          return $this->newInstance();
2768      }
2769      /**
2770       * Enter description here...
2771       *
2772       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2773       */
2774      public function size() {
2775          return count($this->elements);
2776      }
2777      /**
2778       * Enter description here...
2779       *
2780       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2781       * @deprecated Use length as attribute
2782       */
2783      public function length() {
2784          return $this->size();
2785      }
2786      public function count() {
2787          return $this->size();
2788      }
2789      /**
2790       * Enter description here...
2791       *
2792       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2793       * @todo $level
2794       */
2795      public function end($level = 1) {
2796  //      $this->elements = array_pop( $this->history );
2797  //      return $this;
2798  //      $this->previous->DOM = $this->DOM;
2799  //      $this->previous->XPath = $this->XPath;
2800          return $this->previous
2801              ? $this->previous
2802              : $this;
2803      }
2804      /**
2805       * Enter description here...
2806       * Normal use ->clone() .
2807       *
2808       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2809       * @access private
2810       */
2811      public function _clone() {
2812          $newStack = array();
2813          //pr(array('copy... ', $this->whois()));
2814          //$this->dumpHistory('copy');
2815          $this->elementsBackup = $this->elements;
2816          foreach($this->elements as $node) {
2817              $newStack[] = $node->cloneNode(true);
2818          }
2819          $this->elements = $newStack;
2820          return $this->newInstance();
2821      }
2822      /**
2823       * Enter description here...
2824       *
2825       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2826       */
2827      public function replaceWithPHP($code) {
2828          return $this->replaceWith(phpQuery::php($code));
2829      }
2830      /**
2831       * Enter description here...
2832       *
2833       * @param String|phpQuery $content
2834       * @link http://docs.jquery.com/Manipulation/replaceWith#content
2835       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2836       */
2837      public function replaceWith($content) {
2838          return $this->after($content)->remove();
2839      }
2840      /**
2841       * Enter description here...
2842       *
2843       * @param String $selector
2844       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2845       * @todo this works ?
2846       */
2847      public function replaceAll($selector) {
2848          foreach(phpQuery::pq($selector, $this->getDocumentID()) as $node)
2849              phpQuery::pq($node, $this->getDocumentID())
2850                  ->after($this->_clone())
2851                  ->remove();
2852          return $this;
2853      }
2854      /**
2855       * Enter description here...
2856       *
2857       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2858       */
2859      public function remove($selector = null) {
2860          $loop = $selector
2861              ? $this->filter($selector)->elements
2862              : $this->elements;
2863          foreach($loop as $node) {
2864              if (! $node->parentNode )
2865                  continue;
2866              if (isset($node->tagName))
2867                  $this->debug("Removing '{$node->tagName}'");
2868              $node->parentNode->removeChild($node);
2869              // Mutation event
2870              $event = new DOMEvent(array(
2871                  'target' => $node,
2872                  'type' => 'DOMNodeRemoved'
2873              ));
2874              phpQueryEvents::trigger($this->getDocumentID(),
2875                  $event->type, array($event), $node
2876              );
2877          }
2878          return $this;
2879      }
2880      protected function markupEvents($newMarkup, $oldMarkup, $node) {
2881          if ($node->tagName == 'textarea' && $newMarkup != $oldMarkup) {
2882              $event = new DOMEvent(array(
2883                  'target' => $node,
2884                  'type' => 'change'
2885              ));
2886              phpQueryEvents::trigger($this->getDocumentID(),
2887                  $event->type, array($event), $node
2888              );
2889          }
2890      }
2891      /**
2892       * jQuey difference
2893       *
2894       * @param $markup
2895       * @return unknown_type
2896       * @TODO trigger change event for textarea
2897       */
2898      public function markup($markup = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2899          $args = func_get_args();
2900          if ($this->documentWrapper->isXML)
2901              return call_user_func_array(array($this, 'xml'), $args);
2902          else
2903              return call_user_func_array(array($this, 'html'), $args);
2904      }
2905      /**
2906       * jQuey difference
2907       *
2908       * @param $markup
2909       * @return unknown_type
2910       */
2911      public function markupOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2912          $args = func_get_args();
2913          if ($this->documentWrapper->isXML)
2914              return call_user_func_array(array($this, 'xmlOuter'), $args);
2915          else
2916              return call_user_func_array(array($this, 'htmlOuter'), $args);
2917      }
2918      /**
2919       * Enter description here...
2920       *
2921       * @param unknown_type $html
2922       * @return string|phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2923       * @TODO force html result
2924       */
2925      public function html($html = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2926          if (isset($html)) {
2927              // INSERT
2928              $nodes = $this->documentWrapper->import($html);
2929              $this->empty();
2930              foreach($this->stack(1) as $alreadyAdded => $node) {
2931                  // for now, limit events for textarea
2932                  if (($this->isXHTML() || $this->isHTML()) && $node->tagName == 'textarea')
2933                      $oldHtml = pq($node, $this->getDocumentID())->markup();
2934                  foreach($nodes as $newNode) {
2935                      $node->appendChild($alreadyAdded
2936                          ? $newNode->cloneNode(true)
2937                          : $newNode
2938                      );
2939                  }
2940                  // for now, limit events for textarea
2941                  if (($this->isXHTML() || $this->isHTML()) && $node->tagName == 'textarea')
2942                      $this->markupEvents($html, $oldHtml, $node);
2943              }
2944              return $this;
2945          } else {
2946              // FETCH
2947              $return = $this->documentWrapper->markup($this->elements, true);
2948              $args = func_get_args();
2949              foreach(array_slice($args, 1) as $callback) {
2950                  $return = phpQuery::callbackRun($callback, array($return));
2951              }
2952              return $return;
2953          }
2954      }
2955      /**
2956       * @TODO force xml result
2957       */
2958      public function xml($xml = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2959          $args = func_get_args();
2960          return call_user_func_array(array($this, 'html'), $args);
2961      }
2962      /**
2963       * Enter description here...
2964       * @TODO force html result
2965       *
2966       * @return String
2967       */
2968      public function htmlOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2969          $markup = $this->documentWrapper->markup($this->elements);
2970          // pass thou callbacks
2971          $args = func_get_args();
2972          foreach($args as $callback) {
2973              $markup = phpQuery::callbackRun($callback, array($markup));
2974          }
2975          return $markup;
2976      }
2977      /**
2978       * @TODO force xml result
2979       */
2980      public function xmlOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2981          $args = func_get_args();
2982          return call_user_func_array(array($this, 'htmlOuter'), $args);
2983      }
2984      public function __toString() {
2985          return $this->markupOuter();
2986      }
2987      /**
2988       * Just like html(), but returns markup with VALID (dangerous) PHP tags.
2989       *
2990       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2991       * @todo support returning markup with PHP tags when called without param
2992       */
2993      public function php($code = null) {
2994          return $this->markupPHP($code);
2995      }
2996      /**
2997       * Enter description here...
2998       *
2999       * @param $code
3000       * @return unknown_type
3001       */
3002      public function markupPHP($code = null) {
3003          return isset($code)
3004              ? $this->markup(phpQuery::php($code))
3005              : phpQuery::markupToPHP($this->markup());
3006      }
3007      /**
3008       * Enter description here...
3009       *
3010       * @param $code
3011       * @return unknown_type
3012       */
3013      public function markupOuterPHP() {
3014          return phpQuery::markupToPHP($this->markupOuter());
3015      }
3016      /**
3017       * Enter description here...
3018       *
3019       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3020       */
3021      public function children($selector = null) {
3022          $stack = array();
3023          foreach($this->stack(1) as $node) {
3024  //          foreach($node->getElementsByTagName('*') as $newNode) {
3025              foreach($node->childNodes as $newNode) {
3026                  if ($newNode->nodeType != 1)
3027                      continue;
3028                  if ($selector && ! $this->is($selector, $newNode))
3029                      continue;
3030                  if ($this->elementsContainsNode($newNode, $stack))
3031                      continue;
3032                  $stack[] = $newNode;
3033              }
3034          }
3035          $this->elementsBackup = $this->elements;
3036          $this->elements = $stack;
3037          return $this->newInstance();
3038      }
3039      /**
3040       * Enter description here...
3041       *
3042       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3043       */
3044      public function ancestors($selector = null) {
3045          return $this->children( $selector );
3046      }
3047      /**
3048       * Enter description here...
3049       *
3050       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3051       */
3052      public function append( $content) {
3053          return $this->insert($content, __FUNCTION__);
3054      }
3055      /**
3056       * Enter description here...
3057       *
3058       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3059       */
3060      public function appendPHP( $content) {
3061          return $this->insert("<php><!-- {$content} --></php>", 'append');
3062      }
3063      /**
3064       * Enter description here...
3065       *
3066       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3067       */
3068      public function appendTo( $seletor) {
3069          return $this->insert($seletor, __FUNCTION__);
3070      }
3071      /**
3072       * Enter description here...
3073       *
3074       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3075       */
3076      public function prepend( $content) {
3077          return $this->insert($content, __FUNCTION__);
3078      }
3079      /**
3080       * Enter description here...
3081       *
3082       * @todo accept many arguments, which are joined, arrays maybe also
3083       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3084       */
3085      public function prependPHP( $content) {
3086          return $this->insert("<php><!-- {$content} --></php>", 'prepend');
3087      }
3088      /**
3089       * Enter description here...
3090       *
3091       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3092       */
3093      public function prependTo( $seletor) {
3094          return $this->insert($seletor, __FUNCTION__);
3095      }
3096      /**
3097       * Enter description here...
3098       *
3099       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3100       */
3101      public function before($content) {
3102          return $this->insert($content, __FUNCTION__);
3103      }
3104      /**
3105       * Enter description here...
3106       *
3107       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3108       */
3109      public function beforePHP( $content) {
3110          return $this->insert("<php><!-- {$content} --></php>", 'before');
3111      }
3112      /**
3113       * Enter description here...
3114       *
3115       * @param String|phpQuery
3116       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3117       */
3118      public function insertBefore( $seletor) {
3119          return $this->insert($seletor, __FUNCTION__);
3120      }
3121      /**
3122       * Enter description here...
3123       *
3124       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3125       */
3126      public function after( $content) {
3127          return $this->insert($content, __FUNCTION__);
3128      }
3129      /**
3130       * Enter description here...
3131       *
3132       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3133       */
3134      public function afterPHP( $content) {
3135          return $this->insert("<php><!-- {$content} --></php>", 'after');
3136      }
3137      /**
3138       * Enter description here...
3139       *
3140       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3141       */
3142      public function insertAfter( $seletor) {
3143          return $this->insert($seletor, __FUNCTION__);
3144      }
3145      /**
3146       * Internal insert method. Don't use it.
3147       *
3148       * @param unknown_type $target
3149       * @param unknown_type $type
3150       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3151       * @access private
3152       */
3153      public function insert($target, $type) {
3154          $this->debug("Inserting data with '{$type}'");
3155          $to = false;
3156          switch( $type) {
3157              case 'appendTo':
3158              case 'prependTo':
3159              case 'insertBefore':
3160              case 'insertAfter':
3161                  $to = true;
3162          }
3163          switch(gettype($target)) {
3164              case 'string':
3165                  $insertFrom = $insertTo = array();
3166                  if ($to) {
3167                      // INSERT TO
3168                      $insertFrom = $this->elements;
3169                      if (phpQuery::isMarkup($target)) {
3170                          // $target is new markup, import it
3171                          $insertTo = $this->documentWrapper->import($target);
3172                      // insert into selected element
3173                      } else {
3174                          // $tagret is a selector
3175                          $thisStack = $this->elements;
3176                          $this->toRoot();
3177                          $insertTo = $this->find($target)->elements;
3178                          $this->elements = $thisStack;
3179                      }
3180                  } else {
3181                      // INSERT FROM
3182                      $insertTo = $this->elements;
3183                      $insertFrom = $this->documentWrapper->import($target);
3184                  }
3185                  break;
3186              case 'object':
3187                  $insertFrom = $insertTo = array();
3188                  // phpQuery
3189                  if ($target instanceof self) {
3190                      if ($to) {
3191                          $insertTo = $target->elements;
3192                          if ($this->documentFragment && $this->stackIsRoot())
3193                              // get all body children
3194  //                          $loop = $this->find('body > *')->elements;
3195                              // TODO test it, test it hard...
3196  //                          $loop = $this->newInstance($this->root)->find('> *')->elements;
3197                              $loop = $this->root->childNodes;
3198                          else
3199                              $loop = $this->elements;
3200                          // import nodes if needed
3201                          $insertFrom = $this->getDocumentID() == $target->getDocumentID()
3202                              ? $loop
3203                              : $target->documentWrapper->import($loop);
3204                      } else {
3205                          $insertTo = $this->elements;
3206                          if ( $target->documentFragment && $target->stackIsRoot() )
3207                              // get all body children
3208  //                          $loop = $target->find('body > *')->elements;
3209                              $loop = $target->root->childNodes;
3210                          else
3211                              $loop = $target->elements;
3212                          // import nodes if needed
3213                          $insertFrom = $this->getDocumentID() == $target->getDocumentID()
3214                              ? $loop
3215                              : $this->documentWrapper->import($loop);
3216                      }
3217                  // DOMNODE
3218                  } elseif ($target instanceof DOMNODE) {
3219                      // import node if needed
3220  //                  if ( $target->ownerDocument != $this->DOM )
3221  //                      $target = $this->DOM->importNode($target, true);
3222                      if ( $to) {
3223                          $insertTo = array($target);
3224                          if ($this->documentFragment && $this->stackIsRoot())
3225                              // get all body children
3226                              $loop = $this->root->childNodes;
3227  //                          $loop = $this->find('body > *')->elements;
3228                          else
3229                              $loop = $this->elements;
3230                          foreach($loop as $fromNode)
3231                              // import nodes if needed
3232                              $insertFrom[] = ! $fromNode->ownerDocument->isSameNode($target->ownerDocument)
3233                                  ? $target->ownerDocument->importNode($fromNode, true)
3234                                  : $fromNode;
3235                      } else {
3236                          // import node if needed
3237                          if (! $target->ownerDocument->isSameNode($this->document))
3238                              $target = $this->document->importNode($target, true);
3239                          $insertTo = $this->elements;
3240                          $insertFrom[] = $target;
3241                      }
3242                  }
3243                  break;
3244          }
3245          phpQuery::debug("From ".count($insertFrom)."; To ".count($insertTo)." nodes");
3246          foreach($insertTo as $insertNumber => $toNode) {
3247              // we need static relative elements in some cases
3248              switch( $type) {
3249                  case 'prependTo':
3250                  case 'prepend':
3251                      $firstChild = $toNode->firstChild;
3252                      break;
3253                  case 'insertAfter':
3254                  case 'after':
3255                      $nextSibling = $toNode->nextSibling;
3256                      break;
3257              }
3258              foreach($insertFrom as $fromNode) {
3259                  // clone if inserted already before
3260                  $insert = $insertNumber
3261                      ? $fromNode->cloneNode(true)
3262                      : $fromNode;
3263                  switch($type) {
3264                      case 'appendTo':
3265                      case 'append':
3266  //                      $toNode->insertBefore(
3267  //                          $fromNode,
3268  //                          $toNode->lastChild->nextSibling
3269  //                      );
3270                          $toNode->appendChild($insert);
3271                          $eventTarget = $insert;
3272                          break;
3273                      case 'prependTo':
3274                      case 'prepend':
3275                          $toNode->insertBefore(
3276                              $insert,
3277                              $firstChild
3278                          );
3279                          break;
3280                      case 'insertBefore':
3281                      case 'before':
3282                          if (! $toNode->parentNode)
3283                              throw new Exception("No parentNode, can't do {$type}()");
3284                          else
3285                              $toNode->parentNode->insertBefore(
3286                                  $insert,
3287                                  $toNode
3288                              );
3289                          break;
3290                      case 'insertAfter':
3291                      case 'after':
3292                          if (! $toNode->parentNode)
3293                              throw new Exception("No parentNode, can't do {$type}()");
3294                          else
3295                              $toNode->parentNode->insertBefore(
3296                                  $insert,
3297                                  $nextSibling
3298                              );
3299                          break;
3300                  }
3301                  // Mutation event
3302                  $event = new DOMEvent(array(
3303                      'target' => $insert,
3304                      'type' => 'DOMNodeInserted'
3305                  ));
3306                  phpQueryEvents::trigger($this->getDocumentID(),
3307                      $event->type, array($event), $insert
3308                  );
3309              }
3310          }
3311          return $this;
3312      }
3313      /**
3314       * Enter description here...
3315       *
3316       * @return Int
3317       */
3318      public function index($subject) {
3319          $index = -1;
3320          $subject = $subject instanceof phpQueryObject
3321              ? $subject->elements[0]
3322              : $subject;
3323          foreach($this->newInstance() as $k => $node) {
3324              if ($node->isSameNode($subject))
3325                  $index = $k;
3326          }
3327          return $index;
3328      }
3329      /**
3330       * Enter description here...
3331       *
3332       * @param unknown_type $start
3333       * @param unknown_type $end
3334       *
3335       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3336       * @testme
3337       */
3338      public function slice($start, $end = null) {
3339  //      $last = count($this->elements)-1;
3340  //      $end = $end
3341  //          ? min($end, $last)
3342  //          : $last;
3343  //      if ($start < 0)
3344  //          $start = $last+$start;
3345  //      if ($start > $last)
3346  //          return array();
3347          if ($end > 0)
3348              $end = $end-$start;
3349          return $this->newInstance(
3350              array_slice($this->elements, $start, $end)
3351          );
3352      }
3353      /**
3354       * Enter description here...
3355       *
3356       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3357       */
3358      public function reverse() {
3359          $this->elementsBackup = $this->elements;
3360          $this->elements = array_reverse($this->elements);
3361          return $this->newInstance();
3362      }
3363      /**
3364       * Return joined text content.
3365       * @return String
3366       */
3367      public function text($text = null, $callback1 = null, $callback2 = null, $callback3 = null) {
3368          if (isset($text))
3369              return $this->html(htmlspecialchars($text));
3370          $args = func_get_args();
3371          $args = array_slice($args, 1);
3372          $return = '';
3373          foreach($this->elements as $node) {
3374              $text = $node->textContent;
3375              if (count($this->elements) > 1 && $text)
3376                  $text .= "\n";
3377              foreach($args as $callback) {
3378                  $text = phpQuery::callbackRun($callback, array($text));
3379              }
3380              $return .= $text;
3381          }
3382          return $return;
3383      }
3384      /**
3385       * Enter description here...
3386       *
3387       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3388       */
3389      public function plugin($class, $file = null) {
3390          phpQuery::plugin($class, $file);
3391          return $this;
3392      }
3393      /**
3394       * Deprecated, use $pq->plugin() instead.
3395       *
3396       * @deprecated
3397       * @param $class
3398       * @param $file
3399       * @return unknown_type
3400       */
3401      public static function extend($class, $file = null) {
3402          return $this->plugin($class, $file);
3403      }
3404      /**
3405       *
3406       * @access private
3407       * @param $method
3408       * @param $args
3409       * @return unknown_type
3410       */
3411      public function __call($method, $args) {
3412          $aliasMethods = array('clone', 'empty');
3413          if (isset(phpQuery::$extendMethods[$method])) {
3414              array_unshift($args, $this);
3415              return phpQuery::callbackRun(
3416                  phpQuery::$extendMethods[$method], $args
3417              );
3418          } else if (isset(phpQuery::$pluginsMethods[$method])) {
3419              array_unshift($args, $this);
3420              $class = phpQuery::$pluginsMethods[$method];
3421              $realClass = "phpQueryObjectPlugin_$class";
3422              $return = call_user_func_array(
3423                  array($realClass, $method),
3424                  $args
3425              );
3426              // XXX deprecate ?
3427              return is_null($return)
3428                  ? $this
3429                  : $return;
3430          } else if (in_array($method, $aliasMethods)) {
3431              return call_user_func_array(array($this, '_'.$method), $args);
3432          } else
3433              throw new Exception("Method '{$method}' doesnt exist");
3434      }
3435      /**
3436       * Safe rename of next().
3437       *
3438       * Use it ONLY when need to call next() on an iterated object (in same time).
3439       * Normaly there is no need to do such thing ;)
3440       *
3441       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3442       * @access private
3443       */
3444      public function _next($selector = null) {
3445          return $this->newInstance(
3446              $this->getElementSiblings('nextSibling', $selector, true)
3447          );
3448      }
3449      /**
3450       * Use prev() and next().
3451       *
3452       * @deprecated
3453       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3454       * @access private
3455       */
3456      public function _prev($selector = null) {
3457          return $this->prev($selector);
3458      }
3459      /**
3460       * Enter description here...
3461       *
3462       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3463       */
3464      public function prev($selector = null) {
3465          return $this->newInstance(
3466              $this->getElementSiblings('previousSibling', $selector, true)
3467          );
3468      }
3469      /**
3470       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3471       * @todo
3472       */
3473      public function prevAll($selector = null) {
3474          return $this->newInstance(
3475              $this->getElementSiblings('previousSibling', $selector)
3476          );
3477      }
3478      /**
3479       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3480       * @todo FIXME: returns source elements insted of next siblings
3481       */
3482      public function nextAll($selector = null) {
3483          return $this->newInstance(
3484              $this->getElementSiblings('nextSibling', $selector)
3485          );
3486      }
3487      /**
3488       * @access private
3489       */
3490      protected function getElementSiblings($direction, $selector = null, $limitToOne = false) {
3491          $stack = array();
3492          $count = 0;
3493          foreach($this->stack() as $node) {
3494              $test = $node;
3495              while( isset($test->{$direction}) && $test->{$direction}) {
3496                  $test = $test->{$direction};
3497                  if (! $test instanceof DOMELEMENT)
3498                      continue;
3499                  $stack[] = $test;
3500                  if ($limitToOne)
3501                      break;
3502              }
3503          }
3504          if ($selector) {
3505              $stackOld = $this->elements;
3506              $this->elements = $stack;
3507              $stack = $this->filter($selector, true)->stack();
3508              $this->elements = $stackOld;
3509          }
3510          return $stack;
3511      }
3512      /**
3513       * Enter description here...
3514       *
3515       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3516       */
3517      public function siblings($selector = null) {
3518          $stack = array();
3519          $siblings = array_merge(
3520              $this->getElementSiblings('previousSibling', $selector),
3521              $this->getElementSiblings('nextSibling', $selector)
3522          );
3523          foreach($siblings as $node) {
3524              if (! $this->elementsContainsNode($node, $stack))
3525                  $stack[] = $node;
3526          }
3527          return $this->newInstance($stack);
3528      }
3529      /**
3530       * Enter description here...
3531       *
3532       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3533       */
3534      public function not($selector = null) {
3535          if (is_string($selector))
3536              phpQuery::debug(array('not', $selector));
3537          else
3538              phpQuery::debug('not');
3539          $stack = array();
3540          if ($selector instanceof self || $selector instanceof DOMNODE) {
3541              foreach($this->stack() as $node) {
3542                  if ($selector instanceof self) {
3543                      $matchFound = false;
3544                      foreach($selector->stack() as $notNode) {
3545                          if ($notNode->isSameNode($node))
3546                              $matchFound = true;
3547                      }
3548                      if (! $matchFound)
3549                          $stack[] = $node;
3550                  } else if ($selector instanceof DOMNODE) {
3551                      if (! $selector->isSameNode($node))
3552                          $stack[] = $node;
3553                  } else {
3554                      if (! $this->is($selector))
3555                          $stack[] = $node;
3556                  }
3557              }
3558          } else {
3559              $orgStack = $this->stack();
3560              $matched = $this->filter($selector, true)->stack();
3561  //          $matched = array();
3562  //          // simulate OR in filter() instead of AND 5y
3563  //          foreach($this->parseSelector($selector) as $s) {
3564  //              $matched = array_merge($matched,
3565  //                  $this->filter(array($s))->stack()
3566  //              );
3567  //          }
3568              foreach($orgStack as $node)
3569                  if (! $this->elementsContainsNode($node, $matched))
3570                      $stack[] = $node;
3571          }
3572          return $this->newInstance($stack);
3573      }
3574      /**
3575       * Enter description here...
3576       *
3577       * @param string|phpQueryObject
3578       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3579       */
3580      public function add($selector = null) {
3581          if (! $selector)
3582              return $this;
3583          $stack = array();
3584          $this->elementsBackup = $this->elements;
3585          $found = phpQuery::pq($selector, $this->getDocumentID());
3586          $this->merge($found->elements);
3587          return $this->newInstance();
3588      }
3589      /**
3590       * @access private
3591       */
3592      protected function merge() {
3593          foreach(func_get_args() as $nodes)
3594              foreach($nodes as $newNode )
3595                  if (! $this->elementsContainsNode($newNode) )
3596                      $this->elements[] = $newNode;
3597      }
3598      /**
3599       * @access private
3600       * TODO refactor to stackContainsNode
3601       */
3602      protected function elementsContainsNode($nodeToCheck, $elementsStack = null) {
3603          $loop = ! is_null($elementsStack)
3604              ? $elementsStack
3605              : $this->elements;
3606          foreach($loop as $node) {
3607              if ( $node->isSameNode( $nodeToCheck ) )
3608                  return true;
3609          }
3610          return false;
3611      }
3612      /**
3613       * Enter description here...
3614       *
3615       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3616       */
3617      public function parent($selector = null) {
3618          $stack = array();
3619          foreach($this->elements as $node )
3620              if ( $node->parentNode && ! $this->elementsContainsNode($node->parentNode, $stack) )
3621                  $stack[] = $node->parentNode;
3622          $this->elementsBackup = $this->elements;
3623          $this->elements = $stack;
3624          if ( $selector )
3625              $this->filter($selector, true);
3626          return $this->newInstance();
3627      }
3628      /**
3629       * Enter description here...
3630       *
3631       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3632       */
3633      public function parents($selector = null) {
3634          $stack = array();
3635          if (! $this->elements )
3636              $this->debug('parents() - stack empty');
3637          foreach($this->elements as $node) {
3638              $test = $node;
3639              while( $test->parentNode) {
3640                  $test = $test->parentNode;
3641                  if ($this->isRoot($test))
3642                      break;
3643                  if (! $this->elementsContainsNode($test, $stack)) {
3644                      $stack[] = $test;
3645                      continue;
3646                  }
3647              }
3648          }
3649          $this->elementsBackup = $this->elements;
3650          $this->elements = $stack;
3651          if ( $selector )
3652              $this->filter($selector, true);
3653          return $this->newInstance();
3654      }
3655      /**
3656       * Internal stack iterator.
3657       *
3658       * @access private
3659       */
3660      public function stack($nodeTypes = null) {
3661          if (!isset($nodeTypes))
3662              return $this->elements;
3663          if (!is_array($nodeTypes))
3664              $nodeTypes = array($nodeTypes);
3665          $return = array();
3666          foreach($this->elements as $node) {
3667              if (in_array($node->nodeType, $nodeTypes))
3668                  $return[] = $node;
3669          }
3670          return $return;
3671      }
3672      // TODO phpdoc; $oldAttr is result of hasAttribute, before any changes
3673      protected function attrEvents($attr, $oldAttr, $oldValue, $node) {
3674          // skip events for XML documents
3675          if (! $this->isXHTML() && ! $this->isHTML())
3676              return;
3677          $event = null;
3678          // identify
3679          $isInputValue = $node->tagName == 'input'
3680              && (
3681                  in_array($node->getAttribute('type'),
3682                      array('text', 'password', 'hidden'))
3683                  || !$node->getAttribute('type')
3684                   );
3685          $isRadio = $node->tagName == 'input'
3686              && $node->getAttribute('type') == 'radio';
3687          $isCheckbox = $node->tagName == 'input'
3688              && $node->getAttribute('type') == 'checkbox';
3689          $isOption = $node->tagName == 'option';
3690          if ($isInputValue && $attr == 'value' && $oldValue != $node->getAttribute($attr)) {
3691              $event = new DOMEvent(array(
3692                  'target' => $node,
3693                  'type' => 'change'
3694              ));
3695          } else if (($isRadio || $isCheckbox) && $attr == 'checked' && (
3696                  // check
3697                  (! $oldAttr && $node->hasAttribute($attr))
3698                  // un-check
3699                  || (! $node->hasAttribute($attr) && $oldAttr)
3700              )) {
3701              $event = new DOMEvent(array(
3702                  'target' => $node,
3703                  'type' => 'change'
3704              ));
3705          } else if ($isOption && $node->parentNode && $attr == 'selected' && (
3706                  // select
3707                  (! $oldAttr && $node->hasAttribute($attr))
3708                  // un-select
3709                  || (! $node->hasAttribute($attr) && $oldAttr)
3710              )) {
3711              $event = new DOMEvent(array(
3712                  'target' => $node->parentNode,
3713                  'type' => 'change'
3714              ));
3715          }
3716          if ($event) {
3717              phpQueryEvents::trigger($this->getDocumentID(),
3718                  $event->type, array($event), $node
3719              );
3720          }
3721      }
3722      public function attr($attr = null, $value = null) {
3723          foreach($this->stack(1) as $node) {
3724              if (! is_null($value)) {
3725                  $loop = $attr == '*'
3726                      ? $this->getNodeAttrs($node)
3727                      : array($attr);
3728                  foreach($loop as $a) {
3729                      $oldValue = $node->getAttribute($a);
3730                      $oldAttr = $node->hasAttribute($a);
3731                      // TODO raises an error when charset other than UTF-8
3732                      // while document's charset is also not UTF-8
3733                      @$node->setAttribute($a, $value);
3734                      $this->attrEvents($a, $oldAttr, $oldValue, $node);
3735                  }
3736              } else if ($attr == '*') {
3737                  // jQuery difference
3738                  $return = array();
3739                  foreach($node->attributes as $n => $v)
3740                      $return[$n] = $v->value;
3741                  return $return;
3742              } else
3743                  return $node->hasAttribute($attr)
3744                      ? $node->getAttribute($attr)
3745                      : null;
3746          }
3747          return is_null($value)
3748              ? '' : $this;
3749      }
3750      /**
3751       * @access private
3752       */
3753      protected function getNodeAttrs($node) {
3754          $return = array();
3755          foreach($node->attributes as $n => $o)
3756              $return[] = $n;
3757          return $return;
3758      }
3759      /**
3760       * Enter description here...
3761       *
3762       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3763       * @todo check CDATA ???
3764       */
3765      public function attrPHP($attr, $code) {
3766          if (! is_null($code)) {
3767              $value = '<'.'?php '.$code.' ?'.'>';
3768              // TODO tempolary solution
3769              // http://code.google.com/p/phpquery/issues/detail?id=17
3770  //          if (function_exists('mb_detect_encoding') && mb_detect_encoding($value) == 'ASCII')
3771  //              $value  = mb_convert_encoding($value, 'UTF-8', 'HTML-ENTITIES');
3772          }
3773          foreach($this->stack(1) as $node) {
3774              if (! is_null($code)) {
3775  //              $attrNode = $this->DOM->createAttribute($attr);
3776                  $node->setAttribute($attr, $value);
3777  //              $attrNode->value = $value;
3778  //              $node->appendChild($attrNode);
3779              } else if ( $attr == '*') {
3780                  // jQuery diff
3781                  $return = array();
3782                  foreach($node->attributes as $n => $v)
3783                      $return[$n] = $v->value;
3784                  return $return;
3785              } else
3786                  return $node->getAttribute($attr);
3787          }
3788          return $this;
3789      }
3790      /**
3791       * Enter description here...
3792       *
3793       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3794       */
3795      public function removeAttr($attr) {
3796          foreach($this->stack(1) as $node) {
3797              $loop = $attr == '*'
3798                  ? $this->getNodeAttrs($node)
3799                  : array($attr);
3800              foreach($loop as $a) {
3801                  $oldValue = $node->getAttribute($a);
3802                  $node->removeAttribute($a);
3803                  $this->attrEvents($a, $oldValue, null, $node);
3804              }
3805          }
3806          return $this;
3807      }
3808      /**
3809       * Return form element value.
3810       *
3811       * @return String Fields value.
3812       */
3813      public function val($val = null) {
3814          if (! isset($val)) {
3815              if ($this->eq(0)->is('select')) {
3816                      $selected = $this->eq(0)->find('option[selected=selected]');
3817                      if ($selected->is('[value]'))
3818                          return $selected->attr('value');
3819                      else
3820                          return $selected->text();
3821              } else if ($this->eq(0)->is('textarea'))
3822                      return $this->eq(0)->markup();
3823                  else
3824                      return $this->eq(0)->attr('value');
3825          } else {
3826              $_val = null;
3827              foreach($this->stack(1) as $node) {
3828                  $node = pq($node, $this->getDocumentID());
3829                  if (is_array($val) && in_array($node->attr('type'), array('checkbox', 'radio'))) {
3830                      $isChecked = in_array($node->attr('value'), $val)
3831                              || in_array($node->attr('name'), $val);
3832                      if ($isChecked)
3833                          $node->attr('checked', 'checked');
3834                      else
3835                          $node->removeAttr('checked');
3836                  } else if ($node->get(0)->tagName == 'select') {
3837                      if (! isset($_val)) {
3838                          $_val = array();
3839                          if (! is_array($val))
3840                              $_val = array((string)$val);
3841                          else
3842                              foreach($val as $v)
3843                                  $_val[] = $v;
3844                      }
3845                      foreach($node['option']->stack(1) as $option) {
3846                          $option = pq($option, $this->getDocumentID());
3847                          $selected = false;
3848                          // XXX: workaround for string comparsion, see issue #96
3849                          // http://code.google.com/p/phpquery/issues/detail?id=96
3850                          $selected = is_null($option->attr('value'))
3851                              ? in_array($option->markup(), $_val)
3852                              : in_array($option->attr('value'), $_val);
3853  //                      $optionValue = $option->attr('value');
3854  //                      $optionText = $option->text();
3855  //                      $optionTextLenght = mb_strlen($optionText);
3856  //                      foreach($_val as $v)
3857  //                          if ($optionValue == $v)
3858  //                              $selected = true;
3859  //                          else if ($optionText == $v && $optionTextLenght == mb_strlen($v))
3860  //                              $selected = true;
3861                          if ($selected)
3862                              $option->attr('selected', 'selected');
3863                          else
3864                              $option->removeAttr('selected');
3865                      }
3866                  } else if ($node->get(0)->tagName == 'textarea')
3867                      $node->markup($val);
3868                  else
3869                      $node->attr('value', $val);
3870              }
3871          }
3872          return $this;
3873      }
3874      /**
3875       * Enter description here...
3876       *
3877       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3878       */
3879      public function andSelf() {
3880          if ( $this->previous )
3881              $this->elements = array_merge($this->elements, $this->previous->elements);
3882          return $this;
3883      }
3884      /**
3885       * Enter description here...
3886       *
3887       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3888       */
3889      public function addClass( $className) {
3890          if (! $className)
3891              return $this;
3892          foreach($this->stack(1) as $node) {
3893              if (! $this->is(".$className", $node))
3894                  $node->setAttribute(
3895                      'class',
3896                      trim($node->getAttribute('class').' '.$className)
3897                  );
3898          }
3899          return $this;
3900      }
3901      /**
3902       * Enter description here...
3903       *
3904       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3905       */
3906      public function addClassPHP( $className) {
3907          foreach($this->stack(1) as $node) {
3908                  $classes = $node->getAttribute('class');
3909                  $newValue = $classes
3910                      ? $classes.' <'.'?php '.$className.' ?'.'>'
3911                      : '<'.'?php '.$className.' ?'.'>';
3912                  $node->setAttribute('class', $newValue);
3913          }
3914          return $this;
3915      }
3916      /**
3917       * Enter description here...
3918       *
3919       * @param   string  $className
3920       * @return  bool
3921       */
3922      public function hasClass($className) {
3923          foreach($this->stack(1) as $node) {
3924              if ( $this->is(".$className", $node))
3925                  return true;
3926          }
3927          return false;
3928      }
3929      /**
3930       * Enter description here...
3931       *
3932       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3933       */
3934      public function removeClass($className) {
3935          foreach($this->stack(1) as $node) {
3936              $classes = explode( ' ', $node->getAttribute('class'));
3937              if ( in_array($className, $classes)) {
3938                  $classes = array_diff($classes, array($className));
3939                  if ( $classes )
3940                      $node->setAttribute('class', implode(' ', $classes));
3941                  else
3942                      $node->removeAttribute('class');
3943              }
3944          }
3945          return $this;
3946      }
3947      /**
3948       * Enter description here...
3949       *
3950       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3951       */
3952      public function toggleClass($className) {
3953          foreach($this->stack(1) as $node) {
3954              if ( $this->is( $node, '.'.$className ))
3955                  $this->removeClass($className);
3956              else
3957                  $this->addClass($className);
3958          }
3959          return $this;
3960      }
3961      /**
3962       * Proper name without underscore (just ->empty()) also works.
3963       *
3964       * Removes all child nodes from the set of matched elements.
3965       *
3966