NULL, 'omit_xml_declaration' => FALSE, 'replace_entities' => FALSE, 'exception_level' => 771, // E_ERROR | E_USER_ERROR | E_USER_WARNING | E_WARNING 'ignore_parser_warnings' => FALSE, 'escape_xhtml_js_css_sections' => self::JS_CSS_ESCAPE_CDATA_CCOMMENT, ); /** * The array of matches. */ protected $matches = array(); /** * The last array of matches. */ protected $last = array(); // Last set of matches. private $ext = array(); // Extensions array. /** * The number of current matches. * * @see count() */ public $length = 0; /** * Constructor. * * Typically, a new DOMQuery is created by QueryPath::with(), QueryPath::withHTML(), * qp(), or htmlqp(). * * @param mixed $document * A document-like object. * @param string $string * A CSS 3 Selector * @param array $options * An associative array of options. * @see qp() */ public function __construct($document = NULL, $string = NULL, $options = array()) { $string = trim($string); $this->options = $options + Options::get() + $this->options; $parser_flags = isset($options['parser_flags']) ? $options['parser_flags'] : self::DEFAULT_PARSER_FLAGS; if (!empty($this->options['ignore_parser_warnings'])) { // Don't convert parser warnings into exceptions. $this->errTypes = 257; //E_ERROR | E_USER_ERROR; } elseif (isset($this->options['exception_level'])) { // Set the error level at which exceptions will be thrown. By default, // QueryPath will throw exceptions for // E_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING. $this->errTypes = $this->options['exception_level']; } // Empty: Just create an empty QP. if (empty($document)) { $this->document = isset($this->options['encoding']) ? new \DOMDocument('1.0', $this->options['encoding']) : new \DOMDocument(); $this->setMatches(new \SplObjectStorage()); } // Figure out if document is DOM, HTML/XML, or a filename elseif (is_object($document)) { // This is the most frequent object type. if ($document instanceof \SplObjectStorage) { $this->matches = $document; if ($document->count() != 0) { $first = $this->getFirstMatch(); if (!empty($first->ownerDocument)) { $this->document = $first->ownerDocument; } } } elseif ($document instanceof DOMQuery) { //$this->matches = $document->get(NULL, TRUE); $this->setMatches($document->get(NULL, TRUE)); if ($this->matches->count() > 0) $this->document = $this->getFirstMatch()->ownerDocument; } elseif ($document instanceof \DOMDocument) { $this->document = $document; //$this->matches = $this->matches($document->documentElement); $this->setMatches($document->documentElement); } elseif ($document instanceof \DOMNode) { $this->document = $document->ownerDocument; //$this->matches = array($document); $this->setMatches($document); } elseif ($document instanceof \Masterminds\HTML5) { $this->document = $document; $this->setMatches($document->documentElement); } elseif ($document instanceof \SimpleXMLElement) { $import = dom_import_simplexml($document); $this->document = $import->ownerDocument; //$this->matches = array($import); $this->setMatches($import); } else { throw new \QueryPath\Exception('Unsupported class type: ' . get_class($document)); } } elseif (is_array($document)) { //trigger_error('Detected deprecated array support', E_USER_NOTICE); if (!empty($document) && $document[0] instanceof \DOMNode) { $found = new \SplObjectStorage(); foreach ($document as $item) $found->attach($item); //$this->matches = $found; $this->setMatches($found); $this->document = $this->getFirstMatch()->ownerDocument; } } elseif ($this->isXMLish($document)) { // $document is a string with XML $this->document = $this->parseXMLString($document); $this->setMatches($this->document->documentElement); } else { // $document is a filename $context = empty($options['context']) ? NULL : $options['context']; $this->document = $this->parseXMLFile($document, $parser_flags, $context); $this->setMatches($this->document->documentElement); } // Globally set the output option. if (isset($this->options['format_output']) && $this->options['format_output'] == FALSE) { $this->document->formatOutput = FALSE; } else { $this->document->formatOutput = TRUE; } // Do a find if the second param was set. if (isset($string) && strlen($string) > 0) { // We don't issue a find because that creates a new DOMQuery. //$this->find($string); $query = new \QueryPath\CSS\DOMTraverser($this->matches); $query->find($string); $this->setMatches($query->matches()); } } /** * Get the effective options for the current DOMQuery object. * * This returns an associative array of all of the options as set * for the current DOMQuery object. This includes default options, * options directly passed in via {@link qp()} or the constructor, * an options set in the QueryPath::Options object. * * The order of merging options is this: * - Options passed in using qp() are highest priority, and will * override other options. * - Options set with QueryPath::Options will override default options, * but can be overridden by options passed into qp(). * - Default options will be used when no overrides are present. * * This function will return the options currently used, with the above option * overriding having been calculated already. * * @return array * An associative array of options, calculated from defaults and overridden * options. * @see qp() * @see QueryPath::Options::set() * @see QueryPath::Options::merge() * @since 2.0 */ public function getOptions() { return $this->options; } /** * Select the root element of the document. * * This sets the current match to the document's root element. For * practical purposes, this is the same as: * @code * qp($someDoc)->find(':root'); * @endcode * However, since it doesn't invoke a parser, it has less overhead. It also * works in cases where the QueryPath has been reduced to zero elements (a * case that is not handled by find(':root') because there is no element * whose root can be found). * * @param string $selector * A selector. If this is supplied, QueryPath will navigate to the * document root and then run the query. (Added in QueryPath 2.0 Beta 2) * @return \QueryPath\DOMQuery * The DOMQuery object, wrapping the root element (document element) * for the current document. */ public function top($selector = NULL) { //$this->setMatches($this->document->documentElement); //return !empty($selector) ? $this->find($selector) : $this; return $this->inst($this->document->documentElement, $selector, $this->options); } /** * Given a CSS Selector, find matching items. * * @param string $selector * CSS 3 Selector * @return \QueryPath\DOMQuery * @see filter() * @see is() * @todo If a find() returns zero matches, then a subsequent find() will * also return zero matches, even if that find has a selector like :root. * The reason for this is that the {@link QueryPathEventHandler} does * not set the root of the document tree if it cannot find any elements * from which to determine what the root is. The workaround is to use * {@link top()} to select the root element again. */ public function find($selector) { //$query = new QueryPathEventHandler($this->matches); $query = new \QueryPath\CSS\DOMTraverser($this->matches); $query->find($selector); //$this->setMatches($query->matches()); //return $this; return $this->inst($query->matches(), NULL , $this->options); } public function findInPlace($selector) { $query = new \QueryPath\CSS\DOMTraverser($this->matches); $query->find($selector); $this->setMatches($query->matches()); return $this; } /** * Execute an XPath query and store the results in the QueryPath. * * Most methods in this class support CSS 3 Selectors. Sometimes, though, * XPath provides a finer-grained query language. Use this to execute * XPath queries. * * Beware, though. DOMQuery works best on DOM Elements, but an XPath * query can return other nodes, strings, and values. These may not work with * other QueryPath functions (though you will be able to access the * values with {@link get()}). * * @param string $query * An XPath query. * @param array $options * Currently supported options are: * - 'namespace_prefix': And XML namespace prefix to be used as the default. Used * in conjunction with 'namespace_uri' * - 'namespace_uri': The URI to be used as the default namespace URI. Used * with 'namespace_prefix' * @return \QueryPath\DOMQuery * A DOMQuery object wrapping the results of the query. * @see find() * @author M Butcher * @author Xavier Prud'homme */ public function xpath($query, $options = array()) { $xpath = new \DOMXPath($this->document); // Register a default namespace. if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) { $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']); } $found = new \SplObjectStorage(); foreach ($this->matches as $item) { $nl = $xpath->query($query, $item); if ($nl->length > 0) { for ($i = 0; $i < $nl->length; ++$i) $found->attach($nl->item($i)); } } return $this->inst($found, NULL, $this->options); //$this->setMatches($found); //return $this; } /** * Get the number of elements currently wrapped by this object. * * Note that there is no length property on this object. * * @return int * Number of items in the object. * @deprecated QueryPath now implements Countable, so use count(). */ public function size() { return $this->matches->count(); } /** * Get the number of elements currently wrapped by this object. * * Since DOMQuery is Countable, the PHP count() function can also * be used on a DOMQuery. * * @code * * @endcode * * @return int * The number of matches in the DOMQuery. */ public function count() { return $this->matches->count(); } /** * Get one or all elements from this object. * * When called with no paramaters, this returns all objects wrapped by * the DOMQuery. Typically, these are DOMElement objects (unless you have * used map(), xpath(), or other methods that can select * non-elements). * * When called with an index, it will return the item in the DOMQuery with * that index number. * * Calling this method does not change the DOMQuery (e.g. it is * non-destructive). * * You can use qp()->get() to iterate over all elements matched. You can * also iterate over qp() itself (DOMQuery implementations must be Traversable). * In the later case, though, each item * will be wrapped in a DOMQuery object. To learn more about iterating * in QueryPath, see {@link examples/techniques.php}. * * @param int $index * If specified, then only this index value will be returned. If this * index is out of bounds, a NULL will be returned. * @param boolean $asObject * If this is TRUE, an SplObjectStorage object will be returned * instead of an array. This is the preferred method for extensions to use. * @return mixed * If an index is passed, one element will be returned. If no index is * present, an array of all matches will be returned. * @see eq() * @see SplObjectStorage */ public function get($index = NULL, $asObject = FALSE) { if (isset($index)) { return ($this->size() > $index) ? $this->getNthMatch($index) : NULL; } // Retain support for legacy. if (!$asObject) { $matches = array(); foreach ($this->matches as $m) $matches[] = $m; return $matches; } return $this->matches; } /** * Get the namespace of the current element. * * If QP is currently pointed to a list of elements, this will get the * namespace of the first element. */ public function ns() { return $this->get(0)->namespaceURI; } /** * Get the DOMDocument that we currently work with. * * This returns the current DOMDocument. Any changes made to this document will be * accessible to DOMQuery, as both will share access to the same object. * * @return DOMDocument */ public function document() { return $this->document; } /** * On an XML document, load all XIncludes. * * @return \QueryPath\DOMQuery */ public function xinclude() { $this->document->xinclude(); return $this; } /** * Get all current elements wrapped in an array. * Compatibility function for jQuery 1.4, but identical to calling {@link get()} * with no parameters. * * @return array * An array of DOMNodes (typically DOMElements). */ public function toArray() { return $this->get(); } /** * Get/set an attribute. * - If no parameters are specified, this returns an associative array of all * name/value pairs. * - If both $name and $value are set, then this will set the attribute name/value * pair for all items in this object. * - If $name is set, and is an array, then * all attributes in the array will be set for all items in this object. * - If $name is a string and is set, then the attribute value will be returned. * * When an attribute value is retrieved, only the attribute value of the FIRST * match is returned. * * @param mixed $name * The name of the attribute or an associative array of name/value pairs. * @param string $value * A value (used only when setting an individual property). * @return mixed * If this was a setter request, return the DOMQuery object. If this was * an access request (getter), return the string value. * @see removeAttr() * @see tag() * @see hasAttr() * @see hasClass() */ public function attr($name = NULL, $value = NULL) { // Default case: Return all attributes as an assoc array. if (is_null($name)) { if ($this->matches->count() == 0) return NULL; $ele = $this->getFirstMatch(); $buffer = array(); // This does not appear to be part of the DOM // spec. Nor is it documented. But it works. foreach ($ele->attributes as $name => $attrNode) { $buffer[$name] = $attrNode->value; } return $buffer; } // multi-setter if (is_array($name)) { foreach ($name as $k => $v) { foreach ($this->matches as $m) $m->setAttribute($k, $v); } return $this; } // setter if (isset($value)) { foreach ($this->matches as $m) $m->setAttribute($name, $value); return $this; } //getter if ($this->matches->count() == 0) return NULL; // Special node type handler: if ($name == 'nodeType') { return $this->getFirstMatch()->nodeType; } // Always return first match's attr. return $this->getFirstMatch()->getAttribute($name); } /** * Check to see if the given attribute is present. * * This returns TRUE if all selected items have the attribute, or * FALSE if at least one item does not have the attribute. * * @param string $attrName * The attribute name. * @return boolean * TRUE if all matches have the attribute, FALSE otherwise. * @since 2.0 * @see attr() * @see hasClass() */ public function hasAttr($attrName) { foreach ($this->matches as $match) { if (!$match->hasAttribute($attrName)) return FALSE; } return TRUE; } /** * Set/get a CSS value for the current element(s). * This sets the CSS value for each element in the DOMQuery object. * It does this by setting (or getting) the style attribute (without a namespace). * * For example, consider this code: * @code * css('background-color','red')->html(); * ?> * @endcode * This will return the following HTML: * @code *
This is the body ** @endcode * * If no parameters are passed into this function, then the current style * element will be returned unparsed. Example: * @code * css('background-color','red')->css(); * ?> * @endcode * This will return the following: * @code * background-color: red * @endcode * * As of QueryPath 2.1, existing style attributes will be merged with new attributes. * (In previous versions of QueryPath, a call to css() overwrite the existing style * values). * * @param mixed $name * If this is a string, it will be used as a CSS name. If it is an array, * this will assume it is an array of name/value pairs of CSS rules. It will * apply all rules to all elements in the set. * @param string $value * The value to set. This is only set if $name is a string. * @return \QueryPath\DOMQuery */ public function css($name = NULL, $value = '') { if (empty($name)) { return $this->attr('style'); } // Get any existing CSS. $css = array(); foreach ($this->matches as $match) { $style = $match->getAttribute('style'); if (!empty($style)) { // XXX: Is this sufficient? $style_array = explode(';', $style); foreach ($style_array as $item) { $item = trim($item); // Skip empty attributes. if (strlen($item) == 0) continue; list($css_att, $css_val) = explode(':',$item, 2); $css[$css_att] = trim($css_val); } } } if (is_array($name)) { // Use array_merge instead of + to preserve order. $css = array_merge($css, $name); } else { $css[$name] = $value; } // Collapse CSS into a string. $format = '%s: %s;'; $css_string = ''; foreach ($css as $n => $v) { $css_string .= sprintf($format, $n, trim($v)); } $this->attr('style', $css_string); return $this; } /** * Insert or retrieve a Data URL. * * When called with just $attr, it will fetch the result, attempt to decode it, and * return an array with the MIME type and the application data. * * When called with both $attr and $data, it will inject the data into all selected elements * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject * the given PNG image into the selected elements. * * The current implementation only knows how to encode and decode Base 64 data. * * Note that this is known *not* to work on IE 6, but should render fine in other browsers. * * @param string $attr * The name of the attribute. * @param mixed $data * The contents to inject as the data. The value can be any one of the following: * - A URL: If this is given, then the subsystem will read the content from that URL. THIS * MUST BE A FULL URL, not a relative path. * - A string of data: If this is given, then the subsystem will encode the string. * - A stream or file handle: If this is given, the stream's contents will be encoded * and inserted as data. * (Note that we make the assumption here that you would never want to set data to be * a URL. If this is an incorrect assumption, file a bug.) * @param string $mime * The MIME type of the document. * @param resource $context * A valid context. Use this only if you need to pass a stream context. This is only necessary * if $data is a URL. (See {@link stream_context_create()}). * @return \QueryPath\DOMQuery|string * If this is called as a setter, this will return a DOMQuery object. Otherwise, it * will attempt to fetch data out of the attribute and return that. * @see http://en.wikipedia.org/wiki/Data:_URL * @see attr() * @since 2.1 */ public function dataURL($attr, $data = NULL, $mime = 'application/octet-stream', $context = NULL) { if (is_null($data)) { // Attempt to fetch the data $data = $this->attr($attr); if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) { return; } // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data. $regex = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/'; $matches = array(); preg_match($regex, $data, $matches); if (!empty($matches)) { $result = array( 'mime' => $matches[1] . '/' . $matches[2], 'data' => base64_decode($matches[3]), ); return $result; } } else { $attVal = \QueryPath::encodeDataURL($data, $mime, $context); return $this->attr($attr, $attVal); } } /** * Remove the named attribute from all elements in the current DOMQuery. * * This will remove any attribute with the given name. It will do this on each * item currently wrapped by DOMQuery. * * As is the case in jQuery, this operation is not considered destructive. * * @param string $name * Name of the parameter to remove. * @return \QueryPath\DOMQuery * The DOMQuery object with the same elements. * @see attr() */ public function removeAttr($name) { foreach ($this->matches as $m) { //if ($m->hasAttribute($name)) $m->removeAttribute($name); } return $this; } /** * Reduce the matched set to just one. * * This will take a matched set and reduce it to just one item -- the item * at the index specified. This is a destructive operation, and can be undone * with {@link end()}. * * @param $index * The index of the element to keep. The rest will be * discarded. * @return \QueryPath\DOMQuery * @see get() * @see is() * @see end() */ public function eq($index) { return $this->inst($this->getNthMatch($index), NULL, $this->options); // XXX: Might there be a more efficient way of doing this? //$this->setMatches($this->getNthMatch($index)); //return $this; } /** * Given a selector, this checks to see if the current set has one or more matches. * * Unlike jQuery's version, this supports full selectors (not just simple ones). * * @param string $selector * The selector to search for. As of QueryPath 2.1.1, this also supports passing a * DOMNode object. * @return boolean * TRUE if one or more elements match. FALSE if no match is found. * @see get() * @see eq() */ public function is($selector) { if (is_object($selector)) { if ($selector instanceof \DOMNode) { return count($this->matches) == 1 && $selector->isSameNode($this->get(0)); } elseif ($selector instanceof \Traversable) { if (count($selector) != count($this->matches)) { return FALSE; } // Without $seen, there is an edge case here if $selector contains the same object // more than once, but the counts are equal. For example, [a, a, a, a] will // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. $seen = new \SplObjectStorage(); foreach ($selector as $item) { if (!$this->matches->contains($item) || $seen->contains($item)) { return FALSE; } $seen->attach($item); } return TRUE; } throw new \QueryPath\Exception('Cannot compare an object to a DOMQuery.'); return FALSE; } // Testing based on Issue #70. //fprintf(STDOUT, __FUNCTION__ .' found %d', $this->find($selector)->count()); return $this->branch($selector)->count() > 0; // Old version: //foreach ($this->matches as $m) { //$q = new \QueryPath\CSS\QueryPathEventHandler($m); //if ($q->find($selector)->getMatches()->count()) { //return TRUE; //} //} //return FALSE; } /** * Filter a list down to only elements that match the selector. * Use this, for example, to find all elements with a class, or with * certain children. * * @param string $selector * The selector to use as a filter. * @return \QueryPath\DOMQuery * The DOMQuery with non-matching items filtered out. * @see filterLambda() * @see filterCallback() * @see map() * @see find() * @see is() */ public function filter($selector) { $found = new \SplObjectStorage(); $tmp = new \SplObjectStorage(); foreach ($this->matches as $m) { $tmp->attach($m); // Seems like this should be right... but it fails unit // tests. Need to compare to jQuery. // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m); $query = new \QueryPath\CSS\DOMTraverser($tmp); $query->find($selector); if (count($query->matches())) { $found->attach($m); } $tmp->detach($m); } return $this->inst($found, NULL, $this->options); } /** * Sort the contents of the QueryPath object. * * By default, this does not change the order of the elements in the * DOM. Instead, it just sorts the internal list. However, if TRUE * is passed in as the second parameter then QueryPath will re-order * the DOM, too. * * @attention * DOM re-ordering is done by finding the location of the original first * item in the list, and then placing the sorted list at that location. * * The argument $compartor is a callback, such as a function name or a * closure. The callback receives two DOMNode objects, which you can use * as DOMNodes, or wrap in QueryPath objects. * * A simple callback: * @code * textContent == $b->textContent) { * return 0; * } * return $a->textContent > $b->textContent ? 1 : -1; * }; * $qp = QueryPath::with($xml, $selector)->sort($comp); * ?> * @endcode * * The above sorts the matches into lexical order using the text of each node. * If you would prefer to work with QueryPath objects instead of DOMNode * objects, you may prefer something like this: * * @code * text() == $qpb->text()) { * return 0; * } * return $qpa->text()> $qpb->text()? 1 : -1; * }; * * $qp = QueryPath::with($xml, $selector)->sort($comp); * ?> * @endcode * * @param callback $comparator * A callback. This will be called during sorting to compare two DOMNode * objects. * @param boolean $modifyDOM * If this is TRUE, the sorted results will be inserted back into * the DOM at the position of the original first element. * @return \QueryPath\DOMQuery * This object. */ public function sort($comparator, $modifyDOM = FALSE) { // Sort as an array. $list = iterator_to_array($this->matches); if (empty($list)) { return $this; } $oldFirst = $list[0]; usort($list, $comparator); // Copy back into SplObjectStorage. $found = new \SplObjectStorage(); foreach ($list as $node) { $found->attach($node); } //$this->setMatches($found); // Do DOM modifications only if necessary. if ($modifyDOM) { $placeholder = $oldFirst->ownerDocument->createElement('_PLACEHOLDER_'); $placeholder = $oldFirst->parentNode->insertBefore($placeholder, $oldFirst); $len = count($list); for ($i = 0; $i < $len; ++$i) { $node = $list[$i]; $node = $node->parentNode->removeChild($node); $placeholder->parentNode->insertBefore($node, $placeholder); } $placeholder->parentNode->removeChild($placeholder); } return $this->inst($found, NULL, $this->options); } /** * Filter based on a lambda function. * * The function string will be executed as if it were the body of a * function. It is passed two arguments: * - $index: The index of the item. * - $item: The current Element. * If the function returns boolean FALSE, the item will be removed from * the list of elements. Otherwise it will be kept. * * Example: * @code * qp('li')->filterLambda('qp($item)->attr("id") == "test"'); * @endcode * * The above would filter down the list to only an item whose ID is * 'text'. * * @param string $fn * Inline lambda function in a string. * @return \QueryPath\DOMQuery * @see filter() * @see map() * @see mapLambda() * @see filterCallback() */ public function filterLambda($fn) { $function = create_function('$index, $item', $fn); $found = new \SplObjectStorage(); $i = 0; foreach ($this->matches as $item) if ($function($i++, $item) !== FALSE) $found->attach($item); return $this->inst($found, NULL, $this->options); } /** * Use regular expressions to filter based on the text content of matched elements. * * Only items that match the given regular expression will be kept. All others will * be removed. * * The regular expression is run against the text content (the PCDATA) of the * elements. This is a way of filtering elements based on their content. * * Example: * @code * *
* @endcode
*
* @code
* filterPreg('/World/')->size();
* ?>
* @endcode
*
* The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
* @codeHello World@endcode.
*
* Compare this to the behavior of the :contains() CSS3 pseudo-class.
*
* @param string $regex
* A regular expression.
* @return \QueryPath\DOMQuery
* @see filter()
* @see filterCallback()
* @see preg_match()
*/
public function filterPreg($regex) {
$found = new \SplObjectStorage();
foreach ($this->matches as $item) {
if (preg_match($regex, $item->textContent) > 0) {
$found->attach($item);
}
}
return $this->inst($found, NULL, $this->options);
}
/**
* Filter based on a callback function.
*
* A callback may be any of the following:
* - a function: 'my_func'.
* - an object/method combo: $obj, 'myMethod'
* - a class/method combo: 'MyClass', 'myMethod'
* Note that classes are passed in strings. Objects are not.
*
* Each callback is passed to arguments:
* - $index: The index position of the object in the array.
* - $item: The item to be operated upon.
*
* If the callback function returns FALSE, the item will be removed from the
* set of matches. Otherwise the item will be considered a match and left alone.
*
* @param callback $callback.
* A callback either as a string (function) or an array (object, method OR
* classname, method).
* @return \QueryPath\DOMQuery
* Query path object augmented according to the function.
* @see filter()
* @see filterLambda()
* @see map()
* @see is()
* @see find()
*/
public function filterCallback($callback) {
$found = new \SplObjectStorage();
$i = 0;
if (is_callable($callback)) {
foreach($this->matches as $item)
if (call_user_func($callback, $i++, $item) !== FALSE) $found->attach($item);
}
else {
throw new \QueryPath\Exception('The specified callback is not callable.');
}
return $this->inst($found, NULL, $this->options);
}
/**
* Filter a list to contain only items that do NOT match.
*
* @param string $selector
* A selector to use as a negation filter. If the filter is matched, the
* element will be removed from the list.
* @return \QueryPath\DOMQuery
* The DOMQuery object with matching items filtered out.
* @see find()
*/
public function not($selector) {
$found = new \SplObjectStorage();
if ($selector instanceof \DOMElement) {
foreach ($this->matches as $m) if ($m !== $selector) $found->attach($m);
}
elseif (is_array($selector)) {
foreach ($this->matches as $m) {
if (!in_array($m, $selector, TRUE)) $found->attach($m);
}
}
elseif ($selector instanceof \SplObjectStorage) {
foreach ($this->matches as $m) if ($selector->contains($m)) $found->attach($m);
}
else {
foreach ($this->matches as $m) if (!QueryPath::with($m, NULL, $this->options)->is($selector)) $found->attach($m);
}
return $this->inst($found, NULL, $this->options);
}
/**
* Get an item's index.
*
* Given a DOMElement, get the index from the matches. This is the
* converse of {@link get()}.
*
* @param DOMElement $subject
* The item to match.
*
* @return mixed
* The index as an integer (if found), or boolean FALSE. Since 0 is a
* valid index, you should use strong equality (===) to test..
* @see get()
* @see is()
*/
public function index($subject) {
$i = 0;
foreach ($this->matches as $m) {
if ($m === $subject) {
return $i;
}
++$i;
}
return FALSE;
}
/**
* Run a function on each item in a set.
*
* The mapping callback can return anything. Whatever it returns will be
* stored as a match in the set, though. This means that afer a map call,
* there is no guarantee that the elements in the set will behave correctly
* with other DOMQuery functions.
*
* Callback rules:
* - If the callback returns NULL, the item will be removed from the array.
* - If the callback returns an array, the entire array will be stored in
* the results.
* - If the callback returns anything else, it will be appended to the array
* of matches.
*
* @param callback $callback
* The function or callback to use. The callback will be passed two params:
* - $index: The index position in the list of items wrapped by this object.
* - $item: The current item.
*
* @return \QueryPath\DOMQuery
* The DOMQuery object wrapping a list of whatever values were returned
* by each run of the callback.
*
* @see DOMQuery::get()
* @see filter()
* @see find()
*/
public function map($callback) {
$found = new \SplObjectStorage();
if (is_callable($callback)) {
$i = 0;
foreach ($this->matches as $item) {
$c = call_user_func($callback, $i, $item);
if (isset($c)) {
if (is_array($c) || $c instanceof \Iterable) {
foreach ($c as $retval) {
if (!is_object($retval)) {
$tmp = new \stdClass();
$tmp->textContent = $retval;
$retval = $tmp;
}
$found->attach($retval);
}
}
else {
if (!is_object($c)) {
$tmp = new \stdClass();
$tmp->textContent = $c;
$c = $tmp;
}
$found->attach($c);
}
}
++$i;
}
}
else {
throw new \QueryPath\Exception('Callback is not callable.');
}
return $this->inst($found, NULL, $this->options);
}
/**
* Narrow the items in this object down to only a slice of the starting items.
*
* @param integer $start
* Where in the list of matches to begin the slice.
* @param integer $length
* The number of items to include in the slice. If nothing is specified, the
* all remaining matches (from $start onward) will be included in the sliced
* list.
* @return \QueryPath\DOMQuery
* @see array_slice()
*/
public function slice($start, $length = 0) {
$end = $length;
$found = new \SplObjectStorage();
if ($start >= $this->size()) {
return $this->inst($found, NULL, $this->options);
}
$i = $j = 0;
foreach ($this->matches as $m) {
if ($i >= $start) {
if ($end > 0 && $j >= $end) {
break;
}
$found->attach($m);
++$j;
}
++$i;
}
return $this->inst($found, NULL, $this->options);
}
/**
* Run a callback on each item in the list of items.
*
* Rules of the callback:
* - A callback is passed two variables: $index and $item. (There is no
* special treatment of $this, as there is in jQuery.)
* - You will want to pass $item by reference if it is not an
* object (DOMNodes are all objects).
* - A callback that returns FALSE will stop execution of the each() loop. This
* works like break in a standard loop.
* - A TRUE return value from the callback is analogous to a continue statement.
* - All other return values are ignored.
*
* @param callback $callback
* The callback to run.
* @return \QueryPath\DOMQuery
* The DOMQuery.
* @see eachLambda()
* @see filter()
* @see map()
*/
public function each($callback) {
if (is_callable($callback)) {
$i = 0;
foreach ($this->matches as $item) {
if (call_user_func($callback, $i, $item) === FALSE) return $this;
++$i;
}
}
else {
throw new \QueryPath\Exception('Callback is not callable.');
}
return $this;
}
/**
* An each() iterator that takes a lambda function.
*
* @deprecated
* Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
* method is not necessary and should be avoided.
* @param string $lambda
* The lambda function. This will be passed ($index, &$item).
* @return \QueryPath\DOMQuery
* The DOMQuery object.
* @see each()
* @see filterLambda()
* @see filterCallback()
* @see map()
*/
public function eachLambda($lambda) {
$index = 0;
foreach ($this->matches as $item) {
$fn = create_function('$index, &$item', $lambda);
if ($fn($index, $item) === FALSE) return $this;
++$index;
}
return $this;
}
/**
* Insert the given markup as the last child.
*
* The markup will be inserted into each match in the set.
*
* The same element cannot be inserted multiple times into a document. DOM
* documents do not allow a single object to be inserted multiple times
* into the DOM. To insert the same XML repeatedly, we must first clone
* the object. This has one practical implication: Once you have inserted
* an element into the object, you cannot further manipulate the original
* element and expect the changes to be replciated in the appended object.
* (They are not the same -- there is no shared reference.) Instead, you
* will need to retrieve the appended object and operate on that.
*
* @param mixed $data
* This can be either a string (the usual case), or a DOM Element.
* @return \QueryPath\DOMQuery
* The DOMQuery object.
* @see appendTo()
* @see prepend()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function append($data) {
$data = $this->prepareInsert($data);
if (isset($data)) {
if (empty($this->document->documentElement) && $this->matches->count() == 0) {
// Then we assume we are writing to the doc root
$this->document->appendChild($data);
$found = new \SplObjectStorage();
$found->attach($this->document->documentElement);
$this->setMatches($found);
}
else {
// You can only append in item once. So in cases where we
// need to append multiple times, we have to clone the node.
foreach ($this->matches as $m) {
// DOMDocumentFragments are even more troublesome, as they don't
// always clone correctly. So we have to clone their children.
if ($data instanceof \DOMDocumentFragment) {
foreach ($data->childNodes as $n)
$m->appendChild($n->cloneNode(TRUE));
}
else {
// Otherwise a standard clone will do.
$m->appendChild($data->cloneNode(TRUE));
}
}
}
}
return $this;
}
/**
* Append the current elements to the destination passed into the function.
*
* This cycles through all of the current matches and appends them to
* the context given in $destination. If a selector is provided then the
* $destination is queried (using that selector) prior to the data being
* appended. The data is then appended to the found items.
*
* @param DOMQuery $dest
* A DOMQuery object that will be appended to.
* @return \QueryPath\DOMQuery
* The original DOMQuery, unaltered. Only the destination DOMQuery will
* be modified.
* @see append()
* @see prependTo()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function appendTo(DOMQuery $dest) {
foreach ($this->matches as $m) $dest->append($m);
return $this;
}
/**
* Insert the given markup as the first child.
*
* The markup will be inserted into each match in the set.
*
* @param mixed $data
* This can be either a string (the usual case), or a DOM Element.
* @return \QueryPath\DOMQuery
* @see append()
* @see before()
* @see after()
* @see prependTo()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function prepend($data) {
$data = $this->prepareInsert($data);
if (isset($data)) {
foreach ($this->matches as $m) {
$ins = $data->cloneNode(TRUE);
if ($m->hasChildNodes())
$m->insertBefore($ins, $m->childNodes->item(0));
else
$m->appendChild($ins);
}
}
return $this;
}
/**
* Take all nodes in the current object and prepend them to the children nodes of
* each matched node in the passed-in DOMQuery object.
*
* This will iterate through each item in the current DOMQuery object and
* add each item to the beginning of the children of each element in the
* passed-in DOMQuery object.
*
* @see insertBefore()
* @see insertAfter()
* @see prepend()
* @see appendTo()
* @param DOMQuery $dest
* The destination DOMQuery object.
* @return \QueryPath\DOMQuery
* The original DOMQuery, unmodified. NOT the destination DOMQuery.
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function prependTo(DOMQuery $dest) {
foreach ($this->matches as $m) $dest->prepend($m);
return $this;
}
/**
* Insert the given data before each element in the current set of matches.
*
* This will take the give data (XML or HTML) and put it before each of the items that
* the DOMQuery object currently contains. Contrast this with after().
*
* @param mixed $data
* The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
* or the other usual suspects. (See {@link qp()}).
* @return \QueryPath\DOMQuery
* Returns the DOMQuery with the new modifications. The list of elements currently
* selected will remain the same.
* @see insertBefore()
* @see after()
* @see append()
* @see prepend()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function before($data) {
$data = $this->prepareInsert($data);
foreach ($this->matches as $m) {
$ins = $data->cloneNode(TRUE);
$m->parentNode->insertBefore($ins, $m);
}
return $this;
}
/**
* Insert the current elements into the destination document.
* The items are inserted before each element in the given DOMQuery document.
* That is, they will be siblings with the current elements.
*
* @param DOMQuery $dest
* Destination DOMQuery document.
* @return \QueryPath\DOMQuery
* The current DOMQuery object, unaltered. Only the destination DOMQuery
* object is altered.
* @see before()
* @see insertAfter()
* @see appendTo()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function insertBefore(DOMQuery $dest) {
foreach ($this->matches as $m) $dest->before($m);
return $this;
}
/**
* Insert the contents of the current DOMQuery after the nodes in the
* destination DOMQuery object.
*
* @param DOMQuery $dest
* Destination object where the current elements will be deposited.
* @return \QueryPath\DOMQuery
* The present DOMQuery, unaltered. Only the destination object is altered.
* @see after()
* @see insertBefore()
* @see append()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function insertAfter(DOMQuery $dest) {
foreach ($this->matches as $m) $dest->after($m);
return $this;
}
/**
* Insert the given data after each element in the current DOMQuery object.
*
* This inserts the element as a peer to the currently matched elements.
* Contrast this with {@link append()}, which inserts the data as children
* of matched elements.
*
* @param mixed $data
* The data to be appended.
* @return \QueryPath\DOMQuery
* The DOMQuery object (with the items inserted).
* @see before()
* @see append()
* @throws QueryPath::Exception
* Thrown if $data is an unsupported object type.
*/
public function after($data) {
if (empty($data)) {
return $this;
}
$data = $this->prepareInsert($data);
foreach ($this->matches as $m) {
$ins = $data->cloneNode(TRUE);
if (isset($m->nextSibling))
$m->parentNode->insertBefore($ins, $m->nextSibling);
else
$m->parentNode->appendChild($ins);
}
return $this;
}
/**
* Replace the existing element(s) in the list with a new one.
*
* @param mixed $new
* A DOMElement or XML in a string. This will replace all elements
* currently wrapped in the DOMQuery object.
* @return \QueryPath\DOMQuery
* The DOMQuery object wrapping the items that were removed.
* This remains consistent with the jQuery API.
* @see append()
* @see prepend()
* @see before()
* @see after()
* @see remove()
* @see replaceAll()
*/
public function replaceWith($new) {
$data = $this->prepareInsert($new);
$found = new \SplObjectStorage();
foreach ($this->matches as $m) {
$parent = $m->parentNode;
$parent->insertBefore($data->cloneNode(TRUE), $m);
$found->attach($parent->removeChild($m));
}
return $this->inst($found, NULL, $this->options);
}
/**
* Remove the parent element from the selected node or nodes.
*
* This takes the given list of nodes and "unwraps" them, moving them out of their parent
* node, and then deleting the parent node.
*
* For example, consider this:
*
* @code
*
foo
test ** @endcode * * We can retrieve just the contents of this code by doing something like * this: * @code * qp($xml, 'div')->innerHTML(); * @endcode * * This would return the following: * @codetest
foo
test@endcode
*
* @return string
* Returns a string representation of the child nodes of the first
* matched element.
* @see html()
* @see innerXML()
* @see innerXHTML()
* @since 2.0
*/
public function innerHTML() {
return $this->innerXML();
}
/**
* Fetch child (inner) nodes of the first match.
*
* This will return the children of the present match. For an example,
* see {@link innerHTML()}.
*
* @see innerHTML()
* @see innerXML()
* @return string
* Returns a string of XHTML that represents the children of the present
* node.
* @since 2.0
*/
public function innerXHTML() {
$length = $this->size();
if ($length == 0) {
return NULL;
}
// Only return the first item -- that's what JQ does.
$first = $this->getFirstMatch();
// Catch cases where first item is not a legit DOM object.
if (!($first instanceof \DOMNode)) {
return NULL;
}
elseif (!$first->hasChildNodes()) {
return '';
}
$buffer = '';
foreach ($first->childNodes as $child) {
$buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
}
return $buffer;
}
/**
* Fetch child (inner) nodes of the first match.
*
* This will return the children of the present match. For an example,
* see {@link innerHTML()}.
*
* @see innerHTML()
* @see innerXHTML()
* @return string
* Returns a string of XHTML that represents the children of the present
* node.
* @since 2.0
*/
public function innerXML() {
$length = $this->size();
if ($length == 0) {
return NULL;
}
// Only return the first item -- that's what JQ does.
$first = $this->getFirstMatch();
// Catch cases where first item is not a legit DOM object.
if (!($first instanceof \DOMNode)) {
return NULL;
}
elseif (!$first->hasChildNodes()) {
return '';
}
$buffer = '';
foreach ($first->childNodes as $child) {
$buffer .= $this->document->saveXML($child);
}
return $buffer;
}
/**
* Get child elements as an HTML5 string.
*
* TODO: This is a very simple alteration of innerXML. Do we need better
* support?
*/
public function innerHTML5() {
$length = $this->size();
if ($length == 0) {
return NULL;
}
// Only return the first item -- that's what JQ does.
$first = $this->getFirstMatch();
// Catch cases where first item is not a legit DOM object.
if (!($first instanceof \DOMNode)) {
return NULL;
}
elseif (!$first->hasChildNodes()) {
return '';
}
$html5 = new HTML5($this->options);
$buffer = '';
foreach ($first->childNodes as $child) {
$buffer .= $html5->saveHTML($child);
}
return $buffer;
}
/**
* Retrieve the text of each match and concatenate them with the given separator.
*
* This has the effect of looping through all children, retrieving their text
* content, and then concatenating the text with a separator.
*
* @param string $sep
* The string used to separate text items. The default is a comma followed by a
* space.
* @param boolean $filterEmpties
* If this is true, empty items will be ignored.
* @return string
* The text contents, concatenated together with the given separator between
* every pair of items.
* @see implode()
* @see text()
* @since 2.0
*/
public function textImplode($sep = ', ', $filterEmpties = TRUE) {
$tmp = array();
foreach ($this->matches as $m) {
$txt = $m->textContent;
$trimmed = trim($txt);
// If filter empties out, then we only add items that have content.
if ($filterEmpties) {
if (strlen($trimmed) > 0) $tmp[] = $txt;
}
// Else add all content, even if it's empty.
else {
$tmp[] = $txt;
}
}
return implode($sep, $tmp);
}
/**
* Get the text contents from just child elements.
*
* This is a specialized variant of textImplode() that implodes text for just the
* child elements of the current element.
*
* @param string $separator
* The separator that will be inserted between found text content.
* @return string
* The concatenated values of all children.
*/
function childrenText($separator = ' ') {
// Branch makes it non-destructive.
return $this->branch()->xpath('descendant::text()')->textImplode($separator);
}
/**
* Get or set the text contents of a node.
* @param string $text
* If this is not NULL, this value will be set as the text of the node. It
* will replace any existing content.
* @return mixed
* A DOMQuery if $text is set, or the text content if no text
* is passed in as a pram.
* @see html()
* @see xml()
* @see contents()
*/
public function text($text = NULL) {
if (isset($text)) {
$this->removeChildren();
foreach ($this->matches as $m) $m->appendChild($this->document->createTextNode($text));
return $this;
}
// Returns all text as one string:
$buf = '';
foreach ($this->matches as $m) $buf .= $m->textContent;
return $buf;
}
/**
* Get or set the text before each selected item.
*
* If $text is passed in, the text is inserted before each currently selected item.
*
* If no text is given, this will return the concatenated text after each selected element.
*
* @code
*
* *
* *