+
+ /**
+ * Replace the given node with all those in $replacements
+ * @param DOMNode[] $replacements
+ */
+ protected function replaceNodeWithNodes(DOMNode $toReplace, array $replacements): void
+ {
+ /** @var DOMDocument $targetDoc */
+ $targetDoc = $toReplace->ownerDocument;
+
+ foreach ($replacements as $replacement) {
+ if ($replacement->ownerDocument !== $targetDoc) {
+ $replacement = $targetDoc->importNode($replacement, true);
+ }
+
+ $toReplace->parentNode->insertBefore($replacement, $toReplace);
+ }
+
+ $toReplace->parentNode->removeChild($toReplace);
+ }
+
+ /**
+ * Move a tag node to become a sibling of the given parent.
+ * Will attempt to guess a position based upon the tag content within the parent.
+ */
+ protected function moveTagNodeToBesideParent(PageIncludeTag $tag, DOMNode $parent): void
+ {
+ $parentText = $parent->textContent;
+ $tagPos = strpos($parentText, $tag->tagContent);
+ $before = $tagPos < (strlen($parentText) / 2);
+ $this->toCleanup[] = $tag->domNode->parentNode;
+
+ if ($before) {
+ $parent->parentNode->insertBefore($tag->domNode, $parent);
+ } else {
+ $parent->parentNode->insertBefore($tag->domNode, $parent->nextSibling);
+ }
+ }
+
+ /**
+ * Splits the given $parentNode at the location of the $domNode within it.
+ * Attempts replicate the original $parentNode, moving some of their parent
+ * children in where needed, before adding the $domNode between.
+ */
+ protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void
+ {
+ $children = [...$parentNode->childNodes];
+ $splitPos = array_search($domNode, $children, true);
+ if ($splitPos === false) {
+ $splitPos = count($children) - 1;
+ }
+
+ $parentClone = $parentNode->cloneNode();
+ $parentNode->parentNode->insertBefore($parentClone, $parentNode);
+ $parentClone->removeAttribute('id');
+
+ for ($i = 0; $i < $splitPos; $i++) {
+ /** @var DOMNode $child */
+ $child = $children[$i];
+ $parentClone->appendChild($child);
+ }
+
+ $parentNode->parentNode->insertBefore($domNode, $parentNode);
+
+ $this->toCleanup[] = $parentNode;
+ $this->toCleanup[] = $parentClone;
+ }
+
+ /**
+ * Get the parent paragraph of the given node, if existing.
+ */
+ protected function getParentParagraph(DOMNode $parent): ?DOMNode
+ {
+ do {
+ if (strtolower($parent->nodeName) === 'p') {
+ return $parent;
+ }
+
+ $parent = $parent->parentNode;
+ } while ($parent !== null);
+
+ return null;
+ }
+
+ /**
+ * Cleanup after a parse operation.
+ * Removes stranded elements we may have left during the parse.
+ */
+ protected function cleanup(): void
+ {
+ foreach ($this->toCleanup as $element) {
+ $element->normalize();
+ while ($element->parentNode && !$element->hasChildNodes()) {
+ $parent = $element->parentNode;
+ $parent->removeChild($element);
+ $element = $parent;
+ }
+ }
+ }