[ 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   * Shorthand for new Callback(create_function(...), ...);
1027   *
1028   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1029   */
1030  class CallbackBody extends Callback {
1031      public function __construct($paramList, $code, $param1 = null, $param2 = null,
1032              $param3 = null) {
1033          $params = func_get_args();
1034          $params = array_slice($params, 2);
1035          $this->callback = create_function($paramList, $code);
1036          $this->params = $params;
1037      }
1038  }
1039  /**
1040   * Callback type which on execution returns reference passed during creation.
1041   *
1042   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1043   */
1044  class CallbackReturnReference extends Callback
1045      implements ICallbackNamed {
1046      protected $reference;
1047      public function __construct(&$reference, $name = null){
1048          $this->reference =& $reference;
1049          $this->callback = array($this, 'callback');
1050      }
1051      public function callback() {
1052          return $this->reference;
1053      }
1054      public function getName() {
1055          return 'Callback: '.$this->name;
1056      }
1057      public function hasName() {
1058          return isset($this->name) && $this->name;
1059      }
1060  }
1061  /**
1062   * Callback type which on execution returns value passed during creation.
1063   *
1064   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1065   */
1066  class CallbackReturnValue extends Callback
1067      implements ICallbackNamed {
1068      protected $value;
1069      protected $name;
1070      public function __construct($value, $name = null){
1071          $this->value =& $value;
1072          $this->name = $name;
1073          $this->callback = array($this, 'callback');
1074      }
1075      public function callback() {
1076          return $this->value;
1077      }
1078      public function __toString() {
1079          return $this->getName();
1080      }
1081      public function getName() {
1082          return 'Callback: '.$this->name;
1083      }
1084      public function hasName() {
1085          return isset($this->name) && $this->name;
1086      }
1087  }
1088  /**
1089   * CallbackParameterToReference can be used when we don't really want a callback,
1090   * only parameter passed to it. CallbackParameterToReference takes first
1091   * parameter's value and passes it to reference.
1092   *
1093   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1094   */
1095  class CallbackParameterToReference extends Callback {
1096      /**
1097       * @param $reference
1098       * @TODO implement $paramIndex;
1099       * param index choose which callback param will be passed to reference
1100       */
1101      public function __construct(&$reference){
1102          $this->callback =& $reference;
1103      }
1104  }
1105  //class CallbackReference extends Callback {
1106  //  /**
1107  //   *
1108  //   * @param $reference
1109  //   * @param $paramIndex
1110  //   * @todo implement $paramIndex; param index choose which callback param will be passed to reference
1111  //   */
1112  //  public function __construct(&$reference, $name = null){
1113  //      $this->callback =& $reference;
1114  //  }
1115  //}
1116  class CallbackParam {}
1117  
1118  /**
1119   * Class representing phpQuery objects.
1120   *
1121   * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
1122   * @package phpQuery
1123   * @method phpQueryObject clone() clone()
1124   * @method phpQueryObject empty() empty()
1125   * @method phpQueryObject next() next($selector = null)
1126   * @method phpQueryObject prev() prev($selector = null)
1127   * @property Int $length
1128   */
1129  class phpQueryObject
1130      implements Iterator, Countable, ArrayAccess {
1131      public $documentID = null;
1132      /**
1133       * DOMDocument class.
1134       *
1135       * @var DOMDocument
1136       */
1137      public $document = null;
1138      public $charset = null;
1139      /**
1140       *
1141       * @var DOMDocumentWrapper
1142       */
1143      public $documentWrapper = null;
1144      /**
1145       * XPath interface.
1146       *
1147       * @var DOMXPath
1148       */
1149      public $xpath = null;
1150      /**
1151       * Stack of selected elements.
1152       * @TODO refactor to ->nodes
1153       * @var array
1154       */
1155      public $elements = array();
1156      /**
1157       * @access private
1158       */
1159      protected $elementsBackup = array();
1160      /**
1161       * @access private
1162       */
1163      protected $previous = null;
1164      /**
1165       * @access private
1166       * @TODO deprecate
1167       */
1168      protected $root = array();
1169      /**
1170       * Indicated if doument is just a fragment (no <html> tag).
1171       *
1172       * Every document is realy a full document, so even documentFragments can
1173       * be queried against <html>, but getDocument(id)->htmlOuter() will return
1174       * only contents of <body>.
1175       *
1176       * @var bool
1177       */
1178      public $documentFragment = true;
1179      /**
1180       * Iterator interface helper
1181       * @access private
1182       */
1183      protected $elementsInterator = array();
1184      /**
1185       * Iterator interface helper
1186       * @access private
1187       */
1188      protected $valid = false;
1189      /**
1190       * Iterator interface helper
1191       * @access private
1192       */
1193      protected $current = null;
1194      /**
1195       * Enter description here...
1196       *
1197       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1198       */
1199      public function __construct($documentID) {
1200  //      if ($documentID instanceof self)
1201  //          var_dump($documentID->getDocumentID());
1202          $id = $documentID instanceof self
1203              ? $documentID->getDocumentID()
1204              : $documentID;
1205  //      var_dump($id);
1206          if (! isset(phpQuery::$documents[$id] )) {
1207  //          var_dump(phpQuery::$documents);
1208              throw new Exception("Document with ID '{$id}' isn't loaded. Use phpQuery::newDocument(\$html) or phpQuery::newDocumentFile(\$file) first.");
1209          }
1210          $this->documentID = $id;
1211          $this->documentWrapper =& phpQuery::$documents[$id];
1212          $this->document =& $this->documentWrapper->document;
1213          $this->xpath =& $this->documentWrapper->xpath;
1214          $this->charset =& $this->documentWrapper->charset;
1215          $this->documentFragment =& $this->documentWrapper->isDocumentFragment;
1216          // TODO check $this->DOM->documentElement;
1217  //      $this->root = $this->document->documentElement;
1218          $this->root =& $this->documentWrapper->root;
1219  //      $this->toRoot();
1220          $this->elements = array($this->root);
1221      }
1222      /**
1223       *
1224       * @access private
1225       * @param $attr
1226       * @return unknown_type
1227       */
1228      public function __get($attr) {
1229          switch($attr) {
1230              // FIXME doesnt work at all ?
1231              case 'length':
1232                  return $this->size();
1233              break;
1234              default:
1235                  return $this->$attr;
1236          }
1237      }
1238      /**
1239       * Saves actual object to $var by reference.
1240       * Useful when need to break chain.
1241       * @param phpQueryObject $var
1242       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1243       */
1244      public function toReference(&$var) {
1245          return $var = $this;
1246      }
1247      public function documentFragment($state = null) {
1248          if ($state) {
1249              phpQuery::$documents[$this->getDocumentID()]['documentFragment'] = $state;
1250              return $this;
1251          }
1252          return $this->documentFragment;
1253      }
1254      /**
1255     * @access private
1256     * @TODO documentWrapper
1257       */
1258      protected function isRoot( $node) {
1259  //      return $node instanceof DOMDOCUMENT || $node->tagName == 'html';
1260          return $node instanceof DOMDOCUMENT
1261              || ($node instanceof DOMELEMENT && $node->tagName == 'html')
1262              || $this->root->isSameNode($node);
1263      }
1264      /**
1265     * @access private
1266       */
1267      protected function stackIsRoot() {
1268          return $this->size() == 1 && $this->isRoot($this->elements[0]);
1269      }
1270      /**
1271       * Enter description here...
1272       * NON JQUERY METHOD
1273       *
1274       * Watch out, it doesn't creates new instance, can be reverted with end().
1275       *
1276       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1277       */
1278      public function toRoot() {
1279          $this->elements = array($this->root);
1280          return $this;
1281  //      return $this->newInstance(array($this->root));
1282      }
1283      /**
1284       * Saves object's DocumentID to $var by reference.
1285       * <code>
1286       * $myDocumentId;
1287       * phpQuery::newDocument('<div/>')
1288       *     ->getDocumentIDRef($myDocumentId)
1289       *     ->find('div')->...
1290       * </code>
1291       *
1292       * @param unknown_type $domId
1293       * @see phpQuery::newDocument
1294       * @see phpQuery::newDocumentFile
1295       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1296       */
1297      public function getDocumentIDRef(&$documentID) {
1298          $documentID = $this->getDocumentID();
1299          return $this;
1300      }
1301      /**
1302       * Returns object with stack set to document root.
1303       *
1304       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1305       */
1306      public function getDocument() {
1307          return phpQuery::getDocument($this->getDocumentID());
1308      }
1309      /**
1310       *
1311       * @return DOMDocument
1312       */
1313      public function getDOMDocument() {
1314          return $this->document;
1315      }
1316      /**
1317       * Get object's Document ID.
1318       *
1319       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1320       */
1321      public function getDocumentID() {
1322          return $this->documentID;
1323      }
1324      /**
1325       * Unloads whole document from memory.
1326       * CAUTION! None further operations will be possible on this document.
1327       * All objects refering to it will be useless.
1328       *
1329       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1330       */
1331      public function unloadDocument() {
1332          phpQuery::unloadDocuments($this->getDocumentID());
1333      }
1334      public function isHTML() {
1335          return $this->documentWrapper->isHTML;
1336      }
1337      public function isXHTML() {
1338          return $this->documentWrapper->isXHTML;
1339      }
1340      public function isXML() {
1341          return $this->documentWrapper->isXML;
1342      }
1343      /**
1344       * Enter description here...
1345       *
1346       * @link http://docs.jquery.com/Ajax/serialize
1347       * @return string
1348       */
1349      public function serialize() {
1350          return phpQuery::param($this->serializeArray());
1351      }
1352      /**
1353       * Enter description here...
1354       *
1355       * @link http://docs.jquery.com/Ajax/serializeArray
1356       * @return array
1357       */
1358      public function serializeArray($submit = null) {
1359          $source = $this->filter('form, input, select, textarea')
1360              ->find('input, select, textarea')
1361              ->andSelf()
1362              ->not('form');
1363          $return = array();
1364  //      $source->dumpDie();
1365          foreach($source as $input) {
1366              $input = phpQuery::pq($input);
1367              if ($input->is('[disabled]'))
1368                  continue;
1369              if (!$input->is('[name]'))
1370                  continue;
1371              if ($input->is('[type=checkbox]') && !$input->is('[checked]'))
1372                  continue;
1373              // jquery diff
1374              if ($submit && $input->is('[type=submit]')) {
1375                  if ($submit instanceof DOMELEMENT && ! $input->elements[0]->isSameNode($submit))
1376                      continue;
1377                  else if (is_string($submit) && $input->attr('name') != $submit)
1378                      continue;
1379              }
1380              $return[] = array(
1381                  'name' => $input->attr('name'),
1382                  'value' => $input->val(),
1383              );
1384          }
1385          return $return;
1386      }
1387      /**
1388       * @access private
1389       */
1390      protected function debug($in) {
1391          if (! phpQuery::$debug )
1392              return;
1393          print('<pre>');
1394          print_r($in);
1395          // file debug
1396  //      file_put_contents(dirname(__FILE__).'/phpQuery.log', print_r($in, true)."\n", FILE_APPEND);
1397          // quite handy debug trace
1398  //      if ( is_array($in))
1399  //          print_r(array_slice(debug_backtrace(), 3));
1400          print("</pre>\n");
1401      }
1402      /**
1403       * @access private
1404       */
1405      protected function isRegexp($pattern) {
1406          return in_array(
1407              $pattern[ mb_strlen($pattern)-1 ],
1408              array('^','*','$')
1409          );
1410      }
1411      /**
1412       * Determines if $char is really a char.
1413       *
1414       * @param string $char
1415       * @return bool
1416       * @todo rewrite me to charcode range ! ;)
1417       * @access private
1418       */
1419      protected function isChar($char) {
1420          return extension_loaded('mbstring') && phpQuery::$mbstringSupport
1421              ? mb_eregi('\w', $char)
1422              : preg_match('@\w@', $char);
1423      }
1424      /**
1425       * @access private
1426       */
1427      protected function parseSelector($query) {
1428          // clean spaces
1429          // TODO include this inside parsing ?
1430          $query = trim(
1431              preg_replace('@\s+@', ' ',
1432                  preg_replace('@\s*(>|\\+|~)\s*@', '\\1', $query)
1433              )
1434          );
1435          $queries = array(array());
1436          if (! $query)
1437              return $queries;
1438          $return =& $queries[0];
1439          $specialChars = array('>',' ');
1440  //      $specialCharsMapping = array('/' => '>');
1441          $specialCharsMapping = array();
1442          $strlen = mb_strlen($query);
1443          $classChars = array('.', '-');
1444          $pseudoChars = array('-');
1445          $tagChars = array('*', '|', '-');
1446          // split multibyte string
1447          // http://code.google.com/p/phpquery/issues/detail?id=76
1448          $_query = array();
1449          for ($i=0; $i<$strlen; $i++)
1450              $_query[] = mb_substr($query, $i, 1);
1451          $query = $_query;
1452          // it works, but i dont like it...
1453          $i = 0;
1454          while( $i < $strlen) {
1455              $c = $query[$i];
1456              $tmp = '';
1457              // TAG
1458              if ($this->isChar($c) || in_array($c, $tagChars)) {
1459                  while(isset($query[$i])
1460                      && ($this->isChar($query[$i]) || in_array($query[$i], $tagChars))) {
1461                      $tmp .= $query[$i];
1462                      $i++;
1463                  }
1464                  $return[] = $tmp;
1465              // IDs
1466              } else if ( $c == '#') {
1467                  $i++;
1468                  while( isset($query[$i]) && ($this->isChar($query[$i]) || $query[$i] == '-')) {
1469                      $tmp .= $query[$i];
1470                      $i++;
1471                  }
1472                  $return[] = '#'.$tmp;
1473              // SPECIAL CHARS
1474              } else if (in_array($c, $specialChars)) {
1475                  $return[] = $c;
1476                  $i++;
1477              // MAPPED SPECIAL MULTICHARS
1478  //          } else if ( $c.$query[$i+1] == '//') {
1479  //              $return[] = ' ';
1480  //              $i = $i+2;
1481              // MAPPED SPECIAL CHARS
1482              } else if ( isset($specialCharsMapping[$c])) {
1483                  $return[] = $specialCharsMapping[$c];
1484                  $i++;
1485              // COMMA
1486              } else if ( $c == ',') {
1487                  $queries[] = array();
1488                  $return =& $queries[ count($queries)-1 ];
1489                  $i++;
1490                  while( isset($query[$i]) && $query[$i] == ' ')
1491                      $i++;
1492              // CLASSES
1493              } else if ($c == '.') {
1494                  while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $classChars))) {
1495                      $tmp .= $query[$i];
1496                      $i++;
1497                  }
1498                  $return[] = $tmp;
1499              // ~ General Sibling Selector
1500              } else if ($c == '~') {
1501                  $spaceAllowed = true;
1502                  $tmp .= $query[$i++];
1503                  while( isset($query[$i])
1504                      && ($this->isChar($query[$i])
1505                          || in_array($query[$i], $classChars)
1506                          || $query[$i] == '*'
1507                          || ($query[$i] == ' ' && $spaceAllowed)
1508                      )) {
1509                      if ($query[$i] != ' ')
1510                          $spaceAllowed = false;
1511                      $tmp .= $query[$i];
1512                      $i++;
1513                  }
1514                  $return[] = $tmp;
1515              // + Adjacent sibling selectors
1516              } else if ($c == '+') {
1517                  $spaceAllowed = true;
1518                  $tmp .= $query[$i++];
1519                  while( isset($query[$i])
1520                      && ($this->isChar($query[$i])
1521                          || in_array($query[$i], $classChars)
1522                          || $query[$i] == '*'
1523                          || ($spaceAllowed && $query[$i] == ' ')
1524                      )) {
1525                      if ($query[$i] != ' ')
1526                          $spaceAllowed = false;
1527                      $tmp .= $query[$i];
1528                      $i++;
1529                  }
1530                  $return[] = $tmp;
1531              // ATTRS
1532              } else if ($c == '[') {
1533                  $stack = 1;
1534                  $tmp .= $c;
1535                  while( isset($query[++$i])) {
1536                      $tmp .= $query[$i];
1537                      if ( $query[$i] == '[') {
1538                          $stack++;
1539                      } else if ( $query[$i] == ']') {
1540                          $stack--;
1541                          if (! $stack )
1542                              break;
1543                      }
1544                  }
1545                  $return[] = $tmp;
1546                  $i++;
1547              // PSEUDO CLASSES
1548              } else if ($c == ':') {
1549                  $stack = 1;
1550                  $tmp .= $query[$i++];
1551                  while( isset($query[$i]) && ($this->isChar($query[$i]) || in_array($query[$i], $pseudoChars))) {
1552                      $tmp .= $query[$i];
1553                      $i++;
1554                  }
1555                  // with arguments ?
1556                  if ( isset($query[$i]) && $query[$i] == '(') {
1557                      $tmp .= $query[$i];
1558                      $stack = 1;
1559                      while( isset($query[++$i])) {
1560                          $tmp .= $query[$i];
1561                          if ( $query[$i] == '(') {
1562                              $stack++;
1563                          } else if ( $query[$i] == ')') {
1564                              $stack--;
1565                              if (! $stack )
1566                                  break;
1567                          }
1568                      }
1569                      $return[] = $tmp;
1570                      $i++;
1571                  } else {
1572                      $return[] = $tmp;
1573                  }
1574              } else {
1575                  $i++;
1576              }
1577          }
1578          foreach($queries as $k => $q) {
1579              if (isset($q[0])) {
1580                  if (isset($q[0][0]) && $q[0][0] == ':')
1581                      array_unshift($queries[$k], '*');
1582                  if ($q[0] != '>')
1583                      array_unshift($queries[$k], ' ');
1584              }
1585          }
1586          return $queries;
1587      }
1588      /**
1589       * Return matched DOM nodes.
1590       *
1591       * @param int $index
1592       * @return array|DOMElement Single DOMElement or array of DOMElement.
1593       */
1594      public function get($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1595          $return = isset($index)
1596              ? (isset($this->elements[$index]) ? $this->elements[$index] : null)
1597              : $this->elements;
1598          // pass thou callbacks
1599          $args = func_get_args();
1600          $args = array_slice($args, 1);
1601          foreach($args as $callback) {
1602              if (is_array($return))
1603                  foreach($return as $k => $v)
1604                      $return[$k] = phpQuery::callbackRun($callback, array($v));
1605              else
1606                  $return = phpQuery::callbackRun($callback, array($return));
1607          }
1608          return $return;
1609      }
1610      /**
1611       * Return matched DOM nodes.
1612       * jQuery difference.
1613       *
1614       * @param int $index
1615       * @return array|string Returns string if $index != null
1616       * @todo implement callbacks
1617       * @todo return only arrays ?
1618       * @todo maybe other name...
1619       */
1620      public function getString($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1621          if ($index)
1622              $return = $this->eq($index)->text();
1623          else {
1624              $return = array();
1625              for($i = 0; $i < $this->size(); $i++) {
1626                  $return[] = $this->eq($i)->text();
1627              }
1628          }
1629          // pass thou callbacks
1630          $args = func_get_args();
1631          $args = array_slice($args, 1);
1632          foreach($args as $callback) {
1633              $return = phpQuery::callbackRun($callback, array($return));
1634          }
1635          return $return;
1636      }
1637      /**
1638       * Return matched DOM nodes.
1639       * jQuery difference.
1640       *
1641       * @param int $index
1642       * @return array|string Returns string if $index != null
1643       * @todo implement callbacks
1644       * @todo return only arrays ?
1645       * @todo maybe other name...
1646       */
1647      public function getStrings($index = null, $callback1 = null, $callback2 = null, $callback3 = null) {
1648          if ($index)
1649              $return = $this->eq($index)->text();
1650          else {
1651              $return = array();
1652              for($i = 0; $i < $this->size(); $i++) {
1653                  $return[] = $this->eq($i)->text();
1654              }
1655              // pass thou callbacks
1656              $args = func_get_args();
1657              $args = array_slice($args, 1);
1658          }
1659          foreach($args as $callback) {
1660              if (is_array($return))
1661                  foreach($return as $k => $v)
1662                      $return[$k] = phpQuery::callbackRun($callback, array($v));
1663              else
1664                  $return = phpQuery::callbackRun($callback, array($return));
1665          }
1666          return $return;
1667      }
1668      /**
1669       * Returns new instance of actual class.
1670       *
1671       * @param array $newStack Optional. Will replace old stack with new and move old one to history.c
1672       */
1673      public function newInstance($newStack = null) {
1674          $class = get_class($this);
1675          // support inheritance by passing old object to overloaded constructor
1676          $new = $class != 'phpQuery'
1677              ? new $class($this, $this->getDocumentID())
1678              : new phpQueryObject($this->getDocumentID());
1679          $new->previous = $this;
1680          if (is_null($newStack)) {
1681              $new->elements = $this->elements;
1682              if ($this->elementsBackup)
1683                  $this->elements = $this->elementsBackup;
1684          } else if (is_string($newStack)) {
1685              $new->elements = phpQuery::pq($newStack, $this->getDocumentID())->stack();
1686          } else {
1687              $new->elements = $newStack;
1688          }
1689          return $new;
1690      }
1691      /**
1692       * Enter description here...
1693       *
1694       * In the future, when PHP will support XLS 2.0, then we would do that this way:
1695       * contains(tokenize(@class, '\s'), "something")
1696       * @param unknown_type $class
1697       * @param unknown_type $node
1698       * @return boolean
1699       * @access private
1700       */
1701      protected function matchClasses($class, $node) {
1702          // multi-class
1703          if ( mb_strpos($class, '.', 1)) {
1704              $classes = explode('.', substr($class, 1));
1705              $classesCount = count( $classes );
1706              $nodeClasses = explode(' ', $node->getAttribute('class') );
1707              $nodeClassesCount = count( $nodeClasses );
1708              if ( $classesCount > $nodeClassesCount )
1709                  return false;
1710              $diff = count(
1711                  array_diff(
1712                      $classes,
1713                      $nodeClasses
1714                  )
1715              );
1716              if (! $diff )
1717                  return true;
1718          // single-class
1719          } else {
1720              return in_array(
1721                  // strip leading dot from class name
1722                  substr($class, 1),
1723                  // get classes for element as array
1724                  explode(' ', $node->getAttribute('class') )
1725              );
1726          }
1727      }
1728      /**
1729       * @access private
1730       */
1731      protected function runQuery($XQuery, $selector = null, $compare = null) {
1732          if ($compare && ! method_exists($this, $compare))
1733              return false;
1734          $stack = array();
1735          if (! $this->elements)
1736              $this->debug('Stack empty, skipping...');
1737  //      var_dump($this->elements[0]->nodeType);
1738          // element, document
1739          foreach($this->stack(array(1, 9, 13)) as $k => $stackNode) {
1740              $detachAfter = false;
1741              // to work on detached nodes we need temporary place them somewhere
1742              // thats because context xpath queries sucks ;]
1743              $testNode = $stackNode;
1744              while ($testNode) {
1745                  if (! $testNode->parentNode && ! $this->isRoot($testNode)) {
1746                      $this->root->appendChild($testNode);
1747                      $detachAfter = $testNode;
1748                      break;
1749                  }
1750                  $testNode = isset($testNode->parentNode)
1751                      ? $testNode->parentNode
1752                      : null;
1753              }
1754              // XXX tmp ?
1755              $xpath = $this->documentWrapper->isXHTML
1756                  ? $this->getNodeXpath($stackNode, 'html')
1757                  : $this->getNodeXpath($stackNode);
1758              // FIXME pseudoclasses-only query, support XML
1759              $query = $XQuery == '//' && $xpath == '/html[1]'
1760                  ? '//*'
1761                  : $xpath.$XQuery;
1762              $this->debug("XPATH: {$query}");
1763              // run query, get elements
1764              $nodes = $this->xpath->query($query);
1765              $this->debug("QUERY FETCHED");
1766              if (! $nodes->length )
1767                  $this->debug('Nothing found');
1768              $debug = array();
1769              foreach($nodes as $node) {
1770                  $matched = false;
1771                  if ( $compare) {
1772                      phpQuery::$debug ?
1773                          $this->debug("Found: ".$this->whois( $node ).", comparing with {$compare}()")
1774                          : null;
1775                      $phpQueryDebug = phpQuery::$debug;
1776                      phpQuery::$debug = false;
1777                      // TODO ??? use phpQuery::callbackRun()
1778                      if (call_user_func_array(array($this, $compare), array($selector, $node)))
1779                          $matched = true;
1780                      phpQuery::$debug = $phpQueryDebug;
1781                  } else {
1782                      $matched = true;
1783                  }
1784                  if ( $matched) {
1785                      if (phpQuery::$debug)
1786                          $debug[] = $this->whois( $node );
1787                      $stack[] = $node;
1788                  }
1789              }
1790              if (phpQuery::$debug) {
1791                  $this->debug("Matched ".count($debug).": ".implode(', ', $debug));
1792              }
1793              if ($detachAfter)
1794                  $this->root->removeChild($detachAfter);
1795          }
1796          $this->elements = $stack;
1797      }
1798      /**
1799       * Enter description here...
1800       *
1801       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
1802       */
1803      public function find($selectors, $context = null, $noHistory = false) {
1804          if (!$noHistory)
1805              // backup last stack /for end()/
1806              $this->elementsBackup = $this->elements;
1807          // allow to define context
1808          // TODO combine code below with phpQuery::pq() context guessing code
1809          //   as generic function
1810          if ($context) {
1811              if (! is_array($context) && $context instanceof DOMELEMENT)
1812                  $this->elements = array($context);
1813              else if (is_array($context)) {
1814                  $this->elements = array();
1815                  foreach ($context as $c)
1816                      if ($c instanceof DOMELEMENT)
1817                          $this->elements[] = $c;
1818              } else if ( $context instanceof self )
1819                  $this->elements = $context->elements;
1820          }
1821          $queries = $this->parseSelector($selectors);
1822          $this->debug(array('FIND', $selectors, $queries));
1823          $XQuery = '';
1824          // remember stack state because of multi-queries
1825          $oldStack = $this->elements;
1826          // here we will be keeping found elements
1827          $stack = array();
1828          foreach($queries as $selector) {
1829              $this->elements = $oldStack;
1830              $delimiterBefore = false;
1831              foreach($selector as $s) {
1832                  // TAG
1833                  $isTag = extension_loaded('mbstring') && phpQuery::$mbstringSupport
1834                      ? mb_ereg_match('^[\w|\||-]+$', $s) || $s == '*'
1835                      : preg_match('@^[\w|\||-]+$@', $s) || $s == '*';
1836                  if ($isTag) {
1837                      if ($this->isXML()) {
1838                          // namespace support
1839                          if (mb_strpos($s, '|') !== false) {
1840                              $ns = $tag = null;
1841                              list($ns, $tag) = explode('|', $s);
1842                              $XQuery .= "$ns:$tag";
1843                          } else if ($s == '*') {
1844                              $XQuery .= "*";
1845                          } else {
1846                              $XQuery .= "*[local-name()='$s']";
1847                          }
1848                      } else {
1849                          $XQuery .= $s;
1850                      }
1851                  // ID
1852                  } else if ($s[0] == '#') {
1853                      if ($delimiterBefore)
1854                          $XQuery .= '*';
1855                      $XQuery .= "[@id='".substr($s, 1)."']";
1856                  // ATTRIBUTES
1857                  } else if ($s[0] == '[') {
1858                      if ($delimiterBefore)
1859                          $XQuery .= '*';
1860                      // strip side brackets
1861                      $attr = trim($s, '][');
1862                      $execute = false;
1863                      // attr with specifed value
1864                      if (mb_strpos($s, '=')) {
1865                          $value = null;
1866                          list($attr, $value) = explode('=', $attr);
1867                          $value = trim($value, "'\"");
1868                          if ($this->isRegexp($attr)) {
1869                              // cut regexp character
1870                              $attr = substr($attr, 0, -1);
1871                              $execute = true;
1872                              $XQuery .= "[@{$attr}]";
1873                          } else {
1874                              $XQuery .= "[@{$attr}='{$value}']";
1875                          }
1876                      // attr without specified value
1877                      } else {
1878                          $XQuery .= "[@{$attr}]";
1879                      }
1880                      if ($execute) {
1881                          $this->runQuery($XQuery, $s, 'is');
1882                          $XQuery = '';
1883                          if (! $this->length())
1884                              break;
1885                      }
1886                  // CLASSES
1887                  } else if ($s[0] == '.') {
1888                      // TODO use return $this->find("./self::*[contains(concat(\" \",@class,\" \"), \" $class \")]");
1889                      // thx wizDom ;)
1890                      if ($delimiterBefore)
1891                          $XQuery .= '*';
1892                      $XQuery .= '[@class]';
1893                      $this->runQuery($XQuery, $s, 'matchClasses');
1894                      $XQuery = '';
1895                      if (! $this->length() )
1896                          break;
1897                  // ~ General Sibling Selector
1898                  } else if ($s[0] == '~') {
1899                      $this->runQuery($XQuery);
1900                      $XQuery = '';
1901                      $this->elements = $this
1902                          ->siblings(
1903                              substr($s, 1)
1904                          )->elements;
1905                      if (! $this->length() )
1906                          break;
1907                  // + Adjacent sibling selectors
1908                  } else if ($s[0] == '+') {
1909                      // TODO /following-sibling::
1910                      $this->runQuery($XQuery);
1911                      $XQuery = '';
1912                      $subSelector = substr($s, 1);
1913                      $subElements = $this->elements;
1914                      $this->elements = array();
1915                      foreach($subElements as $node) {
1916                          // search first DOMElement sibling
1917                          $test = $node->nextSibling;
1918                          while($test && ! ($test instanceof DOMELEMENT))
1919                              $test = $test->nextSibling;
1920                          if ($test && $this->is($subSelector, $test))
1921                              $this->elements[] = $test;
1922                      }
1923                      if (! $this->length() )
1924                          break;
1925                  // PSEUDO CLASSES
1926                  } else if ($s[0] == ':') {
1927                      // TODO optimization for :first :last
1928                      if ($XQuery) {
1929                          $this->runQuery($XQuery);
1930                          $XQuery = '';
1931                      }
1932                      if (! $this->length())
1933                          break;
1934                      $this->pseudoClasses($s);
1935                      if (! $this->length())
1936                          break;
1937                  // DIRECT DESCENDANDS
1938                  } else if ($s == '>') {
1939                      $XQuery .= '/';
1940                      $delimiterBefore = 2;
1941                  // ALL DESCENDANDS
1942                  } else if ($s == ' ') {
1943                      $XQuery .= '//';
1944                      $delimiterBefore = 2;
1945                  // ERRORS
1946                  } else {
1947                      phpQuery::debug("Unrecognized token '$s'");
1948                  }
1949                  $delimiterBefore = $delimiterBefore === 2;
1950              }
1951              // run query if any
1952              if ($XQuery && $XQuery != '//') {
1953                  $this->runQuery($XQuery);
1954                  $XQuery = '';
1955              }
1956              foreach($this->elements as $node)
1957                  if (! $this->elementsContainsNode($node, $stack))
1958                      $stack[] = $node;
1959          }
1960          $this->elements = $stack;
1961          return $this->newInstance();
1962      }
1963      /**
1964       * @todo create API for classes with pseudoselectors
1965       * @access private
1966       */
1967      protected function pseudoClasses($class) {
1968          // TODO clean args parsing ?
1969          $class = ltrim($class, ':');
1970          $haveArgs = mb_strpos($class, '(');
1971          if ($haveArgs !== false) {
1972              $args = substr($class, $haveArgs+1, -1);
1973              $class = substr($class, 0, $haveArgs);
1974          }
1975          switch($class) {
1976              case 'even':
1977              case 'odd':
1978                  $stack = array();
1979                  foreach($this->elements as $i => $node) {
1980                      if ($class == 'even' && ($i%2) == 0)
1981                          $stack[] = $node;
1982                      else if ( $class == 'odd' && $i % 2 )
1983                          $stack[] = $node;
1984                  }
1985                  $this->elements = $stack;
1986                  break;
1987              case 'eq':
1988                  $k = intval($args);
1989                  $this->elements = isset( $this->elements[$k] )
1990                      ? array( $this->elements[$k] )
1991                      : array();
1992                  break;
1993              case 'gt':
1994                  $this->elements = array_slice($this->elements, $args+1);
1995                  break;
1996              case 'lt':
1997                  $this->elements = array_slice($this->elements, 0, $args+1);
1998                  break;
1999              case 'first':
2000                  if (isset($this->elements[0]))
2001                      $this->elements = array($this->elements[0]);
2002                  break;
2003              case 'last':
2004                  if ($this->elements)
2005                      $this->elements = array($this->elements[count($this->elements)-1]);
2006                  break;
2007              /*case 'parent':
2008                  $stack = array();
2009                  foreach($this->elements as $node) {
2010                      if ( $node->childNodes->length )
2011                          $stack[] = $node;
2012                  }
2013                  $this->elements = $stack;
2014                  break;*/
2015              case 'contains':
2016                  $text = trim($args, "\"'");
2017                  $stack = array();
2018                  foreach($this->elements as $node) {
2019                      if (mb_stripos($node->textContent, $text) === false)
2020                          continue;
2021                      $stack[] = $node;
2022                  }
2023                  $this->elements = $stack;
2024                  break;
2025              case 'not':
2026                  $selector = self::unQuote($args);
2027                  $this->elements = $this->not($selector)->stack();
2028                  break;
2029              case 'slice':
2030                  // TODO jQuery difference ?
2031                  $args = explode(',',
2032                      str_replace(', ', ',', trim($args, "\"'"))
2033                  );
2034                  $start = $args[0];
2035                  $end = isset($args[1])
2036                      ? $args[1]
2037                      : null;
2038                  if ($end > 0)
2039                      $end = $end-$start;
2040                  $this->elements = array_slice($this->elements, $start, $end);
2041                  break;
2042              case 'has':
2043                  $selector = trim($args, "\"'");
2044                  $stack = array();
2045                  foreach($this->stack(1) as $el) {
2046                      if ($this->find($selector, $el, true)->length)
2047                          $stack[] = $el;
2048                  }
2049                  $this->elements = $stack;
2050                  break;
2051              case 'submit':
2052              case 'reset':
2053                  $this->elements = phpQuery::merge(
2054                      $this->map(array($this, 'is'),
2055                          "input[type=$class]", new CallbackParam()
2056                      ),
2057                      $this->map(array($this, 'is'),
2058                          "button[type=$class]", new CallbackParam()
2059                      )
2060                  );
2061              break;
2062  //              $stack = array();
2063  //              foreach($this->elements as $node)
2064  //                  if ($node->is('input[type=submit]') || $node->is('button[type=submit]'))
2065  //                      $stack[] = $el;
2066  //              $this->elements = $stack;
2067              case 'input':
2068                  $this->elements = $this->map(
2069                      array($this, 'is'),
2070                      'input', new CallbackParam()
2071                  )->elements;
2072              break;
2073              case 'password':
2074              case 'checkbox':
2075              case 'radio':
2076              case 'hidden':
2077              case 'image':
2078              case 'file':
2079                  $this->elements = $this->map(
2080                      array($this, 'is'),
2081                      "input[type=$class]", new CallbackParam()
2082                  )->elements;
2083              break;
2084              case 'parent':
2085                  $this->elements = $this->map(
2086                      create_function('$node', '
2087                          return $node instanceof DOMELEMENT && $node->childNodes->length
2088                              ? $node : null;')
2089                  )->elements;
2090              break;
2091              case 'empty':
2092                  $this->elements = $this->map(
2093                      create_function('$node', '
2094                          return $node instanceof DOMELEMENT && $node->childNodes->length
2095                              ? null : $node;')
2096                  )->elements;
2097              break;
2098              case 'disabled':
2099              case 'selected':
2100              case 'checked':
2101                  $this->elements = $this->map(
2102                      array($this, 'is'),
2103                      "[$class]", new CallbackParam()
2104                  )->elements;
2105              break;
2106              case 'enabled':
2107                  $this->elements = $this->map(
2108                      create_function('$node', '
2109                          return pq($node)->not(":disabled") ? $node : null;')
2110                  )->elements;
2111              break;
2112              case 'header':
2113                  $this->elements = $this->map(
2114                      create_function('$node',
2115                          '$isHeader = isset($node->tagName) && in_array($node->tagName, array(
2116                              "h1", "h2", "h3", "h4", "h5", "h6", "h7"
2117                          ));
2118                          return $isHeader
2119                              ? $node
2120                              : null;')
2121                  )->elements;
2122  //              $this->elements = $this->map(
2123  //                  create_function('$node', '$node = pq($node);
2124  //                      return $node->is("h1")
2125  //                          || $node->is("h2")
2126  //                          || $node->is("h3")
2127  //                          || $node->is("h4")
2128  //                          || $node->is("h5")
2129  //                          || $node->is("h6")
2130  //                          || $node->is("h7")
2131  //                          ? $node
2132  //                          : null;')
2133  //              )->elements;
2134              break;
2135              case 'only-child':
2136                  $this->elements = $this->map(
2137                      create_function('$node',
2138                          'return pq($node)->siblings()->size() == 0 ? $node : null;')
2139                  )->elements;
2140              break;
2141              case 'first-child':
2142                  $this->elements = $this->map(
2143                      create_function('$node', 'return pq($node)->prevAll()->size() == 0 ? $node : null;')
2144                  )->elements;
2145              break;
2146              case 'last-child':
2147                  $this->elements = $this->map(
2148                      create_function('$node', 'return pq($node)->nextAll()->size() == 0 ? $node : null;')
2149                  )->elements;
2150              break;
2151              case 'nth-child':
2152                  $param = trim($args, "\"'");
2153                  if (! $param)
2154                      break;
2155                      // nth-child(n+b) to nth-child(1n+b)
2156                  if ($param[0] == 'n')
2157                      $param = '1'.$param;
2158                  // :nth-child(index/even/odd/equation)
2159                  if ($param == 'even' || $param == 'odd')
2160                      $mapped = $this->map(
2161                          create_function('$node, $param',
2162                              '$index = pq($node)->prevAll()->size()+1;
2163                              if ($param == "even" && ($index%2) == 0)
2164                                  return $node;
2165                              else if ($param == "odd" && $index%2 == 1)
2166                                  return $node;
2167                              else
2168                                  return null;'),
2169                          new CallbackParam(), $param
2170                      );
2171                  else if (mb_strlen($param) > 1 && $param[1] == 'n')
2172                      // an+b
2173                      $mapped = $this->map(
2174                          create_function('$node, $param',
2175                              '$prevs = pq($node)->prevAll()->size();
2176                              $index = 1+$prevs;
2177                              $b = mb_strlen($param) > 3
2178                                  ? $param{3}
2179                                  : 0;
2180                              $a = $param{0};
2181                              if ($b && $param{2} == "-")
2182                                  $b = -$b;
2183                              if ($a > 0) {
2184                                  return ($index-$b)%$a == 0
2185                                      ? $node
2186                                      : null;
2187                                  phpQuery::debug($a."*".floor($index/$a)."+$b-1 == ".($a*floor($index/$a)+$b-1)." ?= $prevs");
2188                                  return $a*floor($index/$a)+$b-1 == $prevs
2189                                          ? $node
2190                                          : null;
2191                              } else if ($a == 0)
2192                                  return $index == $b
2193                                          ? $node
2194                                          : null;
2195                              else
2196                                  // negative value
2197                                  return $index <= $b
2198                                          ? $node
2199                                          : null;
2200  //                          if (! $b)
2201  //                              return $index%$a == 0
2202  //                                  ? $node
2203  //                                  : null;
2204  //                          else
2205  //                              return ($index-$b)%$a == 0
2206  //                                  ? $node
2207  //                                  : null;
2208                              '),
2209                          new CallbackParam(), $param
2210                      );
2211                  else
2212                      // index
2213                      $mapped = $this->map(
2214                          create_function('$node, $index',
2215                              '$prevs = pq($node)->prevAll()->size();
2216                              if ($prevs && $prevs == $index-1)
2217                                  return $node;
2218                              else if (! $prevs && $index == 1)
2219                                  return $node;
2220                              else
2221                                  return null;'),
2222                          new CallbackParam(), $param
2223                      );
2224                  $this->elements = $mapped->elements;
2225              break;
2226              default:
2227                  $this->debug("Unknown pseudoclass '{$class}', skipping...");
2228          }
2229      }
2230      /**
2231       * @access private
2232       */
2233      protected function __pseudoClassParam($paramsString) {
2234          // TODO;
2235      }
2236      /**
2237       * Enter description here...
2238       *
2239       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2240       */
2241      public function is($selector, $nodes = null) {
2242          phpQuery::debug(array("Is:", $selector));
2243          if (! $selector)
2244              return false;
2245          $oldStack = $this->elements;
2246          $returnArray = false;
2247          if ($nodes && is_array($nodes)) {
2248              $this->elements = $nodes;
2249          } else if ($nodes)
2250              $this->elements = array($nodes);
2251          $this->filter($selector, true);
2252          $stack = $this->elements;
2253          $this->elements = $oldStack;
2254          if ($nodes)
2255              return $stack ? $stack : null;
2256          return (bool)count($stack);
2257      }
2258      /**
2259       * Enter description here...
2260       * jQuery difference.
2261       *
2262       * Callback:
2263       * - $index int
2264       * - $node DOMNode
2265       *
2266       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2267       * @link http://docs.jquery.com/Traversing/filter
2268       */
2269      public function filterCallback($callback, $_skipHistory = false) {
2270          if (! $_skipHistory) {
2271              $this->elementsBackup = $this->elements;
2272              $this->debug("Filtering by callback");
2273          }
2274          $newStack = array();
2275          foreach($this->elements as $index => $node) {
2276              $result = phpQuery::callbackRun($callback, array($index, $node));
2277              if (is_null($result) || (! is_null($result) && $result))
2278                  $newStack[] = $node;
2279          }
2280          $this->elements = $newStack;
2281          return $_skipHistory
2282              ? $this
2283              : $this->newInstance();
2284      }
2285      /**
2286       * Enter description here...
2287       *
2288       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2289       * @link http://docs.jquery.com/Traversing/filter
2290       */
2291      public function filter($selectors, $_skipHistory = false) {
2292          if ($selectors instanceof Callback OR $selectors instanceof Closure)
2293              return $this->filterCallback($selectors, $_skipHistory);
2294          if (! $_skipHistory)
2295              $this->elementsBackup = $this->elements;
2296          $notSimpleSelector = array(' ', '>', '~', '+', '/');
2297          if (! is_array($selectors))
2298              $selectors = $this->parseSelector($selectors);
2299          if (! $_skipHistory)
2300              $this->debug(array("Filtering:", $selectors));
2301          $finalStack = array();
2302          foreach($selectors as $selector) {
2303              $stack = array();
2304              if (! $selector)
2305                  break;
2306              // avoid first space or /
2307              if (in_array($selector[0], $notSimpleSelector))
2308                  $selector = array_slice($selector, 1);
2309              // PER NODE selector chunks
2310              foreach($this->stack() as $node) {
2311                  $break = false;
2312                  foreach($selector as $s) {
2313                      if (!($node instanceof DOMELEMENT)) {
2314                          // all besides DOMElement
2315                          if ( $s[0] == '[') {
2316                              $attr = trim($s, '[]');
2317                              if ( mb_strpos($attr, '=')) {
2318                                  list( $attr, $val ) = explode('=', $attr);
2319                                  if ($attr == 'nodeType' && $node->nodeType != $val)
2320                                      $break = true;
2321                              }
2322                          } else
2323                              $break = true;
2324                      } else {
2325                          // DOMElement only
2326                          // ID
2327                          if ( $s[0] == '#') {
2328                              if ( $node->getAttribute('id') != substr($s, 1) )
2329                                  $break = true;
2330                          // CLASSES
2331                          } else if ( $s[0] == '.') {
2332                              if (! $this->matchClasses( $s, $node ) )
2333                                  $break = true;
2334                          // ATTRS
2335                          } else if ( $s[0] == '[') {
2336                              // strip side brackets
2337                              $attr = trim($s, '[]');
2338                              if (mb_strpos($attr, '=')) {
2339                                  list($attr, $val) = explode('=', $attr);
2340                                  $val = self::unQuote($val);
2341                                  if ($attr == 'nodeType') {
2342                                      if ($val != $node->nodeType)
2343                                          $break = true;
2344                                  } else if ($this->isRegexp($attr)) {
2345                                      $val = extension_loaded('mbstring') && phpQuery::$mbstringSupport
2346                                          ? quotemeta(trim($val, '"\''))
2347                                          : preg_quote(trim($val, '"\''), '@');
2348                                      // switch last character
2349                                      switch( substr($attr, -1)) {
2350                                          // quotemeta used insted of preg_quote
2351                                          // http://code.google.com/p/phpquery/issues/detail?id=76
2352                                          case '^':
2353                                              $pattern = '^'.$val;
2354                                              break;
2355                                          case '*':
2356                                              $pattern = '.*'.$val.'.*';
2357                                              break;
2358                                          case '$':
2359                                              $pattern = '.*'.$val.'$';
2360                                              break;
2361                                      }
2362                                      // cut last character
2363                                      $attr = substr($attr, 0, -1);
2364                                      $isMatch = extension_loaded('mbstring') && phpQuery::$mbstringSupport
2365                                          ? mb_ereg_match($pattern, $node->getAttribute($attr))
2366                                          : preg_match("@{$pattern}@", $node->getAttribute($attr));
2367                                      if (! $isMatch)
2368                                          $break = true;
2369                                  } else if ($node->getAttribute($attr) != $val)
2370                                      $break = true;
2371                              } else if (! $node->hasAttribute($attr))
2372                                  $break = true;
2373                          // PSEUDO CLASSES
2374                          } else if ( $s[0] == ':') {
2375                              // skip
2376                          // TAG
2377                          } else if (trim($s)) {
2378                              if ($s != '*') {
2379                                  // TODO namespaces
2380                                  if (isset($node->tagName)) {
2381                                      if ($node->tagName != $s)
2382                                          $break = true;
2383                                  } else if ($s == 'html' && ! $this->isRoot($node))
2384                                      $break = true;
2385                              }
2386                          // AVOID NON-SIMPLE SELECTORS
2387                          } else if (in_array($s, $notSimpleSelector)) {
2388                              $break = true;
2389                              $this->debug(array('Skipping non simple selector', $selector));
2390                          }
2391                      }
2392                      if ($break)
2393                          break;
2394                  }
2395                  // if element passed all chunks of selector - add it to new stack
2396                  if (! $break )
2397                      $stack[] = $node;
2398              }
2399              $tmpStack = $this->elements;
2400              $this->elements = $stack;
2401              // PER ALL NODES selector chunks
2402              foreach($selector as $s)
2403                  // PSEUDO CLASSES
2404                  if ($s[0] == ':')
2405                      $this->pseudoClasses($s);
2406              foreach($this->elements as $node)
2407                  // XXX it should be merged without duplicates
2408                  // but jQuery doesnt do that
2409                  $finalStack[] = $node;
2410              $this->elements = $tmpStack;
2411          }
2412          $this->elements = $finalStack;
2413          if ($_skipHistory) {
2414              return $this;
2415          } else {
2416              $this->debug("Stack length after filter(): ".count($finalStack));
2417              return $this->newInstance();
2418          }
2419      }
2420      /**
2421       *
2422       * @param $value
2423       * @return unknown_type
2424       * @TODO implement in all methods using passed parameters
2425       */
2426      protected static function unQuote($value) {
2427          return $value[0] == '\'' || $value[0] == '"'
2428              ? substr($value, 1, -1)
2429              : $value;
2430      }
2431      /**
2432       * Enter description here...
2433       *
2434       * @link http://docs.jquery.com/Ajax/load
2435       * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2436       * @todo Support $selector
2437       */
2438      public function load($url, $data = null, $callback = null) {
2439          if ($data && ! is_array($data)) {
2440              $callback = $data;
2441              $data = null;
2442          }
2443          if (mb_strpos($url, ' ') !== false) {
2444              $matches = null;
2445              if (extension_loaded('mbstring') && phpQuery::$mbstringSupport)
2446                  mb_ereg('^([^ ]+) (.*)$', $url, $matches);
2447              else
2448                  preg_match('^([^ ]+) (.*)$', $url, $matches);
2449              $url = $matches[1];
2450              $selector = $matches[2];
2451              // FIXME this sucks, pass as callback param
2452              $this->_loadSelector = $selector;
2453          }
2454          $ajax = array(
2455              'url' => $url,
2456              'type' => $data ? 'POST' : 'GET',
2457              'data' => $data,
2458              'complete' => $callback,
2459              'success' => array($this, '__loadSuccess')
2460          );
2461          phpQuery::ajax($ajax);
2462          return $this;
2463      }
2464      /**
2465       * @access private
2466       * @param $html
2467       * @return unknown_type
2468       */
2469      public function __loadSuccess($html) {
2470          if ($this->_loadSelector) {
2471              $html = phpQuery::newDocument($html)->find($this->_loadSelector);
2472              unset($this->_loadSelector);
2473          }
2474          foreach($this->stack(1) as $node) {
2475              phpQuery::pq($node, $this->getDocumentID())
2476                  ->markup($html);
2477          }
2478      }
2479      /**
2480       * Enter description here...
2481       *
2482       * @return phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2483       * @todo
2484       */
2485      public function css() {
2486          // TODO
2487          return $this;
2488      }
2489      /**
2490       * @todo
2491       *
2492       */
2493      public function show(){
2494          // TODO
2495          return $this;
2496      }
2497      /**
2498       * @todo
2499       *
2500       */
2501      public function hide(){
2502          // TODO
2503          return $this;
2504      }
2505      /**
2506       * Trigger a type of event on every matched element.
2507       *
2508       * @param unknown_type $type
2509       * @param unknown_type $data
2510       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2511       * @TODO support more than event in $type (space-separated)
2512       */
2513      public function trigger($type, $data = array()) {
2514          foreach($this->elements as $node)
2515              phpQueryEvents::trigger($this->getDocumentID(), $type, $data, $node);
2516          return $this;
2517      }
2518      /**
2519       * This particular method triggers all bound event handlers on an element (for a specific event type) WITHOUT executing the browsers default actions.
2520       *
2521       * @param unknown_type $type
2522       * @param unknown_type $data
2523       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2524       * @TODO
2525       */
2526      public function triggerHandler($type, $data = array()) {
2527          // TODO;
2528      }
2529      /**
2530       * Binds a handler to one or more events (like click) for each matched element.
2531       * Can also bind custom events.
2532       *
2533       * @param unknown_type $type
2534       * @param unknown_type $data Optional
2535       * @param unknown_type $callback
2536       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2537       * @TODO support '!' (exclusive) events
2538       * @TODO support more than event in $type (space-separated)
2539       */
2540      public function bind($type, $data, $callback = null) {
2541          // TODO check if $data is callable, not using is_callable
2542          if (! isset($callback)) {
2543              $callback = $data;
2544              $data = null;
2545          }
2546          foreach($this->elements as $node)
2547              phpQueryEvents::add($this->getDocumentID(), $node, $type, $data, $callback);
2548          return $this;
2549      }
2550      /**
2551       * Enter description here...
2552       *
2553       * @param unknown_type $type
2554       * @param unknown_type $callback
2555       * @return unknown
2556       * @TODO namespace events
2557       * @TODO support more than event in $type (space-separated)
2558       */
2559      public function unbind($type = null, $callback = null) {
2560          foreach($this->elements as $node)
2561              phpQueryEvents::remove($this->getDocumentID(), $node, $type, $callback);
2562          return $this;
2563      }
2564      /**
2565       * Enter description here...
2566       *
2567       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2568       */
2569      public function change($callback = null) {
2570          if ($callback)
2571              return $this->bind('change', $callback);
2572          return $this->trigger('change');
2573      }
2574      /**
2575       * Enter description here...
2576       *
2577       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2578       */
2579      public function submit($callback = null) {
2580          if ($callback)
2581              return $this->bind('submit', $callback);
2582          return $this->trigger('submit');
2583      }
2584      /**
2585       * Enter description here...
2586       *
2587       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2588       */
2589      public function click($callback = null) {
2590          if ($callback)
2591              return $this->bind('click', $callback);
2592          return $this->trigger('click');
2593      }
2594      /**
2595       * Enter description here...
2596       *
2597       * @param String|phpQuery
2598       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2599       */
2600      public function wrapAllOld($wrapper) {
2601          $wrapper = pq($wrapper)->_clone();
2602          if (! $wrapper->length() || ! $this->length() )
2603              return $this;
2604          $wrapper->insertBefore($this->elements[0]);
2605          $deepest = $wrapper->elements[0];
2606          while($deepest->firstChild && $deepest->firstChild instanceof DOMELEMENT)
2607              $deepest = $deepest->firstChild;
2608          pq($deepest)->append($this);
2609          return $this;
2610      }
2611      /**
2612       * Enter description here...
2613       *
2614       * TODO testme...
2615       * @param String|phpQuery
2616       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2617       */
2618      public function wrapAll($wrapper) {
2619          if (! $this->length())
2620              return $this;
2621          return phpQuery::pq($wrapper, $this->getDocumentID())
2622              ->clone()
2623              ->insertBefore($this->get(0))
2624              ->map(array($this, '___wrapAllCallback'))
2625              ->append($this);
2626      }
2627    /**
2628     *
2629       * @param $node
2630       * @return unknown_type
2631       * @access private
2632     */
2633      public function ___wrapAllCallback($node) {
2634          $deepest = $node;
2635          while($deepest->firstChild && $deepest->firstChild instanceof DOMELEMENT)
2636              $deepest = $deepest->firstChild;
2637          return $deepest;
2638      }
2639      /**
2640       * Enter description here...
2641       * NON JQUERY METHOD
2642       *
2643       * @param String|phpQuery
2644       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2645       */
2646      public function wrapAllPHP($codeBefore, $codeAfter) {
2647          return $this
2648              ->slice(0, 1)
2649                  ->beforePHP($codeBefore)
2650              ->end()
2651              ->slice(-1)
2652                  ->afterPHP($codeAfter)
2653              ->end();
2654      }
2655      /**
2656       * Enter description here...
2657       *
2658       * @param String|phpQuery
2659       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2660       */
2661      public function wrap($wrapper) {
2662          foreach($this->stack() as $node)
2663              phpQuery::pq($node, $this->getDocumentID())->wrapAll($wrapper);
2664          return $this;
2665      }
2666      /**
2667       * Enter description here...
2668       *
2669       * @param String|phpQuery
2670       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2671       */
2672      public function wrapPHP($codeBefore, $codeAfter) {
2673          foreach($this->stack() as $node)
2674              phpQuery::pq($node, $this->getDocumentID())->wrapAllPHP($codeBefore, $codeAfter);
2675          return $this;
2676      }
2677      /**
2678       * Enter description here...
2679       *
2680       * @param String|phpQuery
2681       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2682       */
2683      public function wrapInner($wrapper) {
2684          foreach($this->stack() as $node)
2685              phpQuery::pq($node, $this->getDocumentID())->contents()->wrapAll($wrapper);
2686          return $this;
2687      }
2688      /**
2689       * Enter description here...
2690       *
2691       * @param String|phpQuery
2692       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2693       */
2694      public function wrapInnerPHP($codeBefore, $codeAfter) {
2695          foreach($this->stack(1) as $node)
2696              phpQuery::pq($node, $this->getDocumentID())->contents()
2697                  ->wrapAllPHP($codeBefore, $codeAfter);
2698          return $this;
2699      }
2700      /**
2701       * Enter description here...
2702       *
2703       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2704       * @testme Support for text nodes
2705       */
2706      public function contents() {
2707          $stack = array();
2708          foreach($this->stack(1) as $el) {
2709              // FIXME (fixed) http://code.google.com/p/phpquery/issues/detail?id=56
2710  //          if (! isset($el->childNodes))
2711  //              continue;
2712              foreach($el->childNodes as $node) {
2713                  $stack[] = $node;
2714              }
2715          }
2716          return $this->newInstance($stack);
2717      }
2718      /**
2719       * Enter description here...
2720       *
2721       * jQuery difference.
2722       *
2723       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2724       */
2725      public function contentsUnwrap() {
2726          foreach($this->stack(1) as $node) {
2727              if (! $node->parentNode )
2728                  continue;
2729              $childNodes = array();
2730              // any modification in DOM tree breaks childNodes iteration, so cache them first
2731              foreach($node->childNodes as $chNode )
2732                  $childNodes[] = $chNode;
2733              foreach($childNodes as $chNode )
2734  //              $node->parentNode->appendChild($chNode);
2735                  $node->parentNode->insertBefore($chNode, $node);
2736              $node->parentNode->removeChild($node);
2737          }
2738          return $this;
2739      }
2740      /**
2741       * Enter description here...
2742       *
2743       * jQuery difference.
2744       */
2745      public function switchWith($markup) {
2746          $markup = pq($markup, $this->getDocumentID());
2747          $content = null;
2748          foreach($this->stack(1) as $node) {
2749              pq($node)
2750                  ->contents()->toReference($content)->end()
2751                  ->replaceWith($markup->clone()->append($content));
2752          }
2753          return $this;
2754      }
2755      /**
2756       * Enter description here...
2757       *
2758       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2759       */
2760      public function eq($num) {
2761          $oldStack = $this->elements;
2762          $this->elementsBackup = $this->elements;
2763          $this->elements = array();
2764          if ( isset($oldStack[$num]) )
2765              $this->elements[] = $oldStack[$num];
2766          return $this->newInstance();
2767      }
2768      /**
2769       * Enter description here...
2770       *
2771       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2772       */
2773      public function size() {
2774          return count($this->elements);
2775      }
2776      /**
2777       * Enter description here...
2778       *
2779       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2780       * @deprecated Use length as attribute
2781       */
2782      public function length() {
2783          return $this->size();
2784      }
2785      public function count() {
2786          return $this->size();
2787      }
2788      /**
2789       * Enter description here...
2790       *
2791       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2792       * @todo $level
2793       */
2794      public function end($level = 1) {
2795  //      $this->elements = array_pop( $this->history );
2796  //      return $this;
2797  //      $this->previous->DOM = $this->DOM;
2798  //      $this->previous->XPath = $this->XPath;
2799          return $this->previous
2800              ? $this->previous
2801              : $this;
2802      }
2803      /**
2804       * Enter description here...
2805       * Normal use ->clone() .
2806       *
2807       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2808       * @access private
2809       */
2810      public function _clone() {
2811          $newStack = array();
2812          //pr(array('copy... ', $this->whois()));
2813          //$this->dumpHistory('copy');
2814          $this->elementsBackup = $this->elements;
2815          foreach($this->elements as $node) {
2816              $newStack[] = $node->cloneNode(true);
2817          }
2818          $this->elements = $newStack;
2819          return $this->newInstance();
2820      }
2821      /**
2822       * Enter description here...
2823       *
2824       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2825       */
2826      public function replaceWithPHP($code) {
2827          return $this->replaceWith(phpQuery::php($code));
2828      }
2829      /**
2830       * Enter description here...
2831       *
2832       * @param String|phpQuery $content
2833       * @link http://docs.jquery.com/Manipulation/replaceWith#content
2834       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2835       */
2836      public function replaceWith($content) {
2837          return $this->after($content)->remove();
2838      }
2839      /**
2840       * Enter description here...
2841       *
2842       * @param String $selector
2843       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2844       * @todo this works ?
2845       */
2846      public function replaceAll($selector) {
2847          foreach(phpQuery::pq($selector, $this->getDocumentID()) as $node)
2848              phpQuery::pq($node, $this->getDocumentID())
2849                  ->after($this->_clone())
2850                  ->remove();
2851          return $this;
2852      }
2853      /**
2854       * Enter description here...
2855       *
2856       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2857       */
2858      public function remove($selector = null) {
2859          $loop = $selector
2860              ? $this->filter($selector)->elements
2861              : $this->elements;
2862          foreach($loop as $node) {
2863              if (! $node->parentNode )
2864                  continue;
2865              if (isset($node->tagName))
2866                  $this->debug("Removing '{$node->tagName}'");
2867              $node->parentNode->removeChild($node);
2868              // Mutation event
2869              $event = new DOMEvent(array(
2870                  'target' => $node,
2871                  'type' => 'DOMNodeRemoved'
2872              ));
2873              phpQueryEvents::trigger($this->getDocumentID(),
2874                  $event->type, array($event), $node
2875              );
2876          }
2877          return $this;
2878      }
2879      protected function markupEvents($newMarkup, $oldMarkup, $node) {
2880          if ($node->tagName == 'textarea' && $newMarkup != $oldMarkup) {
2881              $event = new DOMEvent(array(
2882                  'target' => $node,
2883                  'type' => 'change'
2884              ));
2885              phpQueryEvents::trigger($this->getDocumentID(),
2886                  $event->type, array($event), $node
2887              );
2888          }
2889      }
2890      /**
2891       * jQuey difference
2892       *
2893       * @param $markup
2894       * @return unknown_type
2895       * @TODO trigger change event for textarea
2896       */
2897      public function markup($markup = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2898          $args = func_get_args();
2899          if ($this->documentWrapper->isXML)
2900              return call_user_func_array(array($this, 'xml'), $args);
2901          else
2902              return call_user_func_array(array($this, 'html'), $args);
2903      }
2904      /**
2905       * jQuey difference
2906       *
2907       * @param $markup
2908       * @return unknown_type
2909       */
2910      public function markupOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2911          $args = func_get_args();
2912          if ($this->documentWrapper->isXML)
2913              return call_user_func_array(array($this, 'xmlOuter'), $args);
2914          else
2915              return call_user_func_array(array($this, 'htmlOuter'), $args);
2916      }
2917      /**
2918       * Enter description here...
2919       *
2920       * @param unknown_type $html
2921       * @return string|phpQuery|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2922       * @TODO force html result
2923       */
2924      public function html($html = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2925          if (isset($html)) {
2926              // INSERT
2927              $nodes = $this->documentWrapper->import($html);
2928              $this->empty();
2929              foreach($this->stack(1) as $alreadyAdded => $node) {
2930                  // for now, limit events for textarea
2931                  if (($this->isXHTML() || $this->isHTML()) && $node->tagName == 'textarea')
2932                      $oldHtml = pq($node, $this->getDocumentID())->markup();
2933                  foreach($nodes as $newNode) {
2934                      $node->appendChild($alreadyAdded
2935                          ? $newNode->cloneNode(true)
2936                          : $newNode
2937                      );
2938                  }
2939                  // for now, limit events for textarea
2940                  if (($this->isXHTML() || $this->isHTML()) && $node->tagName == 'textarea')
2941                      $this->markupEvents($html, $oldHtml, $node);
2942              }
2943              return $this;
2944          } else {
2945              // FETCH
2946              $return = $this->documentWrapper->markup($this->elements, true);
2947              $args = func_get_args();
2948              foreach(array_slice($args, 1) as $callback) {
2949                  $return = phpQuery::callbackRun($callback, array($return));
2950              }
2951              return $return;
2952          }
2953      }
2954      /**
2955       * @TODO force xml result
2956       */
2957      public function xml($xml = null, $callback1 = null, $callback2 = null, $callback3 = null) {
2958          $args = func_get_args();
2959          return call_user_func_array(array($this, 'html'), $args);
2960      }
2961      /**
2962       * Enter description here...
2963       * @TODO force html result
2964       *
2965       * @return String
2966       */
2967      public function htmlOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2968          $markup = $this->documentWrapper->markup($this->elements);
2969          // pass thou callbacks
2970          $args = func_get_args();
2971          foreach($args as $callback) {
2972              $markup = phpQuery::callbackRun($callback, array($markup));
2973          }
2974          return $markup;
2975      }
2976      /**
2977       * @TODO force xml result
2978       */
2979      public function xmlOuter($callback1 = null, $callback2 = null, $callback3 = null) {
2980          $args = func_get_args();
2981          return call_user_func_array(array($this, 'htmlOuter'), $args);
2982      }
2983      public function __toString() {
2984          return $this->markupOuter();
2985      }
2986      /**
2987       * Just like html(), but returns markup with VALID (dangerous) PHP tags.
2988       *
2989       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
2990       * @todo support returning markup with PHP tags when called without param
2991       */
2992      public function php($code = null) {
2993          return $this->markupPHP($code);
2994      }
2995      /**
2996       * Enter description here...
2997       *
2998       * @param $code
2999       * @return unknown_type
3000       */
3001      public function markupPHP($code = null) {
3002          return isset($code)
3003              ? $this->markup(phpQuery::php($code))
3004              : phpQuery::markupToPHP($this->markup());
3005      }
3006      /**
3007       * Enter description here...
3008       *
3009       * @param $code
3010       * @return unknown_type
3011       */
3012      public function markupOuterPHP() {
3013          return phpQuery::markupToPHP($this->markupOuter());
3014      }
3015      /**
3016       * Enter description here...
3017       *
3018       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3019       */
3020      public function children($selector = null) {
3021          $stack = array();
3022          foreach($this->stack(1) as $node) {
3023  //          foreach($node->getElementsByTagName('*') as $newNode) {
3024              foreach($node->childNodes as $newNode) {
3025                  if ($newNode->nodeType != 1)
3026                      continue;
3027                  if ($selector && ! $this->is($selector, $newNode))
3028                      continue;
3029                  if ($this->elementsContainsNode($newNode, $stack))
3030                      continue;
3031                  $stack[] = $newNode;
3032              }
3033          }
3034          $this->elementsBackup = $this->elements;
3035          $this->elements = $stack;
3036          return $this->newInstance();
3037      }
3038      /**
3039       * Enter description here...
3040       *
3041       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3042       */
3043      public function ancestors($selector = null) {
3044          return $this->children( $selector );
3045      }
3046      /**
3047       * Enter description here...
3048       *
3049       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3050       */
3051      public function append( $content) {
3052          return $this->insert($content, __FUNCTION__);
3053      }
3054      /**
3055       * Enter description here...
3056       *
3057       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3058       */
3059      public function appendPHP( $content) {
3060          return $this->insert("<php><!-- {$content} --></php>", 'append');
3061      }
3062      /**
3063       * Enter description here...
3064       *
3065       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3066       */
3067      public function appendTo( $seletor) {
3068          return $this->insert($seletor, __FUNCTION__);
3069      }
3070      /**
3071       * Enter description here...
3072       *
3073       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3074       */
3075      public function prepend( $content) {
3076          return $this->insert($content, __FUNCTION__);
3077      }
3078      /**
3079       * Enter description here...
3080       *
3081       * @todo accept many arguments, which are joined, arrays maybe also
3082       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3083       */
3084      public function prependPHP( $content) {
3085          return $this->insert("<php><!-- {$content} --></php>", 'prepend');
3086      }
3087      /**
3088       * Enter description here...
3089       *
3090       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3091       */
3092      public function prependTo( $seletor) {
3093          return $this->insert($seletor, __FUNCTION__);
3094      }
3095      /**
3096       * Enter description here...
3097       *
3098       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3099       */
3100      public function before($content) {
3101          return $this->insert($content, __FUNCTION__);
3102      }
3103      /**
3104       * Enter description here...
3105       *
3106       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3107       */
3108      public function beforePHP( $content) {
3109          return $this->insert("<php><!-- {$content} --></php>", 'before');
3110      }
3111      /**
3112       * Enter description here...
3113       *
3114       * @param String|phpQuery
3115       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3116       */
3117      public function insertBefore( $seletor) {
3118          return $this->insert($seletor, __FUNCTION__);
3119      }
3120      /**
3121       * Enter description here...
3122       *
3123       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3124       */
3125      public function after( $content) {
3126          return $this->insert($content, __FUNCTION__);
3127      }
3128      /**
3129       * Enter description here...
3130       *
3131       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3132       */
3133      public function afterPHP( $content) {
3134          return $this->insert("<php><!-- {$content} --></php>", 'after');
3135      }
3136      /**
3137       * Enter description here...
3138       *
3139       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3140       */
3141      public function insertAfter( $seletor) {
3142          return $this->insert($seletor, __FUNCTION__);
3143      }
3144      /**
3145       * Internal insert method. Don't use it.
3146       *
3147       * @param unknown_type $target
3148       * @param unknown_type $type
3149       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3150       * @access private
3151       */
3152      public function insert($target, $type) {
3153          $this->debug("Inserting data with '{$type}'");
3154          $to = false;
3155          switch( $type) {
3156              case 'appendTo':
3157              case 'prependTo':
3158              case 'insertBefore':
3159              case 'insertAfter':
3160                  $to = true;
3161          }
3162          switch(gettype($target)) {
3163              case 'string':
3164                  $insertFrom = $insertTo = array();
3165                  if ($to) {
3166                      // INSERT TO
3167                      $insertFrom = $this->elements;
3168                      if (phpQuery::isMarkup($target)) {
3169                          // $target is new markup, import it
3170                          $insertTo = $this->documentWrapper->import($target);
3171                      // insert into selected element
3172                      } else {
3173                          // $tagret is a selector
3174                          $thisStack = $this->elements;
3175                          $this->toRoot();
3176                          $insertTo = $this->find($target)->elements;
3177                          $this->elements = $thisStack;
3178                      }
3179                  } else {
3180                      // INSERT FROM
3181                      $insertTo = $this->elements;
3182                      $insertFrom = $this->documentWrapper->import($target);
3183                  }
3184                  break;
3185              case 'object':
3186                  $insertFrom = $insertTo = array();
3187                  // phpQuery
3188                  if ($target instanceof self) {
3189                      if ($to) {
3190                          $insertTo = $target->elements;
3191                          if ($this->documentFragment && $this->stackIsRoot())
3192                              // get all body children
3193  //                          $loop = $this->find('body > *')->elements;
3194                              // TODO test it, test it hard...
3195  //                          $loop = $this->newInstance($this->root)->find('> *')->elements;
3196                              $loop = $this->root->childNodes;
3197                          else
3198                              $loop = $this->elements;
3199                          // import nodes if needed
3200                          $insertFrom = $this->getDocumentID() == $target->getDocumentID()
3201                              ? $loop
3202                              : $target->documentWrapper->import($loop);
3203                      } else {
3204                          $insertTo = $this->elements;
3205                          if ( $target->documentFragment && $target->stackIsRoot() )
3206                              // get all body children
3207  //                          $loop = $target->find('body > *')->elements;
3208                              $loop = $target->root->childNodes;
3209                          else
3210                              $loop = $target->elements;
3211                          // import nodes if needed
3212                          $insertFrom = $this->getDocumentID() == $target->getDocumentID()
3213                              ? $loop
3214                              : $this->documentWrapper->import($loop);
3215                      }
3216                  // DOMNODE
3217                  } elseif ($target instanceof DOMNODE) {
3218                      // import node if needed
3219  //                  if ( $target->ownerDocument != $this->DOM )
3220  //                      $target = $this->DOM->importNode($target, true);
3221                      if ( $to) {
3222                          $insertTo = array($target);
3223                          if ($this->documentFragment && $this->stackIsRoot())
3224                              // get all body children
3225                              $loop = $this->root->childNodes;
3226  //                          $loop = $this->find('body > *')->elements;
3227                          else
3228                              $loop = $this->elements;
3229                          foreach($loop as $fromNode)
3230                              // import nodes if needed
3231                              $insertFrom[] = ! $fromNode->ownerDocument->isSameNode($target->ownerDocument)
3232                                  ? $target->ownerDocument->importNode($fromNode, true)
3233                                  : $fromNode;
3234                      } else {
3235                          // import node if needed
3236                          if (! $target->ownerDocument->isSameNode($this->document))
3237                              $target = $this->document->importNode($target, true);
3238                          $insertTo = $this->elements;
3239                          $insertFrom[] = $target;
3240                      }
3241                  }
3242                  break;
3243          }
3244          phpQuery::debug("From ".count($insertFrom)."; To ".count($insertTo)." nodes");
3245          foreach($insertTo as $insertNumber => $toNode) {
3246              // we need static relative elements in some cases
3247              switch( $type) {
3248                  case 'prependTo':
3249                  case 'prepend':
3250                      $firstChild = $toNode->firstChild;
3251                      break;
3252                  case 'insertAfter':
3253                  case 'after':
3254                      $nextSibling = $toNode->nextSibling;
3255                      break;
3256              }
3257              foreach($insertFrom as $fromNode) {
3258                  // clone if inserted already before
3259                  $insert = $insertNumber
3260                      ? $fromNode->cloneNode(true)
3261                      : $fromNode;
3262                  switch($type) {
3263                      case 'appendTo':
3264                      case 'append':
3265  //                      $toNode->insertBefore(
3266  //                          $fromNode,
3267  //                          $toNode->lastChild->nextSibling
3268  //                      );
3269                          $toNode->appendChild($insert);
3270                          $eventTarget = $insert;
3271                          break;
3272                      case 'prependTo':
3273                      case 'prepend':
3274                          $toNode->insertBefore(
3275                              $insert,
3276                              $firstChild
3277                          );
3278                          break;
3279                      case 'insertBefore':
3280                      case 'before':
3281                          if (! $toNode->parentNode)
3282                              throw new Exception("No parentNode, can't do {$type}()");
3283                          else
3284                              $toNode->parentNode->insertBefore(
3285                                  $insert,
3286                                  $toNode
3287                              );
3288                          break;
3289                      case 'insertAfter':
3290                      case 'after':
3291                          if (! $toNode->parentNode)
3292                              throw new Exception("No parentNode, can't do {$type}()");
3293                          else
3294                              $toNode->parentNode->insertBefore(
3295                                  $insert,
3296                                  $nextSibling
3297                              );
3298                          break;
3299                  }
3300                  // Mutation event
3301                  $event = new DOMEvent(array(
3302                      'target' => $insert,
3303                      'type' => 'DOMNodeInserted'
3304                  ));
3305                  phpQueryEvents::trigger($this->getDocumentID(),
3306                      $event->type, array($event), $insert
3307                  );
3308              }
3309          }
3310          return $this;
3311      }
3312      /**
3313       * Enter description here...
3314       *
3315       * @return Int
3316       */
3317      public function index($subject) {
3318          $index = -1;
3319          $subject = $subject instanceof phpQueryObject
3320              ? $subject->elements[0]
3321              : $subject;
3322          foreach($this->newInstance() as $k => $node) {
3323              if ($node->isSameNode($subject))
3324                  $index = $k;
3325          }
3326          return $index;
3327      }
3328      /**
3329       * Enter description here...
3330       *
3331       * @param unknown_type $start
3332       * @param unknown_type $end
3333       *
3334       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3335       * @testme
3336       */
3337      public function slice($start, $end = null) {
3338  //      $last = count($this->elements)-1;
3339  //      $end = $end
3340  //          ? min($end, $last)
3341  //          : $last;
3342  //      if ($start < 0)
3343  //          $start = $last+$start;
3344  //      if ($start > $last)
3345  //          return array();
3346          if ($end > 0)
3347              $end = $end-$start;
3348          return $this->newInstance(
3349              array_slice($this->elements, $start, $end)
3350          );
3351      }
3352      /**
3353       * Enter description here...
3354       *
3355       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3356       */
3357      public function reverse() {
3358          $this->elementsBackup = $this->elements;
3359          $this->elements = array_reverse($this->elements);
3360          return $this->newInstance();
3361      }
3362      /**
3363       * Return joined text content.
3364       * @return String
3365       */
3366      public function text($text = null, $callback1 = null, $callback2 = null, $callback3 = null) {
3367          if (isset($text))
3368              return $this->html(htmlspecialchars($text));
3369          $args = func_get_args();
3370          $args = array_slice($args, 1);
3371          $return = '';
3372          foreach($this->elements as $node) {
3373              $text = $node->textContent;
3374              if (count($this->elements) > 1 && $text)
3375                  $text .= "\n";
3376              foreach($args as $callback) {
3377                  $text = phpQuery::callbackRun($callback, array($text));
3378              }
3379              $return .= $text;
3380          }
3381          return $return;
3382      }
3383      /**
3384       * Enter description here...
3385       *
3386       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3387       */
3388      public function plugin($class, $file = null) {
3389          phpQuery::plugin($class, $file);
3390          return $this;
3391      }
3392      /**
3393       * Deprecated, use $pq->plugin() instead.
3394       *
3395       * @deprecated
3396       * @param $class
3397       * @param $file
3398       * @return unknown_type
3399       */
3400      public static function extend($class, $file = null) {
3401          return $this->plugin($class, $file);
3402      }
3403      /**
3404       *
3405       * @access private
3406       * @param $method
3407       * @param $args
3408       * @return unknown_type
3409       */
3410      public function __call($method, $args) {
3411          $aliasMethods = array('clone', 'empty');
3412          if (isset(phpQuery::$extendMethods[$method])) {
3413              array_unshift($args, $this);
3414              return phpQuery::callbackRun(
3415                  phpQuery::$extendMethods[$method], $args
3416              );
3417          } else if (isset(phpQuery::$pluginsMethods[$method])) {
3418              array_unshift($args, $this);
3419              $class = phpQuery::$pluginsMethods[$method];
3420              $realClass = "phpQueryObjectPlugin_$class";
3421              $return = call_user_func_array(
3422                  array($realClass, $method),
3423                  $args
3424              );
3425              // XXX deprecate ?
3426              return is_null($return)
3427                  ? $this
3428                  : $return;
3429          } else if (in_array($method, $aliasMethods)) {
3430              return call_user_func_array(array($this, '_'.$method), $args);
3431          } else
3432              throw new Exception("Method '{$method}' doesnt exist");
3433      }
3434      /**
3435       * Safe rename of next().
3436       *
3437       * Use it ONLY when need to call next() on an iterated object (in same time).
3438       * Normaly there is no need to do such thing ;)
3439       *
3440       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3441       * @access private
3442       */
3443      public function _next($selector = null) {
3444          return $this->newInstance(
3445              $this->getElementSiblings('nextSibling', $selector, true)
3446          );
3447      }
3448      /**
3449       * Use prev() and next().
3450       *
3451       * @deprecated
3452       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3453       * @access private
3454       */
3455      public function _prev($selector = null) {
3456          return $this->prev($selector);
3457      }
3458      /**
3459       * Enter description here...
3460       *
3461       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3462       */
3463      public function prev($selector = null) {
3464          return $this->newInstance(
3465              $this->getElementSiblings('previousSibling', $selector, true)
3466          );
3467      }
3468      /**
3469       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3470       * @todo
3471       */
3472      public function prevAll($selector = null) {
3473          return $this->newInstance(
3474              $this->getElementSiblings('previousSibling', $selector)
3475          );
3476      }
3477      /**
3478       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3479       * @todo FIXME: returns source elements insted of next siblings
3480       */
3481      public function nextAll($selector = null) {
3482          return $this->newInstance(
3483              $this->getElementSiblings('nextSibling', $selector)
3484          );
3485      }
3486      /**
3487       * @access private
3488       */
3489      protected function getElementSiblings($direction, $selector = null, $limitToOne = false) {
3490          $stack = array();
3491          $count = 0;
3492          foreach($this->stack() as $node) {
3493              $test = $node;
3494              while( isset($test->{$direction}) && $test->{$direction}) {
3495                  $test = $test->{$direction};
3496                  if (! $test instanceof DOMELEMENT)
3497                      continue;
3498                  $stack[] = $test;
3499                  if ($limitToOne)
3500                      break;
3501              }
3502          }
3503          if ($selector) {
3504              $stackOld = $this->elements;
3505              $this->elements = $stack;
3506              $stack = $this->filter($selector, true)->stack();
3507              $this->elements = $stackOld;
3508          }
3509          return $stack;
3510      }
3511      /**
3512       * Enter description here...
3513       *
3514       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3515       */
3516      public function siblings($selector = null) {
3517          $stack = array();
3518          $siblings = array_merge(
3519              $this->getElementSiblings('previousSibling', $selector),
3520              $this->getElementSiblings('nextSibling', $selector)
3521          );
3522          foreach($siblings as $node) {
3523              if (! $this->elementsContainsNode($node, $stack))
3524                  $stack[] = $node;
3525          }
3526          return $this->newInstance($stack);
3527      }
3528      /**
3529       * Enter description here...
3530       *
3531       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3532       */
3533      public function not($selector = null) {
3534          if (is_string($selector))
3535              phpQuery::debug(array('not', $selector));
3536          else
3537              phpQuery::debug('not');
3538          $stack = array();
3539          if ($selector instanceof self || $selector instanceof DOMNODE) {
3540              foreach($this->stack() as $node) {
3541                  if ($selector instanceof self) {
3542                      $matchFound = false;
3543                      foreach($selector->stack() as $notNode) {
3544                          if ($notNode->isSameNode($node))
3545                              $matchFound = true;
3546                      }
3547                      if (! $matchFound)
3548                          $stack[] = $node;
3549                  } else if ($selector instanceof DOMNODE) {
3550                      if (! $selector->isSameNode($node))
3551                          $stack[] = $node;
3552                  } else {
3553                      if (! $this->is($selector))
3554                          $stack[] = $node;
3555                  }
3556              }
3557          } else {
3558              $orgStack = $this->stack();
3559              $matched = $this->filter($selector, true)->stack();
3560  //          $matched = array();
3561  //          // simulate OR in filter() instead of AND 5y
3562  //          foreach($this->parseSelector($selector) as $s) {
3563  //              $matched = array_merge($matched,
3564  //                  $this->filter(array($s))->stack()
3565  //              );
3566  //          }
3567              foreach($orgStack as $node)
3568                  if (! $this->elementsContainsNode($node, $matched))
3569                      $stack[] = $node;
3570          }
3571          return $this->newInstance($stack);
3572      }
3573      /**
3574       * Enter description here...
3575       *
3576       * @param string|phpQueryObject
3577       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3578       */
3579      public function add($selector = null) {
3580          if (! $selector)
3581              return $this;
3582          $stack = array();
3583          $this->elementsBackup = $this->elements;
3584          $found = phpQuery::pq($selector, $this->getDocumentID());
3585          $this->merge($found->elements);
3586          return $this->newInstance();
3587      }
3588      /**
3589       * @access private
3590       */
3591      protected function merge() {
3592          foreach(func_get_args() as $nodes)
3593              foreach($nodes as $newNode )
3594                  if (! $this->elementsContainsNode($newNode) )
3595                      $this->elements[] = $newNode;
3596      }
3597      /**
3598       * @access private
3599       * TODO refactor to stackContainsNode
3600       */
3601      protected function elementsContainsNode($nodeToCheck, $elementsStack = null) {
3602          $loop = ! is_null($elementsStack)
3603              ? $elementsStack
3604              : $this->elements;
3605          foreach($loop as $node) {
3606              if ( $node->isSameNode( $nodeToCheck ) )
3607                  return true;
3608          }
3609          return false;
3610      }
3611      /**
3612       * Enter description here...
3613       *
3614       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3615       */
3616      public function parent($selector = null) {
3617          $stack = array();
3618          foreach($this->elements as $node )
3619              if ( $node->parentNode && ! $this->elementsContainsNode($node->parentNode, $stack) )
3620                  $stack[] = $node->parentNode;
3621          $this->elementsBackup = $this->elements;
3622          $this->elements = $stack;
3623          if ( $selector )
3624              $this->filter($selector, true);
3625          return $this->newInstance();
3626      }
3627      /**
3628       * Enter description here...
3629       *
3630       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3631       */
3632      public function parents($selector = null) {
3633          $stack = array();
3634          if (! $this->elements )
3635              $this->debug('parents() - stack empty');
3636          foreach($this->elements as $node) {
3637              $test = $node;
3638              while( $test->parentNode) {
3639                  $test = $test->parentNode;
3640                  if ($this->isRoot($test))
3641                      break;
3642                  if (! $this->elementsContainsNode($test, $stack)) {
3643                      $stack[] = $test;
3644                      continue;
3645                  }
3646              }
3647          }
3648          $this->elementsBackup = $this->elements;
3649          $this->elements = $stack;
3650          if ( $selector )
3651              $this->filter($selector, true);
3652          return $this->newInstance();
3653      }
3654      /**
3655       * Internal stack iterator.
3656       *
3657       * @access private
3658       */
3659      public function stack($nodeTypes = null) {
3660          if (!isset($nodeTypes))
3661              return $this->elements;
3662          if (!is_array($nodeTypes))
3663              $nodeTypes = array($nodeTypes);
3664          $return = array();
3665          foreach($this->elements as $node) {
3666              if (in_array($node->nodeType, $nodeTypes))
3667                  $return[] = $node;
3668          }
3669          return $return;
3670      }
3671      // TODO phpdoc; $oldAttr is result of hasAttribute, before any changes
3672      protected function attrEvents($attr, $oldAttr, $oldValue, $node) {
3673          // skip events for XML documents
3674          if (! $this->isXHTML() && ! $this->isHTML())
3675              return;
3676          $event = null;
3677          // identify
3678          $isInputValue = $node->tagName == 'input'
3679              && (
3680                  in_array($node->getAttribute('type'),
3681                      array('text', 'password', 'hidden'))
3682                  || !$node->getAttribute('type')
3683                   );
3684          $isRadio = $node->tagName == 'input'
3685              && $node->getAttribute('type') == 'radio';
3686          $isCheckbox = $node->tagName == 'input'
3687              && $node->getAttribute('type') == 'checkbox';
3688          $isOption = $node->tagName == 'option';
3689          if ($isInputValue && $attr == 'value' && $oldValue != $node->getAttribute($attr)) {
3690              $event = new DOMEvent(array(
3691                  'target' => $node,
3692                  'type' => 'change'
3693              ));
3694          } else if (($isRadio || $isCheckbox) && $attr == 'checked' && (
3695                  // check
3696                  (! $oldAttr && $node->hasAttribute($attr))
3697                  // un-check
3698                  || (! $node->hasAttribute($attr) && $oldAttr)
3699              )) {
3700              $event = new DOMEvent(array(
3701                  'target' => $node,
3702                  'type' => 'change'
3703              ));
3704          } else if ($isOption && $node->parentNode && $attr == 'selected' && (
3705                  // select
3706                  (! $oldAttr && $node->hasAttribute($attr))
3707                  // un-select
3708                  || (! $node->hasAttribute($attr) && $oldAttr)
3709              )) {
3710              $event = new DOMEvent(array(
3711                  'target' => $node->parentNode,
3712                  'type' => 'change'
3713              ));
3714          }
3715          if ($event) {
3716              phpQueryEvents::trigger($this->getDocumentID(),
3717                  $event->type, array($event), $node
3718              );
3719          }
3720      }
3721      public function attr($attr = null, $value = null) {
3722          foreach($this->stack(1) as $node) {
3723              if (! is_null($value)) {
3724                  $loop = $attr == '*'
3725                      ? $this->getNodeAttrs($node)
3726                      : array($attr);
3727                  foreach($loop as $a) {
3728                      $oldValue = $node->getAttribute($a);
3729                      $oldAttr = $node->hasAttribute($a);
3730                      // TODO raises an error when charset other than UTF-8
3731                      // while document's charset is also not UTF-8
3732                      @$node->setAttribute($a, $value);
3733                      $this->attrEvents($a, $oldAttr, $oldValue, $node);
3734                  }
3735              } else if ($attr == '*') {
3736                  // jQuery difference
3737                  $return = array();
3738                  foreach($node->attributes as $n => $v)
3739                      $return[$n] = $v->value;
3740                  return $return;
3741              } else
3742                  return $node->hasAttribute($attr)
3743                      ? $node->getAttribute($attr)
3744                      : null;
3745          }
3746          return is_null($value)
3747              ? '' : $this;
3748      }
3749      /**
3750       * @access private
3751       */
3752      protected function getNodeAttrs($node) {
3753          $return = array();
3754          foreach($node->attributes as $n => $o)
3755              $return[] = $n;
3756          return $return;
3757      }
3758      /**
3759       * Enter description here...
3760       *
3761       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3762       * @todo check CDATA ???
3763       */
3764      public function attrPHP($attr, $code) {
3765          if (! is_null($code)) {
3766              $value = '<'.'?php '.$code.' ?'.'>';
3767              // TODO tempolary solution
3768              // http://code.google.com/p/phpquery/issues/detail?id=17
3769  //          if (function_exists('mb_detect_encoding') && mb_detect_encoding($value) == 'ASCII')
3770  //              $value  = mb_convert_encoding($value, 'UTF-8', 'HTML-ENTITIES');
3771          }
3772          foreach($this->stack(1) as $node) {
3773              if (! is_null($code)) {
3774  //              $attrNode = $this->DOM->createAttribute($attr);
3775                  $node->setAttribute($attr, $value);
3776  //              $attrNode->value = $value;
3777  //              $node->appendChild($attrNode);
3778              } else if ( $attr == '*') {
3779                  // jQuery diff
3780                  $return = array();
3781                  foreach($node->attributes as $n => $v)
3782                      $return[$n] = $v->value;
3783                  return $return;
3784              } else
3785                  return $node->getAttribute($attr);
3786          }
3787          return $this;
3788      }
3789      /**
3790       * Enter description here...
3791       *
3792       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3793       */
3794      public function removeAttr($attr) {
3795          foreach($this->stack(1) as $node) {
3796              $loop = $attr == '*'
3797                  ? $this->getNodeAttrs($node)
3798                  : array($attr);
3799              foreach($loop as $a) {
3800                  $oldValue = $node->getAttribute($a);
3801                  $node->removeAttribute($a);
3802                  $this->attrEvents($a, $oldValue, null, $node);
3803              }
3804          }
3805          return $this;
3806      }
3807      /**
3808       * Return form element value.
3809       *
3810       * @return String Fields value.
3811       */
3812      public function val($val = null) {
3813          if (! isset($val)) {
3814              if ($this->eq(0)->is('select')) {
3815                      $selected = $this->eq(0)->find('option[selected=selected]');
3816                      if ($selected->is('[value]'))
3817                          return $selected->attr('value');
3818                      else
3819                          return $selected->text();
3820              } else if ($this->eq(0)->is('textarea'))
3821                      return $this->eq(0)->markup();
3822                  else
3823                      return $this->eq(0)->attr('value');
3824          } else {
3825              $_val = null;
3826              foreach($this->stack(1) as $node) {
3827                  $node = pq($node, $this->getDocumentID());
3828                  if (is_array($val) && in_array($node->attr('type'), array('checkbox', 'radio'))) {
3829                      $isChecked = in_array($node->attr('value'), $val)
3830                              || in_array($node->attr('name'), $val);
3831                      if ($isChecked)
3832                          $node->attr('checked', 'checked');
3833                      else
3834                          $node->removeAttr('checked');
3835                  } else if ($node->get(0)->tagName == 'select') {
3836                      if (! isset($_val)) {
3837                          $_val = array();
3838                          if (! is_array($val))
3839                              $_val = array((string)$val);
3840                          else
3841                              foreach($val as $v)
3842                                  $_val[] = $v;
3843                      }
3844                      foreach($node['option']->stack(1) as $option) {
3845                          $option = pq($option, $this->getDocumentID());
3846                          $selected = false;
3847                          // XXX: workaround for string comparsion, see issue #96
3848                          // http://code.google.com/p/phpquery/issues/detail?id=96
3849                          $selected = is_null($option->attr('value'))
3850                              ? in_array($option->markup(), $_val)
3851                              : in_array($option->attr('value'), $_val);
3852  //                      $optionValue = $option->attr('value');
3853  //                      $optionText = $option->text();
3854  //                      $optionTextLenght = mb_strlen($optionText);
3855  //                      foreach($_val as $v)
3856  //                          if ($optionValue == $v)
3857  //                              $selected = true;
3858  //                          else if ($optionText == $v && $optionTextLenght == mb_strlen($v))
3859  //                              $selected = true;
3860                          if ($selected)
3861                              $option->attr('selected', 'selected');
3862                          else
3863                              $option->removeAttr('selected');
3864                      }
3865                  } else if ($node->get(0)->tagName == 'textarea')
3866                      $node->markup($val);
3867                  else
3868                      $node->attr('value', $val);
3869              }
3870          }
3871          return $this;
3872      }
3873      /**
3874       * Enter description here...
3875       *
3876       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3877       */
3878      public function andSelf() {
3879          if ( $this->previous )
3880              $this->elements = array_merge($this->elements, $this->previous->elements);
3881          return $this;
3882      }
3883      /**
3884       * Enter description here...
3885       *
3886       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3887       */
3888      public function addClass( $className) {
3889          if (! $className)
3890              return $this;
3891          foreach($this->stack(1) as $node) {
3892              if (! $this->is(".$className", $node))
3893                  $node->setAttribute(
3894                      'class',
3895                      trim($node->getAttribute('class').' '.$className)
3896                  );
3897          }
3898          return $this;
3899      }
3900      /**
3901       * Enter description here...
3902       *
3903       * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
3904       */