* @copyright 2010 Niels A.D., 2014 Todd Burry * @license http://opensource.org/licenses/LGPL-2.1 LGPL-2.1 * @package pQuery */ namespace MailPoetVendor\pQuery; if (!defined('ABSPATH')) exit; /** * Holds (x)html/xml tag information like tag name, attributes, * parent, children, self close, etc. * */ class DomNode implements IQuery { /** * Element Node, used for regular elements */ const NODE_ELEMENT = 0; /** * Text Node */ const NODE_TEXT = 1; /** * Comment Node */ const NODE_COMMENT = 2; /** * Conditional Node ( */ const NODE_CDATA = 4; /** * Doctype Node */ const NODE_DOCTYPE = 5; /** * XML Node, used for tags that start with ?, like 'value') * @internal Public for faster access! * @see getAttribute() * @see setAttribute() * @access private */ var $attributes = array(); /** * Namespace info for attributes * @var array * @internal array('tag' => array(array('ns', 'tag', 'ns:tag', index))) * @internal Public for easy outside modifications! * @see findAttribute() * @access private */ var $attributes_ns = null; /** * Array of child nodes * @var array * @internal Public for faster access! * @see childCount() * @see getChild() * @see addChild() * @see deleteChild() * @access private */ var $children = array(); /** * Full tag name (including namespace) * @var string * @see getTagName() * @see getNamespace() */ var $tag = ''; /** * Namespace info for tag * @var array * @internal array('namespace', 'tag') * @internal Public for easy outside modifications! * @access private */ var $tag_ns = null; /** * Is node a self closing node? No closing tag if true. * @var bool */ var $self_close = false; /** * If self close, then this will be used to close the tag * @var string * @see $self_close */ var $self_close_str = ' /'; /** * Use short tags for attributes? If true, then attributes * with values equal to the attribute name will not output * the value, e.g. selected="selected" will be selected. * @var bool */ var $attribute_shorttag = true; /** * Function map used for the selector filter * @var array * @internal array('root' => 'filter_root') will cause the * selector to call $this->filter_root at :root * @access private */ var $filter_map = array( 'root' => 'filter_root', 'nth-child' => 'filter_nchild', 'eq' => 'filter_nchild', //jquery (naming) compatibility 'gt' => 'filter_gt', 'lt' => 'filter_lt', 'nth-last-child' => 'filter_nlastchild', 'nth-of-type' => 'filter_ntype', 'nth-last-of-type' => 'filter_nlastype', 'odd' => 'filter_odd', 'even' => 'filter_even', 'every' => 'filter_every', 'first-child' => 'filter_first', 'last-child' => 'filter_last', 'first-of-type' => 'filter_firsttype', 'last-of-type' => 'filter_lasttype', 'only-child' => 'filter_onlychild', 'only-of-type' => 'filter_onlytype', 'empty' => 'filter_empty', 'not-empty' => 'filter_notempty', 'has-text' => 'filter_hastext', 'no-text' => 'filter_notext', 'lang' => 'filter_lang', 'contains' => 'filter_contains', 'has' => 'filter_has', 'not' => 'filter_not', 'element' => 'filter_element', 'text' => 'filter_text', 'comment' => 'filter_comment', 'checked' => 'filter_checked', 'selected' => 'filter_selected', ); /** * Class constructor * @param string|array $tag Name of the tag, or array with taginfo (array( * 'tag_name' => 'tag', * 'self_close' => false, * 'attributes' => array('attribute' => 'value'))) * @param DomNode $parent Parent of node, null if none */ function __construct($tag, $parent) { $this->parent = $parent; if (is_string($tag)) { $this->tag = $tag; } else { $this->tag = $tag['tag_name']; $this->self_close = $tag['self_close']; $this->attributes = $tag['attributes']; } } #php4 PHP4 class constructor compatibility #function DomNode($tag, $parent) {return $this->__construct($tag, $parent);} #php4e /** * Class destructor * @access private */ function __destruct() { $this->delete(); } /** * Class toString, outputs {@link $tag} * @return string * @access private */ function __toString() { return (($this->tag === '~root~') ? $this->toString(true, true, 1) : $this->tag); } /** * Class magic get method, outputs {@link getAttribute()} * @return string * @access private */ function __get($attribute) { return $this->getAttribute($attribute); } /** * Class magic set method, performs {@link setAttribute()} * @access private */ function __set($attribute, $value) { $this->setAttribute($attribute, $value); } /** * Class magic isset method, returns {@link hasAttribute()} * @return bool * @access private */ function __isset($attribute) { return $this->hasAttribute($attribute); } /** * Class magic unset method, performs {@link deleteAttribute()} * @access private */ function __unset($attribute) { return $this->deleteAttribute($attribute); } /** * Class magic invoke method, performs {@link query()}. * @param string $query The css query to run on the nodes. * @return pQuery */ function __invoke($query = '*') { return $this->query($query); } /** * Returns place in document * @return string */ function dumpLocation() { return (($this->parent) ? (($p = $this->parent->dumpLocation()) ? $p.' > ' : '').$this->tag.'('.$this->typeIndex().')' : ''); } /** * Returns all the attributes and their values * @return string * @access private */ protected function toString_attributes() { $s = ''; foreach($this->attributes as $a => $v) { $s .= ' '.$a; if ((!$this->attribute_shorttag) || ($v !== $a)) { $quote = (strpos($v, '"') === false) ? '"' : "'"; $s .= '='.$quote.$v.$quote; } } return $s; } /** * Returns the content of the node (child tags and text) * @param bool $attributes Print attributes of child tags * @param bool|int $recursive How many sublevels of childtags to print. True for all. * @param bool $content_only Only print text, false will print tags too. * @return string * @access private */ protected function toString_content($attributes = true, $recursive = true, $content_only = false) { $s = ''; foreach($this->children as $c) { $s .= $c->toString($attributes, $recursive, $content_only); } return $s; } /** * Returns the node as string * @param bool $attributes Print attributes (of child tags) * @param bool|int $recursive How many sub-levels of child tags to print. True for all. * @param bool|int $content_only Only print text, false will print tags too. * @return string */ function toString($attributes = true, $recursive = true, $content_only = false) { if ($content_only) { if (is_int($content_only)) { --$content_only; } return $this->toString_content($attributes, $recursive, $content_only); } $s = '<'.$this->tag; if ($attributes) { $s .= $this->toString_attributes(); } if ($this->self_close) { $s .= $this->self_close_str.'>'; } else { $s .= '>'; if($recursive) { $s .= $this->toString_content($attributes); } $s .= 'tag.'>'; } return $s; } /** * Similar to JavaScript outerText, will return full (html formatted) node * @return string */ function getOuterText() { return html_entity_decode($this->toString(), ENT_QUOTES); } /** * Similar to JavaScript outerText, will replace node (and child nodes) with new text * @param string $text * @param HtmlParserBase $parser Null to auto create instance * @return bool|array True on succeed, array with errors on failure */ function setOuterText($text, $parser = null) { if (trim($text)) { $index = $this->index(); if ($parser === null) { $parser = new $this->parserClass(); } $parser->setDoc($text); $parser->parse_all(); $parser->root->moveChildren($this->parent, $index); } $this->delete(); return (($parser && $parser->errors) ? $parser->errors : true); } /** * Return html code of node * @internal jquery (naming) compatibility * @param string|null $value The value to set or null to get the value. * @see toString() * @return string */ function html($value = null) { if ($value !== null) { $this->setInnerText($value); } return $this->getInnerText(); } /** * Similar to JavaScript innerText, will return (html formatted) content * @return string */ function getInnerText() { return html_entity_decode($this->toString(true, true, 1), ENT_QUOTES); } /** * Similar to JavaScript innerText, will replace child nodes with new text * @param string $text * @param HtmlParserBase $parser Null to auto create instance * @return bool|array True on succeed, array with errors on failure */ function setInnerText($text, $parser = null) { $this->clear(); if (trim($text)) { if ($parser === null) { $parser = new $this->parserClass(); } $parser->root =& $this; $parser->setDoc($text); $parser->parse_all(); } return (($parser && $parser->errors) ? $parser->errors : true); } /** * Similar to JavaScript plainText, will return text in node (and subnodes) * @return string */ function getPlainText() { return preg_replace('`\s+`', ' ', html_entity_decode($this->toString(true, true, true), ENT_QUOTES)); } /** * Return plaintext taking document encoding into account * @return string */ function getPlainTextUTF8() { $txt = $this->toString(true, true, true); $enc = $this->getEncoding(); if ($enc !== false) { $txt = mb_convert_encoding($txt, 'UTF-8', $enc); } return preg_replace('`\s+`', ' ', html_entity_decode($txt, ENT_QUOTES, 'UTF-8')); } /** * Similar to JavaScript plainText, will replace child nodes with new text (literal) * @param string $text */ function setPlainText($text) { $this->clear(); if (trim($text)) { $this->addText(htmlentities($text, ENT_QUOTES)); } } /** * Delete node from parent and clear node */ function delete() { if (($p = $this->parent) !== null) { $this->parent = null; $p->deleteChild($this); } else { $this->clear(); } } /** * Detach node from parent * @param bool $move_children_up Only detach current node and replace it with child nodes * @internal jquery (naming) compatibility * @see delete() */ function detach($move_children_up = false) { if (($p = $this->parent) !== null) { $index = $this->index(); $this->parent = null; if ($move_children_up) { $this->moveChildren($p, $index); } $p->deleteChild($this, true); } } /** * Deletes all child nodes from node */ function clear() { foreach($this->children as $c) { $c->parent = null; $c->delete(); } $this->children = array(); } /** * Get top parent * @return DomNode Root, null if node has no parent */ function getRoot() { $r = $this->parent; $n = ($r === null) ? null : $r->parent; while ($n !== null) { $r = $n; $n = $r->parent; } return $r; } /** * Change parent * @param null|DomNode $to New parent, null if none * @param false|int $index Add child to parent if not present at index, false to not add, negative to count from end, null to append */ #php4 #function changeParent($to, &$index) { #php4e #php5 function changeParent($to, &$index = null) { #php5e if ($this->parent !== null) { $this->parent->deleteChild($this, true); } $this->parent = $to; if ($index !== false) { $new_index = $this->index(); if (!(is_int($new_index) && ($new_index >= 0))) { $this->parent->addChild($this, $index); } } } /** * Find out if node has (a certain) parent * @param DomNode|string $tag Match against parent, string to match tag, object to fully match node, null to return if node has parent * @param bool $recursive * @return bool */ function hasParent($tag = null, $recursive = false) { if ($this->parent !== null) { if ($tag === null) { return true; } elseif (is_string($tag)) { return (($this->parent->tag === $tag) || ($recursive && $this->parent->hasParent($tag))); } elseif (is_object($tag)) { return (($this->parent === $tag) || ($recursive && $this->parent->hasParent($tag))); } } return false; } /** * Find out if node is parent of a certain tag * @param DomNode|string $tag Match against parent, string to match tag, object to fully match node * @param bool $recursive * @return bool * @see hasParent() */ function isParent($tag, $recursive = false) { return ($this->hasParent($tag, $recursive) === ($tag !== null)); } /** * Find out if node is text * @return bool */ function isText() { return false; } /** * Find out if node is comment * @return bool */ function isComment() { return false; } /** * Find out if node is text or comment node * @return bool */ function isTextOrComment() { return false; } /** * Move node to other node * @param DomNode $to New parent, null if none * @param int $new_index Add child to parent at index if not present, null to not add, negative to count from end * @internal Performs {@link changeParent()} */ #php4 #function move($to, &$new_index) { #php4e #php5 function move($to, &$new_index = -1) { #php5e $this->changeParent($to, $new_index); } /** * Move child nodes to other node * @param DomNode $to New parent, null if none * @param int $new_index Add child to new node at index if not present, null to not add, negative to count from end * @param int $start Index from child node where to start wrapping, 0 for first element * @param int $end Index from child node where to end wrapping, -1 for last element */ #php4 #function moveChildren($to, &$new_index, $start = 0, $end = -1) { #php4e #php5 function moveChildren($to, &$new_index = -1, $start = 0, $end = -1) { #php5e if ($end < 0) { $end += count($this->children); } for ($i = $start; $i <= $end; $i++) { $this->children[$start]->changeParent($to, $new_index); } } /** * Index of node in parent * @param bool $count_all True to count all tags, false to ignore text and comments * @return int -1 if not found */ function index($count_all = true) { if (!$this->parent) { return -1; } elseif ($count_all) { return $this->parent->findChild($this); } else{ $index = -1; //foreach($this->parent->children as &$c) { // if (!$c->isTextOrComment()) { // ++$index; // } // if ($c === $this) { // return $index; // } //} foreach(array_keys($this->parent->children) as $k) { if (!$this->parent->children[$k]->isTextOrComment()) { ++$index; } if ($this->parent->children[$k] === $this) { return $index; } } return -1; } } /** * Change index of node in parent * @param int $index New index */ function setIndex($index) { if ($this->parent) { if ($index > $this->index()) { --$index; } $this->delete(); $this->parent->addChild($this, $index); } } /** * Index of all similar nodes in parent * @return int -1 if not found */ function typeIndex() { if (!$this->parent) { return -1; } else { $index = -1; //foreach($this->parent->children as &$c) { // if (strcasecmp($this->tag, $c->tag) === 0) { // ++$index; // } // if ($c === $this) { // return $index; // } //} foreach(array_keys($this->parent->children) as $k) { if (strcasecmp($this->tag, $this->parent->children[$k]->tag) === 0) { ++$index; } if ($this->parent->children[$k] === $this) { return $index; } } return -1; } } /** * Calculate indent of node (number of parent tags - 1) * @return int */ function indent() { return (($this->parent) ? $this->parent->indent() + 1 : -1); } /** * Get sibling node * @param int $offset Offset from current node * @return DomNode Null if not found */ function getSibling($offset = 1) { $index = $this->index() + $offset; if (($index >= 0) && ($index < $this->parent->childCount())) { return $this->parent->getChild($index); } else { return null; } } /** * Get node next to current * @param bool $skip_text_comments * @return DomNode Null if not found * @see getSibling() * @see getPreviousSibling() */ function getNextSibling($skip_text_comments = true) { $offset = 1; while (($n = $this->getSibling($offset)) !== null) { if ($skip_text_comments && ($n->tag[0] === '~')) { ++$offset; } else { break; } } return $n; } /** * Get node previous to current * @param bool $skip_text_comments * @return DomNode Null if not found * @see getSibling() * @see getNextSibling() */ function getPreviousSibling($skip_text_comments = true) { $offset = -1; while (($n = $this->getSibling($offset)) !== null) { if ($skip_text_comments && ($n->tag[0] === '~')) { --$offset; } else { break; } } return $n; } /** * Get namespace of node * @return string * @see setNamespace() */ function getNamespace() { if ($this->tag_ns === null) { $a = explode(':', $this->tag, 2); if (empty($a[1])) { $this->tag_ns = array('', $a[0]); } else { $this->tag_ns = array($a[0], $a[1]); } } return $this->tag_ns[0]; } /** * Set namespace of node * @param string $ns * @see getNamespace() */ function setNamespace($ns) { if ($this->getNamespace() !== $ns) { $this->tag_ns[0] = $ns; $this->tag = $ns.':'.$this->tag_ns[1]; } } /** * Get tagname of node (without namespace) * @return string * @see setTag() */ function getTag() { if ($this->tag_ns === null) { $this->getNamespace(); } return $this->tag_ns[1]; } /** * Set tag (with or without namespace) * @param string $tag * @param bool $with_ns Does $tag include namespace? * @see getTag() */ function setTag($tag, $with_ns = false) { $with_ns = $with_ns || (strpos($tag, ':') !== false); if ($with_ns) { $this->tag = $tag; $this->tag_ns = null; } elseif ($this->getTag() !== $tag) { $this->tag_ns[1] = $tag; $this->tag = (($this->tag_ns[0]) ? $this->tag_ns[0].':' : '').$tag; } } /** * Try to determine the encoding of the current tag * @return string|bool False if encoding could not be found */ function getEncoding() { $root = $this->getRoot(); if ($root !== null) { if ($enc = $root->select('meta[charset]', 0, true, true)) { return $enc->getAttribute("charset"); } elseif ($enc = $root->select('"?xml"[encoding]', 0, true, true)) { return $enc->getAttribute("encoding"); } elseif ($enc = $root->select('meta[content*="charset="]', 0, true, true)) { $enc = $enc->getAttribute("content"); return substr($enc, strpos($enc, "charset=")+8); } } return false; } /** * Number of children in node * @param bool $ignore_text_comments Ignore text/comments with calculation * @return int */ function childCount($ignore_text_comments = false) { if (!$ignore_text_comments) { return count($this->children); } else{ $count = 0; //foreach($this->children as &$c) { // if (!$c->isTextOrComment()) { // ++$count; // } //} foreach(array_keys($this->children) as $k) { if (!$this->children[$k]->isTextOrComment()) { ++$count; } } return $count; } } /** * Find node in children * @param DomNode $child * @return int False if not found */ function findChild($child) { return array_search($child, $this->children, true); } /** * Checks if node has another node as child * @param DomNode $child * @return bool */ function hasChild($child) { return ((bool) findChild($child)); } /** * Get childnode * @param int|DomNode $child Index, negative to count from end * @param bool $ignore_text_comments Ignore text/comments with index calculation * @return DomNode */ function &getChild($child, $ignore_text_comments = false) { if (!is_int($child)) { $child = $this->findChild($child); } elseif ($child < 0) { $child += $this->childCount($ignore_text_comments); } if ($ignore_text_comments) { $count = 0; $last = null; //foreach($this->children as &$c) { // if (!$c->isTextOrComment()) { // if ($count++ === $child) { // return $c; // } // $last = $c; // } //} foreach(array_keys($this->children) as $k) { if (!$this->children[$k]->isTextOrComment()) { if ($count++ === $child) { return $this->children[$k]; } $last = $this->children[$k]; } } return (($child > $count) ? $last : null); } else { return $this->children[$child]; } } /** * Add child node * @param string|DomNode $tag Tag name or object * @param int $offset Position to insert node, negative to count from end, null to append * @return DomNode Added node */ #php4 #function &addChild($tag, &$offset) { #php4e #php5 function &addChild($tag, &$offset = null) { #php5e if (is_array($tag)) { $tag = new $this->childClass($tag, $this); } elseif (is_string($tag)) { $nodes = $this->createNodes($tag); $tag = array_shift($nodes); if ($tag && $tag->parent !== $this) { $index = false; $tag->changeParent($this, $index); } } elseif (is_object($tag) && $tag->parent !== $this) { $index = false; //Needs to be passed by ref $tag->changeParent($this, $index); } if (is_int($offset) && ($offset < count($this->children)) && ($offset !== -1)) { if ($offset < 0) { $offset += count($this->children); } array_splice($this->children, $offset++, 0, array(&$tag)); } else { $this->children[] =& $tag; } return $tag; } /** * First child node * @param bool $ignore_text_comments Ignore text/comments with index calculation * @return DomNode */ function &firstChild($ignore_text_comments = false) { return $this->getChild(0, $ignore_text_comments); } /** * Last child node * @param bool $ignore_text_comments Ignore text/comments with index calculation * @return DomNode */ function &lastChild($ignore_text_comments = false) { return $this->getChild(-1, $ignore_text_comments); } /** * Insert childnode * @param string|DomNode $tag Tagname or object * @param int $offset Position to insert node, negative to count from end, null to append * @return DomNode Added node * @see addChild(); */ function &insertChild($tag, $index) { return $this->addChild($tag, $index); } /** * Add text node * @param string $text * @param int $offset Position to insert node, negative to count from end, null to append * @return DomNode Added node * @see addChild(); */ #php4 #function &addText($text, &$offset) { #php4e #php5 function &addText($text, &$offset = null) { #php5e return $this->addChild(new $this->childClass_Text($this, $text), $offset); } /** * Add comment node * @param string $text * @param int $offset Position to insert node, negative to count from end, null to append * @return DomNode Added node * @see addChild(); */ #php4 #function &addComment($text, &$offset) { #php4e #php5 function &addComment($text, &$offset = null) { #php5e return $this->addChild(new $this->childClass_Comment($this, $text), $offset); } /** * Add conditional node * @param string $condition * @param bool True for ';} } /** * Node subclass for conditional tags */ class ConditionalTagNode extends DomNode { #php4 Compatibility with PHP4, this gets changed to a regular var in release tool #static $NODE_TYPE = self::NODE_CONDITIONAL; #php4e #php5 const NODE_TYPE = self::NODE_CONDITIONAL; #php5e var $tag = '~conditional~'; /** * @var string */ var $condition = ''; /** * Class constructor * @param DomNode $parent * @param string $condition e.g. "if IE" * @param bool $hidden