5 use wcf\system\exception\SystemException
;
8 * Provides helper methods to work with PHP's DOM implementation.
10 * @author Alexander Ebert
11 * @copyright 2001-2019 WoltLab GmbH
12 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
13 * @package WoltLabSuite\Core\Util
18 * Moves all child nodes from given element into a document fragment.
20 * @param \DOMElement $element element
21 * @return \DOMDocumentFragment document fragment containing all child nodes from `$element`
23 public static function childNodesToFragment(\DOMElement
$element)
25 $fragment = $element->ownerDocument
->createDocumentFragment();
27 while ($element->hasChildNodes()) {
28 $fragment->appendChild($element->childNodes
->item(0));
35 * Returns true if `$ancestor` contains the node `$node`.
37 * @param \DOMNode $ancestor ancestor node
38 * @param \DOMNode $node node
39 * @return bool true if `$ancestor` contains the node `$node`
41 public static function contains(\DOMNode
$ancestor, \DOMNode
$node)
43 // nodes cannot contain themselves
44 if ($ancestor === $node) {
48 // text nodes cannot contain any other nodes
49 if ($ancestor->nodeType
=== \XML_TEXT_NODE
) {
54 while ($parent = $parent->parentNode
) {
55 if ($parent === $ancestor) {
64 * Returns a static list of child nodes of provided element.
66 * @param \DOMElement $element target element
67 * @return \DOMNode[] list of child nodes
69 public static function getChildNodes(\DOMElement
$element)
72 foreach ($element->childNodes
as $node) {
80 * Returns the common ancestor of both nodes.
82 * @param \DOMNode $node1 first node
83 * @param \DOMNode $node2 second node
84 * @return \DOMNode|null common ancestor or null
86 public static function getCommonAncestor(\DOMNode
$node1, \DOMNode
$node2)
88 // abort if both elements share a common element or are both direct descendants
89 // of the same document
90 if ($node1->parentNode
=== $node2->parentNode
) {
91 return $node1->parentNode
;
94 // collect the list of all direct ancestors of `$node1`
95 $parents = self
::getParents($node1);
97 // compare each ancestor of `$node2` to the known list of parents of `$node1`
99 while ($parent = $parent->parentNode
) {
100 // requires strict type check
101 if (\
in_array($parent, $parents, true)) {
108 * Returns a non-live collection of elements.
110 * @param (\DOMDocument|\DOMElement) $context context element
111 * @param string $tagName tag name
112 * @return \DOMElement[] list of elements
113 * @throws SystemException
115 public static function getElements($context, $tagName)
117 if (!($context instanceof \DOMDocument
) && !($context instanceof \DOMElement
)) {
118 throw new SystemException("Expected context to be either of type \\DOMDocument or \\DOMElement.");
122 foreach ($context->getElementsByTagName($tagName) as $element) {
123 $elements[] = $element;
130 * Returns the immediate parent element before provided ancestor element. Returns null if
131 * the ancestor element is the direct parent of provided node.
133 * @param \DOMNode $node node
134 * @param \DOMElement $ancestor ancestor node
135 * @return \DOMElement|null immediate parent element before ancestor element
137 public static function getParentBefore(\DOMNode
$node, \DOMElement
$ancestor)
139 if ($node->parentNode
=== $ancestor) {
143 $parents = self
::getParents($node);
144 for ($i = \
count($parents) - 1; $i >= 0; $i--) {
145 if ($parents[$i] === $ancestor) {
146 return $parents[$i - 1];
150 throw new \
InvalidArgumentException("Provided node is a not a descendant of ancestor element.");
154 * Returns the parent node of given node.
156 * @param \DOMNode $node node
157 * @return \DOMNode parent node, can be `\DOMElement` or `\DOMDocument`
159 public static function getParentNode(\DOMNode
$node)
161 return $node->parentNode ?
: $node->ownerDocument
;
165 * Returns all ancestors nodes for given node.
167 * @param \DOMNode $node node
168 * @param bool $reverseOrder reversing the order causes the most top ancestor to appear first
169 * @return \DOMElement[] list of ancestor nodes
171 public static function getParents(\DOMNode
$node, $reverseOrder = false)
176 while ($parent = $parent->parentNode
) {
177 $parents[] = $parent;
180 return $reverseOrder ? \array_reverse
($parents) : $parents;
184 * Returns a cloned parent tree that is virtually readonly. In fact it can be
185 * modified, but all changes are non permanent and do not affect the source
188 * @param \DOMNode $node node
189 * @return \DOMElement[] list of parent elements
191 public static function getReadonlyParentTree(\DOMNode
$node)
194 /** @var \DOMElement $parent */
195 foreach (self
::getParents($node) as $parent) {
196 // do not include <body>, <html> and the document itself
197 if ($parent->nodeName
=== 'body') {
201 $tree[] = $parent->cloneNode(false);
208 * Determines the relative position of two nodes to each other.
210 * @param \DOMNode $node1 first node
211 * @param \DOMNode $node2 second node
214 public static function getRelativePosition(\DOMNode
$node1, \DOMNode
$node2)
216 if ($node1->ownerDocument
!== $node2->ownerDocument
) {
217 throw new \
InvalidArgumentException("Both nodes must be contained in the same DOM document.");
220 $nodeList1 = self
::getParents($node1, true);
221 $nodeList1[] = $node1;
223 $nodeList2 = self
::getParents($node2, true);
224 $nodeList2[] = $node2;
227 while ($nodeList1[$i] === $nodeList2[$i]) {
231 // check if parent of node 2 appears before parent of node 1
232 $previousSibling = $nodeList1[$i];
233 while ($previousSibling = $previousSibling->previousSibling
) {
234 if ($previousSibling === $nodeList2[$i]) {
239 $nextSibling = $nodeList1[$i];
240 while ($nextSibling = $nextSibling->nextSibling
) {
241 if ($nextSibling === $nodeList2[$i]) {
246 throw new \
RuntimeException("Unable to determine relative node position.");
250 * Returns true if there is at least one parent with the provided tag name.
252 * @param \DOMElement $element start element
253 * @param string $tagName tag name to match
256 public static function hasParent(\DOMElement
$element, $tagName)
258 while ($element = $element->parentNode
) {
259 if ($element->nodeName
=== $tagName) {
268 * Inserts given DOM node after the reference node.
270 * @param \DOMNode $node node
271 * @param \DOMNode $refNode reference node
273 public static function insertAfter(\DOMNode
$node, \DOMNode
$refNode)
275 if ($refNode->nextSibling
) {
276 self
::insertBefore($node, $refNode->nextSibling
);
278 self
::getParentNode($refNode)->appendChild($node);
283 * Inserts given node before the reference node.
285 * @param \DOMNode $node node
286 * @param \DOMNode $refNode reference node
288 public static function insertBefore(\DOMNode
$node, \DOMNode
$refNode)
290 self
::getParentNode($refNode)->insertBefore($node, $refNode);
294 * Returns true if this node is empty.
296 * @param \DOMNode $node node
297 * @return bool true if node is empty
299 public static function isEmpty(\DOMNode
$node)
301 if ($node->nodeType
=== \XML_TEXT_NODE
) {
302 return StringUtil
::trim($node->nodeValue
) === '';
303 } elseif ($node->nodeType
=== \XML_ELEMENT_NODE
) {
304 /** @var \DOMElement $node */
305 if (self
::isVoidElement($node)) {
307 } elseif ($node->hasChildNodes()) {
308 for ($i = 0, $length = $node->childNodes
->length
; $i < $length; $i++
) {
309 if (!self
::isEmpty($node->childNodes
->item($i))) {
322 * Returns true if given node is the first node of its given ancestor.
324 * @param \DOMNode $node node
325 * @param \DOMElement $ancestor ancestor element
326 * @return bool true if `$node` is the first node of its given ancestor
328 public static function isFirstNode(\DOMNode
$node, \DOMElement
$ancestor)
330 if ($node->previousSibling
=== null) {
331 if ($node->parentNode
=== $ancestor ||
$node->parentNode
->nodeName
=== 'body') {
334 return self
::isFirstNode($node->parentNode
, $ancestor);
336 } elseif ($node->parentNode
->nodeName
=== 'body') {
344 * Returns true if given node is the last node of its given ancestor.
346 * @param \DOMNode $node node
347 * @param \DOMElement $ancestor ancestor element
348 * @return bool true if `$node` is the last node of its given ancestor
350 public static function isLastNode(\DOMNode
$node, \DOMElement
$ancestor)
352 if ($node->nextSibling
=== null) {
353 if ($node->parentNode
=== null) {
354 throw new \
InvalidArgumentException("Provided node is a not a descendant of ancestor element.");
355 } elseif ($node->parentNode
=== $ancestor ||
$node->parentNode
->nodeName
=== 'body') {
358 return self
::isLastNode($node->parentNode
, $ancestor);
360 } elseif ($node->parentNode
->nodeName
=== 'body') {
368 * Nodes can get partially destroyed in which they're still an
369 * actual DOM node (such as \DOMElement) but almost their entire
370 * body is gone, including the `nodeType` attribute.
372 * @param \DOMNode $node node
373 * @return bool true if node has been destroyed
375 public static function isRemoved(\DOMNode
$node)
377 return !isset($node->nodeType
);
381 * Returns true if provided element is a void element. Void elements are elements
382 * that neither contain content nor have a closing tag, such as `<br>`.
384 * @param \DOMElement $element element
385 * @return bool true if provided element is a void element
387 public static function isVoidElement(\DOMElement
$element)
391 '~^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$~',
402 * Moves all nodes into `$container` until it reaches `$lastElement`. The direction
403 * in which nodes will be considered for moving is determined by the logical position
406 * @param \DOMElement $container destination element
407 * @param \DOMElement $lastElement last element to move
408 * @param \DOMElement $commonAncestor common ancestor of `$container` and `$lastElement`
410 public static function moveNodesInto(\DOMElement
$container, \DOMElement
$lastElement, \DOMElement
$commonAncestor)
412 if (!self
::contains($commonAncestor, $container)) {
413 throw new \
InvalidArgumentException(
414 "The container element must be a child of the common ancestor element."
416 } elseif ($lastElement->parentNode
!== $commonAncestor) {
417 throw new \
InvalidArgumentException(
418 "The last element must be a direct child of the common ancestor element."
422 $relativePosition = self
::getRelativePosition($container, $lastElement);
424 // move everything that is logically after `$container` but within
425 // `$commonAncestor` into `$container` until `$lastElement` has been moved
426 $element = $container;
428 if ($relativePosition === 'before') {
429 while ($sibling = $element->previousSibling
) {
430 self
::prepend($sibling, $container);
431 if ($sibling === $lastElement) {
436 while ($sibling = $element->nextSibling
) {
437 $container->appendChild($sibling);
438 if ($sibling === $lastElement) {
444 $element = $element->parentNode
;
445 } while ($element !== $commonAncestor);
449 * Normalizes an element by joining adjacent text nodes.
451 * @param \DOMElement $element target element
453 public static function normalize(\DOMElement
$element)
455 $childNodes = self
::getChildNodes($element);
456 /** @var \DOMNode $lastTextNode */
457 $lastTextNode = null;
458 foreach ($childNodes as $childNode) {
459 if ($childNode->nodeType
!== \XML_TEXT_NODE
) {
460 $lastTextNode = null;
464 if ($lastTextNode === null) {
465 $lastTextNode = $childNode;
467 // merge with last text node
468 $newTextNode = $childNode
470 ->createTextNode($lastTextNode->textContent
. $childNode->textContent
);
471 $element->insertBefore($newTextNode, $lastTextNode);
473 $element->removeChild($lastTextNode);
474 $element->removeChild($childNode);
476 $lastTextNode = $newTextNode;
482 * Prepends a node to provided element.
484 * @param \DOMNode $node node
485 * @param \DOMElement $element target element
487 public static function prepend(\DOMNode
$node, \DOMElement
$element)
489 if ($element->firstChild
=== null) {
490 $element->appendChild($node);
492 $element->insertBefore($node, $element->firstChild
);
497 * Removes a node, optionally preserves the child nodes if `$node` is an element.
499 * @param \DOMNode $node target node
500 * @param bool $preserveChildNodes preserve child nodes, only supported for elements
502 public static function removeNode(\DOMNode
$node, $preserveChildNodes = false)
504 if ($preserveChildNodes) {
505 if (!($node instanceof \DOMElement
)) {
506 throw new \
InvalidArgumentException("Preserving child nodes is only supported for DOMElement.");
509 while ($node->hasChildNodes()) {
510 self
::insertBefore($node->childNodes
->item(0), $node);
514 self
::getParentNode($node)->removeChild($node);
518 * Replaces a DOM element with another, preserving all child nodes by default.
520 * @param \DOMElement $oldElement old element
521 * @param \DOMElement $newElement new element
522 * @param bool $preserveChildNodes true if child nodes should be moved, otherwise they'll be implicitly removed
524 public static function replaceElement(\DOMElement
$oldElement, \DOMElement
$newElement, $preserveChildNodes = true)
526 self
::insertBefore($newElement, $oldElement);
528 // move all child nodes
529 if ($preserveChildNodes) {
530 while ($oldElement->hasChildNodes()) {
531 $newElement->appendChild($oldElement->childNodes
->item(0));
535 // remove old element
536 self
::getParentNode($oldElement)->removeChild($oldElement);
540 * Splits all parent nodes until `$ancestor` and moved other nodes after/before
541 * (determined by `$splitBefore`) into the newly created nodes. This allows
542 * extraction of DOM parts while preserving nesting for both the extracted nodes
543 * and the remaining siblings.
545 * @param \DOMNode $node reference node
546 * @param \DOMElement $ancestor ancestor element that should not be split
547 * @param bool $splitBefore true if nodes before `$node` should be moved into a new node, false to split nodes after `$node`
548 * @return \DOMNode parent node containing `$node`, direct child of `$ancestor`
550 public static function splitParentsUntil(\DOMNode
$node, \DOMElement
$ancestor, $splitBefore = true)
552 if (!self
::contains($ancestor, $node)) {
553 throw new \
InvalidArgumentException("Node is not contained in ancestor node.");
556 // clone the parent node right "below" `$ancestor`
557 $cloneNode = self
::getParentBefore($node, $ancestor);
560 if ($cloneNode === null) {
561 // target node is already a direct descendant of the ancestor
562 // node, no need to split anything
564 } elseif (self
::isFirstNode($node, $cloneNode)) {
565 // target node is at the very start, we can safely move the
566 // entire parent node around
570 $currentNode = $node;
571 while (($parent = $currentNode->parentNode
) !== $ancestor) {
572 /** @var \DOMElement $newNode */
573 $newNode = $parent->cloneNode();
574 self
::insertBefore($newNode, $parent);
576 while ($currentNode->previousSibling
) {
577 self
::prepend($currentNode->previousSibling
, $newNode);
580 $currentNode = $parent;
583 if ($cloneNode === null) {
584 // target node is already a direct descendant of the ancestor
585 // node, no need to split anything
587 } elseif (self
::isLastNode($node, $cloneNode)) {
588 // target node is at the very end, we can safely move the
589 // entire parent node around
593 $currentNode = $node;
594 while (($parent = $currentNode->parentNode
) !== $ancestor) {
595 $newNode = $parent->cloneNode();
596 self
::insertAfter($newNode, $parent);
598 while ($currentNode->nextSibling
) {
599 $newNode->appendChild($currentNode->nextSibling
);
602 $currentNode = $parent;
606 return self
::getParentBefore($node, $ancestor);
610 * Forbid creation of DOMUtil objects.
612 private function __construct()