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|null $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 DOMQuery * The DOMQuery object, wrapping the root element (document element) * for the current document. * @throws CSS\ParseException */ public function top($selector = null): Query { return $this->inst($this->document->documentElement, $selector); } /** * Given a CSS Selector, find matching items. * * @param string $selector * CSS 3 Selector * * @return DOMQuery * @throws CSS\ParseException * @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. * @see filter() */ public function find($selector): Query { $query = new DOMTraverser($this->matches); $query->find($selector); return $this->inst($query->matches(), null); } /** * @param $selector * * @return DOMQuery * @throws CSS\ParseException */ public function findInPlace($selector) { $query = new 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 DOMQuery * A DOMQuery object wrapping the results of the query. * @throws CSS\ParseException * @author M Butcher * @author Xavier Prud'homme * @see find() */ public function xpath($query, $options = []) { $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); } /** * 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(): int { 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|null $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 array|SplObjectStorage|null * 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 ($index !== null) { return ($this->count() > $index) ? $this->getNthMatch($index) : null; } // Retain support for legacy. if (! $asObject) { $matches = []; 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. * * @return string */ 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 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(); } /** * 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 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 = []; preg_match($regex, $data, $matches); if (! empty($matches)) { $result = [ 'mime' => $matches[1] . '/' . $matches[2], 'data' => base64_decode($matches[3]), ]; return $result; } } else { $attVal = QueryPath::encodeDataURL($data, $mime, $context); return $this->attr($attr, $attVal); } } /** * 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 DOMQuery * This object. * @throws CSS\ParseException */ public function sort($comparator, $modifyDOM = false): Query { // 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); } /** * 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 int|false * * 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; } /** * The tag name of the first element in the list. * * This returns the tag name of the first element in the list of matches. If * the list is empty, an empty string will be used. * * @return string * The tag name of the first element in the list. * @see replaceWith() * @see replaceAll() */ public function tag() { return ($this->matches->count() > 0) ? $this->getFirstMatch()->tagName : ''; } /** * Revert to the previous set of matches. * * DEPRECATED Do not use. * * This will revert back to the last set of matches (before the last * "destructive" set of operations). This undoes any change made to the set of * matched objects. Functions like find() and filter() change the * list of matched objects. The end() function will revert back to the last set of * matched items. * * Note that functions that modify the document, but do not change the list of * matched objects, are not "destructive". Thus, calling append('something')->end() * will not undo the append() call. * * Only one level of changes is stored. Reverting beyond that will result in * an empty set of matches. Example: * * @code * // The line below returns the same thing as qp(document, 'p'); * qp(document, 'p')->find('div')->end(); * // This returns an empty array: * qp(document, 'p')->end(); * // This returns an empty array: * qp(document, 'p')->find('div')->find('span')->end()->end(); * @endcode * * The last one returns an empty array because only one level of changes is stored. * * @return DOMQuery * A DOMNode object reflecting the list of matches prior to the last destructive * operation. * @see andSelf() * @see add() * @deprecated This function will be removed. */ public function end() { // Note that this does not use setMatches because it must set the previous // set of matches to empty array. $this->matches = $this->last; $this->last = new SplObjectStorage(); return $this; } /** * Combine the current and previous set of matched objects. * * Example: * * @code * qp(document, 'p')->find('div')->andSelf(); * @endcode * * The code above will contain a list of all p elements and all div elements that * are beneath p elements. * * @return DOMQuery * A DOMNode object with the results of the last two "destructive" operations. * @see end(); * @see add() * @see end() */ public function andSelf() { // This is destructive, so we need to set $last: $last = $this->matches; foreach ($this->last as $item) { $this->matches->attach($item); } $this->last = $last; return $this; } /** * Set or get the markup for an element. * * If $markup is set, then the giving markup will be injected into each * item in the set. All other children of that node will be deleted, and this * new code will be the only child or children. The markup MUST BE WELL FORMED. * * If no markup is given, this will return a string representing the child * markup of the first node. * * Important: This differs from jQuery's html() function. This function * returns the current node and all of its children. jQuery returns only * the children. This means you do not need to do things like this: * @code$qp->parent()->html()@endcode. * * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML. * * @param null|string $markup * The text to insert. * * @return DOMQuery|string|null * * A string if no markup was passed, or a DOMQuery if markup was passed. * @throws Exception * @throws QueryPath * @see xml() * @see text() * @see contents() */ public function html($markup = null) { if (isset($markup)) { if ($this->options['replace_entities']) { $markup = Entities::replaceAllEntities($markup); } // Parse the HTML and insert it into the DOM //$doc = DOMDocument::loadHTML($markup); $doc = $this->document->createDocumentFragment(); $doc->appendXML($markup); $this->removeChildren(); $this->append($doc); return $this; } $length = $this->matches->count(); 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; } // Added by eabrand. if (! $first->ownerDocument->documentElement) { return null; } if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) { return $this->document->saveHTML(); } // saveHTML cannot take a node and serialize it. return $this->document->saveXML($first); } /** * Write the QueryPath document to HTML5. * * See html() * * @param null|string $markup * * @return null|DOMQuery|string * @throws QueryPath * @throws \QueryPath\Exception */ public function html5($markup = null) { $html5 = new HTML5($this->options); // append HTML to existing if ($markup === null) { // Parse the HTML and insert it into the DOM $doc = $html5->loadHTMLFragment($markup); $this->removeChildren(); $this->append($doc); return $this; } $length = $this->count(); 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; } // Added by eabrand. if (! $first->ownerDocument->documentElement) { return null; } if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) { return $html5->saveHTML($this->document); //$this->document->saveHTML(); } return $html5->saveHTML($first); } /** * Fetch the HTML contents INSIDE of the first DOMQuery item. * * This behaves the way jQuery's @codehtml()@endcode function behaves. * * This gets all children of the first match in DOMQuery. * * Consider this fragment: * * @code *
* test

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|null * 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()}. * * @return string|null * Returns a string of XHTML that represents the children of the present * node. * @see innerXML() * @see innerHTML() * @since 2.0 */ public function innerXHTML() { $length = $this->matches->count(); 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; } if (! $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()}. * * @return string|null * Returns a string of XHTML that represents the children of the present * node. * @see innerXHTML() * @see innerHTML() * @since 2.0 */ public function innerXML() { $length = $this->matches->count(); 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; } if (! $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? * * @return string|null */ public function innerHTML5() { $length = $this->matches->count(); 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; } if (! $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): string { $tmp = []; 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. * @throws CSS\ParseException */ public function childrenText($separator = ' '): string { // Branch makes it non-destructive. return $this->branch()->xpath('descendant::text()')->textImplode($separator); } /** * Get or set the text contents of a node. * * @param string|null $text * If this is not NULL, this value will be set as the text of the node. It * will replace any existing content. * * @return DOMQuery|string * 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 before each selected element. * * @code * FooBar'; * * // This will return 'Foo' * qp($xml, 'a')->textBefore(); * * // This will insert 'Baz' right before . * qp($xml, 'b')->textBefore('Baz'); * ?> * @endcode * * @param string|null $text * If this is set, it will be inserted before each node in the current set of * selected items. * * @return DOMQuery|string * Returns the DOMQuery object if $text was set, and returns a string (possibly empty) * if no param is passed. * @throws Exception * @throws QueryPath */ public function textBefore($text = null) { if (isset($text)) { $textNode = $this->document->createTextNode($text); return $this->before($textNode); } $buffer = ''; foreach ($this->matches as $m) { $p = $m; while (isset($p->previousSibling) && $p->previousSibling->nodeType === XML_TEXT_NODE) { $p = $p->previousSibling; $buffer .= $p->textContent; } } return $buffer; } /** * Get or set the text after each selected item. * * If $text is passed in, the text is inserted after each currently selected item. * * If no text is given, this will return the concatenated text after each selected element. * * @code * FooBarBaz'; * * // This will return 'Baz' * qp($xml, 'a')->textAfter(); * * // This will insert 'Bin' right after . * qp($xml, 'c')->textAfter('Bin'); * ?> * @endcode * * @param string|null $text * If this is set, it will be inserted after each node in the current set of * selected items. * * @return DOMQuery|string * Returns the DOMQuery object if $text was set, and returns a string (possibly empty) * if no param is passed. * @throws Exception * @throws QueryPath */ public function textAfter($text = null) { if (isset($text)) { $textNode = $this->document->createTextNode($text); return $this->after($textNode); } $buffer = ''; foreach ($this->matches as $m) { $n = $m; while (isset($n->nextSibling) && $n->nextSibling->nodeType === XML_TEXT_NODE) { $n = $n->nextSibling; $buffer .= $n->textContent; } } return $buffer; } /** * Set or get the value of an element's 'value' attribute. * * The 'value' attribute is common in HTML form elements. This is a * convenience function for accessing the values. Since this is not common * task on the server side, this method may be removed in future releases. (It * is currently provided for jQuery compatibility.) * * If a value is provided in the params, then the value will be set for all * matches. If no params are given, then the value of the first matched element * will be returned. This may be NULL. * * @deprecated Just use attr(). There's no reason to use this on the server. * @see attr() * * @param string|null $value * * @return DOMQuery|array|string|null * Returns a DOMQuery if a string was passed in, and a string if no string * was passed in. In the later case, an error will produce NULL. */ public function val($value = null) { if (isset($value)) { $this->attr('value', $value); return $this; } return $this->attr('value'); } /** * Set or get XHTML markup for an element or elements. * * This differs from {@link html()} in that it processes (and produces) * strictly XML 1.0 compliant markup. * * Like {@link xml()} and {@link html()}, this functions as both a * setter and a getter. * * This is a convenience function for fetching HTML in XML format. * It does no processing of the markup (such as schema validation). * * @param string|null $markup * A string containing XML data. * * @return DOMQuery|string|null * If markup is passed in, a DOMQuery is returned. If no markup is passed * in, XML representing the first matched element is returned. * @throws Exception * @see html() * @see innerXHTML() */ public function xhtml($markup = null) { // XXX: This is a minor reworking of the original xml() method. // This should be refactored, probably. // See http://github.com/technosophos/querypath/issues#issue/10 $omit_xml_decl = $this->options['omit_xml_declaration']; if ($markup === true) { // Basically, we handle the special case where we don't // want the XML declaration to be displayed. $omit_xml_decl = true; } elseif (isset($markup)) { return $this->xml($markup); } $length = $this->matches->count(); 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; } if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) { // Has the unfortunate side-effect of stripping doctype. //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG)); $text = $this->document->saveXML(null, LIBXML_NOEMPTYTAG); } else { $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG); } // Issue #47: Using the old trick for removing the XML tag also removed the // doctype. So we remove it with a regex: if ($omit_xml_decl) { $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text); } // This is slightly lenient: It allows for cases where code incorrectly places content // inside of these supposedly unary elements. $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i'; $text = preg_replace($unary, '<\\1\\2 />', $text); // Experimental: Support for enclosing CDATA sections with comments to be both XML compat // and HTML 4/5 compat $cdata = '/()/i'; $replace = $this->options['escape_xhtml_js_css_sections']; $text = preg_replace($cdata, $replace, $text); return $text; } /** * Set or get the XML markup for an element or elements. * * Like {@link html()}, this functions in both a setter and a getter mode. * * In setter mode, the string passed in will be parsed and then appended to the * elements wrapped by this DOMNode object.When in setter mode, this parses * the XML using the DOMFragment parser. For that reason, an XML declaration * is not necessary. * * In getter mode, the first element wrapped by this DOMNode object will be * converted to an XML string and returned. * * @param string|null $markup * A string containing XML data. * * @return DOMQuery|string|null * If markup is passed in, a DOMQuery is returned. If no markup is passed * in, XML representing the first matched element is returned. * @throws Exception * @see xhtml() * @see html() * @see text() * @see content() * @see innerXML() */ public function xml($markup = null) { $omit_xml_decl = $this->options['omit_xml_declaration']; if ($markup === true) { // Basically, we handle the special case where we don't // want the XML declaration to be displayed. $omit_xml_decl = true; } elseif (isset($markup)) { if ($this->options['replace_entities']) { $markup = Entities::replaceAllEntities($markup); } $doc = $this->document->createDocumentFragment(); $doc->appendXML($markup); $this->removeChildren(); $this->append($doc); return $this; } $length = $this->matches->count(); 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; } if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) { return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML()); } return $this->document->saveXML($first); } /** * Send the XML document to the client. * * Write the document to a file path, if given, or * to stdout (usually the client). * * This prints the entire document. * * @param string|null $path * The path to the file into which the XML should be written. if * this is NULL, data will be written to STDOUT, which is usually * sent to the remote browser. * @param int $options * (As of QueryPath 2.1) Pass libxml options to the saving mechanism. * * @return DOMQuery * The DOMQuery object, unmodified. * @throws Exception * In the event that a file cannot be written, an Exception will be thrown. * @see innerXML() * @see writeXHTML() * @see xml() */ public function writeXML($path = null, $options = 0) { // Backwards compatibility fix for PHP8+ if (is_null($options)) { $options = 0; } if ($path === null) { print $this->document->saveXML(null, $options); } else { try { set_error_handler([IOException::class, 'initializeFromError']); $this->document->save($path, $options); } catch (Exception $e) { restore_error_handler(); throw $e; } restore_error_handler(); } return $this; } /** * Writes HTML to output. * * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}. * * Write the document to stdout (usually the client) or to a file. * * @param string|null $path * The path to the file into which the XML should be written. if * this is NULL, data will be written to STDOUT, which is usually * sent to the remote browser. * * @return DOMQuery * The DOMQuery object, unmodified. * @throws Exception * In the event that a file cannot be written, an Exception will be thrown. * @see innerHTML() * @see html() */ public function writeHTML($path = null) { if ($path === null) { print $this->document->saveHTML(); } else { try { set_error_handler(['\QueryPath\ParseException', 'initializeFromError']); $this->document->saveHTMLFile($path); } catch (Exception $e) { restore_error_handler(); throw $e; } restore_error_handler(); } return $this; } /** * Write the document to HTML5. * * This works the same as the other write* functions, but it encodes the output * as HTML5 with UTF-8. * * @throws Exception * In the event that a file cannot be written, an Exception will be thrown. * @see innerHTML5() * @see html5() */ public function writeHTML5($path = null) { $html5 = new HTML5(); if ($path === null) { // Print the document to stdout. print $html5->saveHTML($this->document); return; } $html5->save($this->document, $path); } /** * Write an XHTML file to output. * * Typically, you should use this instead of {@link writeHTML()}. * * Currently, this functions identically to {@link toXML()} except that * it always uses closing tags (e.g. always @code@endcode, * never @code