Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
19.44% |
7 / 36 |
CRAP | |
17.31% |
27 / 156 |
Node | |
0.00% |
0 / 1 |
|
19.44% |
7 / 36 |
5984.96 | |
17.31% |
27 / 156 |
_subclass_isEqualNode | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
_subclass_cloneNodeShallow | n/a |
0 / 0 |
2 | n/a |
0 / 0 |
|||||
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
7 / 7 |
|||
getNodeType | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getNodeName | n/a |
0 / 0 |
1 | n/a |
0 / 0 |
|||||
getNodeValue | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
setNodeValue | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
getTextContent | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
setTextContent | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
getOwnerDocument | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getParentNode | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getParentElement | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
getPreviousSibling | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
getNextSibling | |
0.00% |
0 / 1 |
3.07 | |
80.00% |
4 / 5 |
|||
getChildNodes | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
getFirstChild | |
0.00% |
0 / 1 |
4.94 | |
40.00% |
2 / 5 |
|||
getLastChild | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 7 |
|||
hasChildNodes | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
insertBefore | |
0.00% |
0 / 1 |
2.02 | |
83.33% |
5 / 6 |
|||
appendChild | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
__unsafe_appendChild | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
replaceChild | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
removeChild | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
normalize | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 10 |
|||
compareDocumentPosition | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
contains | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
isSameNode | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
isEqualNode | |
0.00% |
0 / 1 |
72 | |
0.00% |
0 / 12 |
|||
cloneNode | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
lookupPrefix | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 1 |
|||
lookupNamespaceURI | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 1 |
|||
isDefaultNamespace | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
__set_owner | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
__is_rooted | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
__root | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 10 |
|||
__uproot | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 8 |
|||
__node_document | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
__sibling_index | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 10 |
|||
__remove_children | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 11 |
|||
_node_serialize | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
<?php | |
declare( strict_types = 1 ); | |
// @phan-file-suppress PhanCoalescingNeverUndefined | |
// @phan-file-suppress PhanParamSignatureMismatch | |
// @phan-file-suppress PhanTypeMismatchArgument | |
// @phan-file-suppress PhanTypeMismatchReturn | |
// @phan-file-suppress PhanUndeclaredMethod | |
// @phan-file-suppress PhanUndeclaredProperty | |
// @phan-file-suppress PhanUndeclaredTypeThrowsType | |
// phpcs:disable Generic.NamingConventions.CamelCapsFunctionName.MethodDoubleUnderscore | |
// phpcs:disable Generic.NamingConventions.CamelCapsFunctionName.ScopeNotCamelCaps | |
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic | |
// phpcs:disable MediaWiki.Commenting.FunctionComment.WrongStyle | |
// phpcs:disable MediaWiki.Commenting.PropertyDocumentation.MissingDocumentationPublic | |
// phpcs:disable MediaWiki.Commenting.PropertyDocumentation.WrongStyle | |
namespace Wikimedia\Dodo; | |
use Wikimedia\IDLeDOM\Node as INode; | |
/** | |
* Node.php | |
* -------- | |
* Defines a "Node", the primary datatype of the W3C Document Object Model. | |
* | |
* Conforms to W3C Document Object Model (DOM) Level 1 Recommendation | |
* (see: https://www.w3.org/TR/2000/WD-DOM-Level-1-20000929) | |
*/ | |
abstract class Node extends EventTarget implements \Wikimedia\IDLeDOM\Node { | |
// Stub out methods not yet implemented. | |
use \Wikimedia\IDLeDOM\Stub\Node; | |
use UnimplementedTrait; | |
// Helper functions from IDLeDOM | |
use \Wikimedia\IDLeDOM\Helper\Node; | |
/********************************************************************** | |
* Abstract methods that must be defined in subclasses | |
*/ | |
/** | |
* Delegated subclass method called by Node::isEqualNode() | |
* @param Node $node | |
* @return bool | |
*/ | |
abstract protected function _subclass_isEqualNode( Node $node ): bool; | |
/** | |
* Delegated subclass method called by Node::cloneNode() | |
* @return ?Node | |
*/ | |
abstract protected function _subclass_cloneNodeShallow(): ?Node; | |
/********************************************************************** | |
* Properties that appear in DOM-LS | |
*/ | |
/* | |
* SET WHEN SOMETHING APPENDS NODE | |
* | |
* @var Node|null should be considered read-only | |
*/ | |
public $_ownerDocument; | |
/** | |
* @see $_ownerDocument | |
* | |
* @var Node|null should be considered read-only | |
*/ | |
public $_parentNode; | |
/** | |
* DEVIATION FROM SPEC | |
* PURPOSE: SIBLING TRAVERSAL OPTIMIZATION | |
* | |
* If a Node has no siblings, i.e. it is the 'only child' of $_parentNode, then the | |
* properties $_nextSibling and $_previousSibling are set equal to $this. | |
* | |
* This is an optimization for traversing siblings, but in DOM-LS, these properties | |
* should be null in this scenario. | |
* | |
* The relevant accessors are spec-compliant, returning null in this situation. | |
* | |
* @var Node|null should be considered read-only | |
*/ | |
public $_nextSibling; | |
/** | |
* @see $_nextSibling | |
* @var Node|null should be considered read-only | |
*/ | |
public $_previousSibling; | |
/** | |
* SET WHEN NODE APPENDS SOMETHING | |
* | |
* @var Node|null should be considered read-only | |
*/ | |
public $_firstChild; | |
/* | |
* DEVIATION FROM SPEC | |
* PURPOSE: APPEND OPTIMIZATION | |
* | |
* The $_childNodes property holds an array-like object (a NodeList) referencing | |
* each of a Node's children as a live representation of the DOM. | |
* | |
* This 'liveness' is somewhat unperformant, and the upkeep of this object has | |
* a significant impact on append performance. | |
* | |
* So, this implementation chooses to defer its construction until a value | |
* is requested by calling Node::childNodes(). | |
* | |
* Until that time, it will have the value null. | |
* | |
* TODO this should have {at}var with the next line, but that breaks phan | |
* because even though NodeList extends ArrayObject, it can't be used in array_splice? | |
* | |
* NodeList|null should be considered read-only | |
*/ | |
public $_childNodes; | |
/********************************************************************** | |
* Properties that are for internal use by this library | |
*/ | |
/* | |
* DEVELOPERS NOTE: | |
* An index is assigned on ADOPTION. It uniquely identifies the Node | |
* within its owner Document. | |
* | |
* This index makes it simple to represent a Node as an integer. | |
* | |
* It exists for a single optimization. If two Elements have the same id, | |
* they will be stored in an array under their $document_index. This | |
* means we don't have to search the array for a matching Node, we can | |
* look it up in O(1). Yep. | |
* | |
* FIXME It is public because it gets used by the whatwg algorithms page. | |
*/ | |
public $__document_index; | |
/* | |
* DEVELOPERS NOTE: | |
* An index is assigned on INSERTION. It uniquely identifies the Node among | |
* its siblings. | |
* | |
* It is used to help compute document position and to mark where insertion should | |
* occur. | |
* | |
* Its existence is, frankly, mostly for convenience due to the fact that the most | |
* common representation of child nodes is a linked list that doesn't have numeric | |
* indices otherwise. | |
* | |
* FIXME It is public because it gets used by the whatwg algorithms page. | |
*/ | |
public $__sibling_index; | |
/* TODO: Unused */ | |
public $__roothook; | |
public function __construct() { | |
/* Our ancestors */ | |
$this->_ownerDocument = null; | |
$this->_parentNode = null; | |
/* Our children */ | |
$this->_firstChild = null; | |
$this->_childNodes = null; | |
/* Our siblings */ | |
$this->_nextSibling = $this; // for LL | |
$this->_previousSibling = $this; // for LL | |
} | |
/********************************************************************** | |
* ACCESSORS | |
*/ | |
/* | |
* Sometimes, subclasses will override | |
* nodeValue and textContent, so these | |
* accessors should be seen as "defaults," | |
* which in some cases are extended. | |
*/ | |
/** | |
* Return the node type enumeration for this node. | |
* @see https://dom.spec.whatwg.org/#dom-node-nodetype | |
* @return int | |
*/ | |
abstract public function getNodeType(): int; | |
/** | |
* Return the `nodeName` for this node. | |
* @see https://dom.spec.whatwg.org/#dom-node-nodename | |
* @return string | |
*/ | |
abstract public function getNodeName(): string; | |
/** | |
* Return the `value` for this node. | |
* @see https://dom.spec.whatwg.org/#dom-node-nodevalue | |
* @return ?string | |
*/ | |
public function getNodeValue() : ?string { | |
return null; // Override in subclasses | |
} | |
/** @inheritDoc */ | |
public function setNodeValue( ?string $val ) : void { | |
/* Any other node: Do nothing */ | |
} | |
/** | |
* Return the `textContent` for this node. | |
* @see https://dom.spec.whatwg.org/#dom-node-textcontent | |
* @return ?string | |
*/ | |
public function getTextContent() : ?string { | |
return null; // Override in subclasses | |
} | |
/** @inheritDoc */ | |
public function setTextContent( ?string $val ) : void { | |
/* Any other node: Do nothing */ | |
} | |
/** | |
* Nodes might not have an ownerDocument. Perhaps they have not been inserted | |
* into a DOM, or are themselves a Document. In those cases, the value of | |
* ownerDocument will be null. | |
* | |
* @inheritDoc | |
*/ | |
final public function getOwnerDocument() { | |
return $this->_ownerDocument; | |
} | |
/** | |
* Nodes might not have a parentNode. Perhaps they have not been inserted | |
* into a DOM, or are a Document node, which is the root of a DOM tree and | |
* thus has no parent. In those cases, the value of parentNode is null. | |
* | |
* @inheritDoc | |
*/ | |
final public function getParentNode() { | |
return $this->_parentNode; | |
} | |
/** | |
* This value is the same as parentNode, except it puts an extra condition, | |
* that the parentNode must be an Element. | |
* | |
* Accordingly, it requires no additional backing property, and can exist only | |
* as an accessor. | |
* | |
* @inheritDoc | |
*/ | |
final public function getParentElement() { | |
if ( $this->_parentNode === null ) { | |
return null; | |
} | |
if ( $this->_parentNode->getNodeType() === self::ELEMENT_NODE ) { | |
return $this->_parentNode; | |
} | |
return null; | |
} | |
/** @inheritDoc */ | |
final public function getPreviousSibling() { | |
if ( $this->_parentNode === null ) { | |
return null; | |
} | |
if ( $this->_parentNode->_firstChild === $this ) { | |
/* | |
* TODO: Why not check $this->_nextSibling === $this | |
* | |
* Is it because firstChild will be set to null if we should be using | |
* NodeList??? | |
*/ | |
return null; | |
} | |
return $this->_previousSibling; | |
} | |
/** @inheritDoc */ | |
final public function getNextSibling() { | |
if ( $this->_parentNode === null ) { | |
return null; | |
} | |
if ( $this->_nextSibling === $this->_parentNode->_firstChild ) { | |
/* | |
* TODO: Why not check $this->_nextSibling === $this | |
* | |
* Is it because firstChild will be set to null if we should be using | |
* NodeList??? | |
*/ | |
return null; | |
} | |
return $this->_nextSibling; | |
} | |
/** | |
* When, in other place of the code, you observe folks testing for | |
* $this->_childNodes, it is to see whether we should use the NodeList | |
* or the linked list traversal methods. | |
* | |
* FIXME: | |
* Wait, doesn't this need to be live? I mean, don't we need to re-compute | |
* this thing when things are appended or removed...? Or is it not live? | |
* | |
* @inheritDoc | |
*/ | |
public function getChildNodes() { | |
if ( $this->_childNodes === null ) { | |
/* | |
* If childNodes has never been created, we've now created it. | |
*/ | |
$this->_childNodes = new NodeList(); | |
for ( $c = $this->getFirstChild(); $c !== null; $c = $c->getNextSibling() ) { | |
$this->_childNodes[] = $c; | |
} | |
/* | |
* TODO: Must we? | |
* Setting this to null is a signal that we are not to use the Linked List, but | |
* it is stupid and I think we don't actually need it. | |
*/ | |
$this->_firstChild = null; | |
} | |
return $this->_childNodes; | |
} | |
/** | |
* CAUTION | |
* Directly accessing _firstChild alone is *not* a shortcut for this | |
* method. Depending on whether we are in NodeList or LinkedList mode, one | |
* or the other or both may be null. | |
* | |
* I'm trying to factor it out, but it will take some time. | |
* | |
* @inheritDoc | |
*/ | |
public function getFirstChild() { | |
if ( $this->_childNodes === null ) { | |
/* | |
* If we are using the Linked List representation, then just return | |
* the backing property (may still be null). | |
*/ | |
return $this->_firstChild; | |
} | |
if ( isset( $this->_childNodes[0] ) ) { | |
/* | |
* If we are using the NodeList representation, and the | |
* NodeList is not empty, then return the first item in the | |
* NodeList. | |
*/ | |
return $this->_childNodes[0]; | |
} | |
/* Otherwise, the NodeList is empty, so return null. */ | |
return null; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function getLastChild() { | |
if ( $this->_childNodes === null ) { | |
/* If we are using the Linked List representation. */ | |
if ( $this->_firstChild !== null ) { | |
/* If we have a firstChild, its previousSibling is the last child. */ | |
return $this->_firstChild->previousSibling(); | |
} else { | |
/* Otherwise there are no children, and so last child is null. */ | |
return null; | |
} | |
} else { | |
/* If we are using the NodeList representation. */ | |
if ( isset( $this->_childNodes[0] ) ) { | |
/* | |
* If there is at least one element in the NodeList, return the | |
* last element in the NodeList. | |
*/ | |
return end( $this->_childNodes ); | |
} else { | |
/* Otherwise, there are no children, and so last child is null. */ | |
return null; | |
} | |
} | |
} | |
/** | |
* CAUTION | |
* Testing _firstChild or _childNodes alone is *not* a shortcut for this | |
* method. Depending on whether we are in NodeList or LinkedList mode, one | |
* or the other or both may be null. | |
* | |
* I'm trying to factor it out, but it will take some time. | |
* | |
* @inheritDoc | |
*/ | |
public function hasChildNodes(): bool { | |
if ( $this->_childNodes === null ) { | |
/* | |
* If we are using the Linked List representation, then the NULL-ity | |
* of firstChild is diagnostic. | |
*/ | |
return $this->_firstChild !== null; | |
} else { | |
/* | |
* If we are using the NodeList representation, then the | |
* non-emptiness of childNodes is diagnostic. | |
*/ | |
return !empty( $this->_childNodes ); | |
} | |
} | |
/********************************************************************** | |
* MUTATION ALGORITHMS | |
*/ | |
/** | |
* Insert $node as a child of $this, and insert it before $refChild | |
* in the document order. | |
* | |
* spec DOM-LS | |
* | |
* THINGS TO KNOW FROM THE SPEC: | |
* | |
* 1. If $node already exists in | |
* this Document, this function | |
* moves it from its current | |
* position to its new position | |
* ('move' means 'remove' followed | |
* by 're-insert'). | |
* | |
* 2. If $refNode is NULL, then $node | |
* is added to the end of the list | |
* of children of $this. In other | |
* words, insertBefore($node, NULL) | |
* is equivalent to appendChild($node). | |
* | |
* 3. If $node is a DocumentFragment, | |
* the children of the DocumentFragment | |
* are moved into the child list of | |
* $this, and the empty DocumentFragment | |
* is returned. | |
* | |
* THINGS TO KNOW IN LIFE: | |
* | |
* Despite its weird syntax (blame the spec), | |
* this is a real workhorse, used to implement | |
* all of the non-replacing insertion mutations. | |
* | |
* @param INode $node To be inserted | |
* @param ?INode $refNode Child of this node before which to insert $node | |
* @return INode Newly inserted Node or empty DocumentFragment | |
* @throws DOMException "HierarchyRequestError" or "NotFoundError" | |
*/ | |
public function insertBefore( $node, $refNode ) { | |
/* | |
* [1] | |
* Ensure pre-insertion validity. | |
* Validation failure will throw | |
* DOMException "HierarchyRequestError" or | |
* DOMException "NotFoundError". | |
*/ | |
WhatWG::ensure_insert_valid( $node, $this, $refNode ); | |
/* | |
* [2] | |
* If $refNode is $node, re-assign | |
* $refNode to the next sibling of | |
* $node. This may well be NULL. | |
*/ | |
if ( $refNode === $node ) { | |
$refNode = $node->getNextSibling(); | |
} | |
/* | |
* [3] | |
* Adopt $node into the Document | |
* to which $this is rooted. | |
*/ | |
$this->__node_document()->adoptNode( $node ); | |
/* | |
* [4] | |
* Run the complicated algorithm | |
* to Insert $node into $this at | |
* a position before $refNode. | |
*/ | |
WhatWG::insert_before_or_replace( $node, $this, $refNode, false ); | |
/* | |
* [5] | |
* Return $node | |
*/ | |
return $node; | |
} | |
/** @inheritDoc */ | |
public function appendChild( $node ) { | |
return $this->insertBefore( $node, null ); | |
} | |
/** | |
* Does not check for insertion validity. This out-performs PHP DOMDocument by | |
* over 2x. | |
* | |
* @param Node $node | |
* @return Node | |
*/ | |
final public function __unsafe_appendChild( Node $node ): Node { | |
WhatWG::insert_before_or_replace( $node, $this, null, false ); | |
return $node; | |
} | |
/** @inheritDoc */ | |
public function replaceChild( $new, $old ) { | |
/* | |
* [1] | |
* Ensure pre-replacement validity. | |
* Validation failure will throw | |
* DOMException "HierarchyRequestError" or | |
* DOMException "NotFoundError". | |
*/ | |
WhatWG::ensure_replace_valid( $new, $this, $old ); | |
/* | |
* [2] | |
* Adopt $node into the Document | |
* to which $this is rooted. | |
*/ | |
if ( $new->__node_document() !== $this->__node_document() ) { | |
/* | |
* FIXME | |
* adoptNode has a side-effect | |
* of removing the adopted node | |
* from its parent, which | |
* generates a mutation event, | |
* causing _insertOrReplace to | |
* generate 2 deletes and 1 insert | |
* instead of a 'move' event. | |
* | |
* It looks like the MutationObserver | |
* stuff avoids this problem, but for | |
* now let's only adopt (ie, remove | |
* 'node' from its parent) here if we | |
* need to. | |
*/ | |
$this->__node_document()->adoptNode( $new ); | |
} | |
/* | |
* [4] | |
* Run the complicated algorithm | |
* to replace $old with $new. | |
*/ | |
WhatWG::insert_before_or_replace( $new, $this, $old, true ); | |
/* | |
* [5] | |
* Return $old | |
*/ | |
return $old; | |
} | |
/** @inheritDoc */ | |
public function removeChild( $node ) { | |
if ( $this === $node->_parentNode ) { | |
/* Defined on ChildNode class */ | |
$node->remove(); | |
} else { | |
/* That's not my child! */ | |
Util::error( "NotFoundError" ); | |
} | |
/* | |
* The spec requires that | |
* the return value always | |
* be equal to $node. | |
*/ | |
return $node; | |
} | |
/** | |
* Puts $this and the entire subtree | |
* rooted at $this into "normalized" | |
* form. | |
* | |
* In a normalized sub-tree, no text | |
* nodes in the sub-tree are empty, | |
* and there are no adjacent text nodes. | |
* | |
* @see https://dom.spec.whatwg.org/#dom-node-normalize | |
* @inheritDoc | |
*/ | |
final public function normalize() : void { | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
/* | |
* [0] | |
* Proceed to traverse the | |
* subtree in a depth-first | |
* fashion. | |
*/ | |
$n->normalize(); | |
if ( $n->getNodeType() === self::TEXT_NODE ) { | |
if ( $n->getNodeValue() === '' ) { | |
/* | |
* [1] | |
* If you are a text node, | |
* and you are empty, then | |
* you get pruned. | |
*/ | |
$this->removeChild( $n ); | |
} else { | |
$p = $n->previousSibling(); | |
if ( $p && $p->getNodeType() === self::TEXT_NODE ) { | |
/* | |
* [2] | |
* If you are a text node, | |
* and you are not empty, | |
* and you follow a | |
* non-empty text node | |
* (if it were empty, it | |
* would have been pruned | |
* in the depth-first | |
* traversal), then you | |
* get merged into that | |
* previous non-empty text | |
* node. | |
*/ | |
$p->appendData( $n->getNodeValue() ); | |
$this->removeChild( $n ); | |
} | |
} | |
} | |
} | |
} | |
/********************************************************************** | |
* COMPARISONS AND PREDICATES | |
*/ | |
/** @inheritDoc */ | |
final public function compareDocumentPosition( $that ): int { | |
/* | |
* CAUTION | |
* The order of these args matters | |
*/ | |
return WhatWG::compare_document_position( $that, $this ); | |
} | |
/** @inheritDoc */ | |
final public function contains( $node ): bool { | |
if ( $node === null ) { | |
return false; | |
} | |
if ( $this === $node ) { | |
/* As per the DOM-LS, containment is inclusive. */ | |
return true; | |
} | |
return ( $this->compareDocumentPosition( $node ) & self::DOCUMENT_POSITION_CONTAINED_BY ) !== 0; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
final public function isSameNode( $node ): bool { | |
return $this === $node; | |
} | |
/** | |
* Determine whether this node and $other are equal | |
* | |
* spec: DOM-LS | |
* | |
* NOTE: | |
* Each subclass of Node has its own criteria for equality. | |
* Rather than extend Node::isEqualNode(), subclasses | |
* must implement _subclass_isEqualNode(), which is called | |
* from Node::isEqualNode() and handles all of the equality | |
* testing specific to the subclass. | |
* | |
* This allows the recursion and other fast checks to be | |
* handled here and written just once. | |
* | |
* Yes, we realize it's a bit weird. | |
* | |
* @inheritDoc | |
*/ | |
public function isEqualNode( $node ): bool { | |
if ( $node === null ) { | |
/* We're not equal to NULL */ | |
return false; | |
} | |
if ( $node->getNodeType() !== $this->getNodeType() ) { | |
/* If we're not the same nodeType, we can stop */ | |
return false; | |
} | |
if ( !$this->_subclass_isEqualNode( $node ) ) { | |
/* Run subclass-specific equality comparison */ | |
return false; | |
} | |
/* Call this method on the children of both nodes */ | |
for ( | |
$a = $this->getFirstChild(), $b = $node->getFirstChild(); | |
$a !== null && $b !== null; | |
$a = $a->getNextSibling(), $b = $b->getNextSibling() | |
) { | |
if ( !$a->isEqualNode( $b ) ) { | |
return false; | |
} | |
} | |
/* If we got through all of the children (why wouldn't we?) */ | |
return $a === null && $b === null; | |
} | |
/** | |
* Clone this Node | |
* | |
* spec DOM-LS | |
* | |
* NOTE: | |
* 1. If $deep is false, then no child nodes are cloned, including | |
* any text the node contains (since these are Text nodes). | |
* 2. The duplicate returned by this method is not part of any | |
* document until it is added using ::appendChild() or similar. | |
* 3. Initially (DOM4) , $deep was optional with default of 'true'. | |
* Currently (DOM4-LS), $deep is optional with default of 'false'. | |
* 4. Shallow cloning is delegated to _subclass_cloneNodeShallow(), | |
* which needs to be implemented by the subclass. | |
* For a similar pattern, see Node::isEqualNode(). | |
* 5. All "deep clones" are a shallow clone followed by recursion on | |
* the tree structure, so this suffices to capture subclass-specific | |
* behavior. | |
* | |
* @param bool $deep if true, clone entire subtree | |
* @return ?Node (clone of $this) | |
*/ | |
public function cloneNode( bool $deep = false ) { | |
/* Make a shallow clone using the delegated method */ | |
$clone = $this->_subclass_cloneNodeShallow(); | |
/* If the shallow clone is all we wanted, we're done. */ | |
if ( $deep === false ) { | |
return $clone; | |
} | |
/* Otherwise, recurse on the children */ | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
/* APPEND DIRECTLY; NO CHECKINSERTVALID */ | |
WhatWG::insert_before_or_replace( $clone, $n->cloneNode( true ), null, false ); | |
} | |
return $clone; | |
} | |
/** | |
* Return DOMString containing prefix for given namespace URI. | |
* | |
* spec DOM-LS | |
* | |
* NOTE | |
* Think this function looks weird? It's actually spec: | |
* https://dom.spec.whatwg.org/#locate-a-namespace | |
* | |
* @inheritDoc | |
*/ | |
public function lookupPrefix( ?string $ns ): ?string { | |
return WhatWG::locate_prefix( $this, $ns ); | |
} | |
/** | |
* Return DOMString containing namespace URI for a given prefix | |
* | |
* NOTE | |
* Inverse of Node::lookupPrefix | |
* | |
* @inheritDoc | |
*/ | |
public function lookupNamespaceURI( ?string $prefix ): ?string { | |
return WhatWG::locate_namespace( $this, $prefix ); | |
} | |
/** | |
* Determine whether this is the default namespace | |
* | |
* @inheritDoc | |
*/ | |
public function isDefaultNamespace( ?string $ns ): bool { | |
return ( $ns ?? null ) === $this->lookupNamespaceURI( null ); | |
} | |
/********************************************************************** | |
* UTILITY METHODS AND DODO EXTENSIONS | |
*/ | |
/* | |
* You were sorting out ROOTEDNESS AND STUFF | |
* At the same time, you were unravelling the | |
* crucial function ChildNode::remove. | |
* | |
* | |
* There are three distinct phases in which a Node | |
* can exist, and the state diagram works like | |
* this: | |
* | |
* [1] Unowned, Unrooted | |
* 7| | |
* / Document::adoptNode() | |
* / v | |
* Node::remove() [2] Owned, Unrooted | |
* \ | | |
* \ Document:;insertBefore() | |
* \v | |
* [3] Owned, Rooted | |
* | |
* [1]->[2] (adoption) | |
* Sets: | |
* ownerDocument on Nodes of subtree rooted at Node | |
* __document_index on Nodes of subtree rooted at Node | |
* | |
* [2]->[3] (insertion) | |
* Sets: | |
* parentNode on Node | |
* nextSibling on Node | |
* previousSibling on Node | |
* __sibling_index on Node | |
* | |
* Possibly sets: | |
* firstChild on parent of Node, if Node is | |
* the first child. | |
* | |
* [3]->[1] (removal) | |
* Unsets: | |
* parentNode | |
* nextSibling | |
* previousSibling | |
* __sibling_index | |
* parentNode->firstChild, if we were last | |
* ??? | |
* Does it unset ownerDocument? | |
* Does it unset __document_index? | |
* (remove_from_node_table does this) | |
* | |
* __document_index is being set by add_to_node_table. ugh | |
* __document_index is being set by add_to_node_table. ugh | |
* | |
* TODO | |
* Centralize all of this. | |
* For instance, node->removeChild(node) | |
* should just call node->remove()? | |
* | |
* Document::importNode($node) | |
* $this->adoptNode($node->clone()) | |
* Document::insertBefore() | |
* Node::insertBefore() | |
* update_document_stuff; | |
* Document::replaceChild() | |
* Node::replaceChild() | |
* update_document_stuff; | |
* Document::removeChild() | |
* Node::removeChild() | |
* update_document_stuff; | |
* Document::cloneNode() | |
* Node::cloneNode(); | |
* (clone children) | |
* update_document_stuff | |
* | |
* FIXME: This is an antipattern right here. | |
* These don't need to be re-defined on the | |
* Document. | |
* | |
* Already, insert_before_or_replace is calling | |
* node->__root() | |
* node->mutate | |
* | |
* and FIXME update_document_state is just | |
* setting whether the document has a doctype | |
* node or a document element. it's horrible. | |
* | |
* And where is __document_index being set? | |
*/ | |
/** | |
* Set the ownerDocument reference on a subtree rooted at $this. | |
* | |
* When a Node becomes part of a Document, even if it is not yet inserted. | |
* | |
* Called by Document::adoptNode() | |
* | |
* @param Document $doc | |
*/ | |
public function __set_owner( Document $doc ) { | |
$this->_ownerDocument = $doc; | |
/* FIXME: Wat ? */ | |
if ( method_exists( $this, "tagName" ) ) { | |
/* Element subclasses might need to change case */ | |
$this->tagName = null; | |
} | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
$n->__set_owner( $n, $owner ); | |
} | |
} | |
/** | |
* Determine whether this Node is rooted (belongs to a tree) | |
* | |
* @return bool | |
* | |
* NOTE | |
* A Node is rooted if it belongs to a tree, in which case it will | |
* have an ownerDocument. Document nodes maintain a list of all the | |
* nodes inside their tree, assigning each an index, | |
* Node::__document_index. | |
* | |
* Therefore if we are currently rooted, we can tell by checking that | |
* we have one of these. | |
* | |
* TODO: This should be Node::isConnected(), see spec. | |
*/ | |
public function __is_rooted(): bool { | |
return (bool)$this->__document_index; | |
} | |
/* Called by WhatWG::insert_before_or_replace */ | |
/* | |
* TODO | |
* This is the only place where | |
* __add_to_node_table | |
* __add_from_id_table | |
* is called. | |
* | |
* FIXME | |
* The *REASON* that this, and __uproot(), | |
* and __set_owner() exist, is fundamentally | |
* that they need to operate recursively on | |
* the subtree, which means it needs to be | |
* down here on Node. | |
* | |
* All of this extra stuff in here just | |
* crept in here over time. | |
*/ | |
public function __root(): void { | |
$doc = $this->getOwnerDocument(); | |
if ( $this->getNodeType() === self::ELEMENT_NODE ) { | |
/* getElementById table */ | |
$id = $this->getAttribute( 'id' ); | |
if ( $id !== null ) { | |
$doc->__add_to_id_table( $id, $this ); | |
} | |
/* <SCRIPT> elements use this hook */ | |
/* TODO This hook */ | |
if ( $this->__roothook ) { | |
$this->__roothook(); | |
} | |
/* | |
* TODO: Why do we only do this for Element? | |
* This is how it was written in Domino. Is this | |
* a bug? | |
* | |
* Oh, I see, it doesn't recurse if the first | |
* thing isn't an ELEMENT? Well, maybe then | |
* it can't have children? I dunno. | |
*/ | |
/* RECURSE ON CHILDREN */ | |
/* | |
* TODO | |
* What if we didn't use recursion to do this? | |
* What if we used some other way? Wouldn't that | |
* make it even faster? | |
* | |
* What if we somehow had a list of indices in | |
* documentorder that would give us the subtree. | |
*/ | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
$n->__root(); | |
} | |
} | |
} | |
/* | |
* TODO | |
* This is the only place where | |
* __remove_from_id_table | |
* __remove_from_node_table | |
* is called. | |
*/ | |
public function __uproot(): void { | |
$doc = $this->getOwnerDocument(); | |
/* Manage id to element mapping */ | |
if ( $this->getNodeType() === self::ELEMENT_NODE ) { | |
$id = $this->getAttribute( 'id' ); | |
if ( $id !== null ) { | |
$doc->__remove_from_id_table( $id, $this ); | |
} | |
} | |
/* | |
* TODO: And here we don't restrict to ELEMENT_NODE. | |
* Why not? I think this is the intended behavior, no? | |
* Then does that make the behavior in root() a bug? | |
* Go over with Scott. | |
*/ | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
$n->__uproot(); | |
} | |
} | |
/** | |
* The document this node is associated to. | |
* | |
* spec DOM-LS | |
* | |
* NOTE | |
* How is this different from ownerDocument? According to DOM-LS, | |
* Document::ownerDocument() must equal NULL, even though it's often | |
* more convenient if a document is its own owner. | |
* | |
* What we're looking for is the "node document" concept, as laid | |
* out in the DOM-LS spec: | |
* | |
* -"Each node has an associated node document, set upon creation, | |
* that is a document." | |
* | |
* -"A node's node document can be changed by the 'adopt' | |
* algorithm." | |
* | |
* -"The node document of a document is that document itself." | |
* | |
* -"All nodes have a node document at all times." | |
* | |
* TODO | |
* Does the DOM-LS method Node::getRootNode (not implemented here) | |
* in its non-shadow-tree branch, do the same thing? | |
* | |
* TODO | |
* Wouldn't it fit better with all the __root* junk if it were | |
* called __root_node? | |
* | |
* @return Document | |
*/ | |
public function __node_document(): Document { | |
return $this->_ownerDocument ?? $this; | |
} | |
/** | |
* The index of this Node in its parent's childNodes list | |
* | |
* @return int index | |
* @throws Something if we have no parent | |
* | |
* NOTE | |
* Calling Node::__sibling_index() will automatically trigger a switch | |
* to the NodeList representation (see Node::childNodes()). | |
*/ | |
public function __sibling_index(): int { | |
if ( $this->_parentNode === null ) { | |
return 0; /* ??? TODO: throw or make an error ??? */ | |
} | |
if ( $this === $this->_parentNode->getFirstChild() ) { | |
return 0; | |
} | |
/* We fire up the NodeList mode */ | |
$childNodes = $this->_parentNode->childNodes(); | |
/* We end up re-indexing here if we ever run into trouble */ | |
if ( $this->___sibling_index === null || $childNodes[$this->___sibling_index] !== $this ) { | |
/* | |
* Ensure that we don't have an O(N^2) blowup | |
* if none of the kids have defined indices yet | |
* and we're traversing via nextSibling or | |
* previousSibling | |
*/ | |
foreach ( $childNodes as $i => $child ) { | |
$child->___sibling_index = $i; | |
} | |
Util::assert( $childNodes[$this->___sibling_index] === $this ); | |
} | |
return $this->___sibling_index; | |
} | |
/** | |
* Remove all of the Node's children. | |
* | |
* NOTE | |
* Provides minor optimization over iterative calls to | |
* Node::removeChild(), since it calls Node::modify() once. | |
* TODO: Node::modify() no longer exists. Does this optimization? | |
*/ | |
public function __remove_children() { | |
if ( $this->__is_rooted() ) { | |
$root = $this->_ownerDocument; | |
} else { | |
$root = null; | |
} | |
/* Go through all the children and remove me as their parent */ | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
if ( $root !== null ) { | |
/* If we're rooted, mutate */ | |
$root->__mutate_remove( $n ); | |
} | |
$n->_parentNode = null; | |
} | |
/* Remove the child node memory or references on this node */ | |
if ( $this->_childNodes !== null ) { | |
/* BRANCH: NodeList (array-like) */ | |
$this->_childNodes = new NodeList(); | |
} else { | |
/* BRANCH: circular linked list */ | |
$this->_firstChild = null; | |
} | |
} | |
/** | |
* Convert the children of a node to an HTML string. | |
* This is used by the innerHTML getter | |
* | |
* @return string | |
*/ | |
public function _node_serialize(): string { | |
$s = ""; | |
for ( $n = $this->getFirstChild(); $n !== null; $n = $n->getNextSibling() ) { | |
$s .= WhatWG::serialize_node( $n, $this ); | |
} | |
return $s; | |
} | |
} |