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