Modern DOM library written in PHP for HTML documents
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

779 lines
33 KiB

<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\Inner\{
Document as InnerDocument,
Reflection
};
use MensBeam\HTML\Parser,
Symfony\Component\CssSelector\CssSelectorConverter,
Symfony\Component\CssSelector\Exception\SyntaxErrorException as SymfonySyntaxErrorException;
/** @property \DOMElement $_innerNode */
class Element extends Node {
use ChildNode, DocumentOrElement, NonDocumentTypeChildNode, ParentNode;
protected function __get_attributes(): NamedNodeMap {
// NamedNodeMaps cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\NamedNodeMap', $this, ($this instanceof Document) ? $this->_innerNode : $this->_innerNode->ownerDocument, $this->_innerNode->attributes);
}
protected function __get_classList(): DOMTokenList {
# The classList getter steps are to return a DOMTokenList object whose
# associated element is this and whose associated attribute’s local name is
# class. The token set of this particular DOMTokenList object are also known as
# the element’s classes.
// DOMTokenLists cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\DOMTokenList', $this, 'class');
}
protected function __get_className(): string {
# The className attribute must reflect the "class" content attribute.
# Return the result of running get an attribute value given this and name.
return $this->getAttribute('class') ?? '';
}
protected function __set_className(string $value): void {
# The className attribute must reflect the "class" content attribute.
# Set an attribute value for this using name and the given value.
$this->setAttribute('class', $value);
}
protected function __get_id(): string {
# The id attribute must reflect the "id" content attribute.
# Return the result of running get an attribute value given this and name.
return $this->getAttribute('id') ?? '';
}
protected function __set_id(string $value): void {
# The id attribute must reflect the "id" content attribute.
# Set an attribute value for this using name and the given value.
$this->setAttribute('id', $value);
}
protected function __get_innerHTML(): string {
# On getting, return the result of invoking the fragment serializing algorithm
# on the context object providing true for the require well-formed flag (this
# might throw an exception instead of returning a string).
// TODO: If adding better XML support this should require XML well-formed
// serialization here.
return Serializer::serializeInner($this->_innerNode);
}
protected function __set_innerHTML(string $value): void {
# On setting, these steps must be run:
# 1. Let context element be the context object's host if the context object is a
# ShadowRoot object, or the context object otherwise.
// There's no scripting in this implementation and therefore no shadow root
// object.
$context = $this;
$innerContext = $this->_innerNode;
/** @var InnerDocument */
$innerOwnerDocument = $innerContext->ownerDocument;
# 2. Let fragment be the result of invoking the fragment parsing algorithm with
# the new value as markup, and with context element.
$innerFragment = Parser::parseFragment($innerContext, Parser::NO_QUIRKS_MODE, $value, 'UTF-8');
$fragment = $innerOwnerDocument->getWrapperNode($innerFragment);
$this->postParsingTemplatesFix($innerFragment);
# 3. If the context object is a template element, then let context object be the
# template's template contents (a DocumentFragment).
# NOTE: Setting innerHTML on a template element will replace all the nodes in
# its template contents (template.content) rather than its children.
if ($this instanceof HTMLTemplateElement) {
$context = $this->content;
$innerContext = $context->innerNode;
}
# 4. Replace all with fragment within the context object.
while ($innerContext->hasChildNodes()) {
$innerContext->removeChild($innerContext->firstChild);
}
if (!$this instanceof HTMLTemplateElement) {
$context->appendChild($fragment);
} else {
while ($innerFragment->hasChildNodes()) {
$context->appendChild($fragment->firstChild);
}
}
}
protected function __get_localName(): ?string {
// PHP's DOM does this correctly already.
return $this->_innerNode->localName;
}
protected function __get_namespaceURI(): ?string {
// PHP's DOM uses null incorrectly for the HTML namespace, and if you attempt to
// use the HTML namespace anyway it has additional bugs we don't have to work
// around because of the wrapper classes; So, use the null namespace internally
// but print out the HTML namespace instead.
$namespace = $this->_innerNode->namespaceURI;
return (!$this->ownerDocument instanceof XMLDocument && $namespace === null) ? self::HTML_NAMESPACE : $namespace;
}
protected function __get_outerHTML(): string {
# On getting, return the result of invoking the fragment serializing algorithm
# on a fictional node whose only child is the context object providing true
# for the require well-formed flag (this might throw an exception instead of
# returning a string).
// TODO: If adding better XML support this should require XML well-formed
// serialization here.
return Serializer::serialize($this->_innerNode);
}
protected function __set_outerHTML(string $value): void {
# On setting, the following steps must be run:
#
# 1. Let parent be the context object's parent.
$parent = $this->parentNode;
$innerParent = $this->_innerNode->parentNode;
/** @var InnerDocument */
$innerOwnerDocument = $this->_innerNode->ownerDocument;
# 2. If parent is null, terminate these steps. There would be no way to obtain a
# reference to the nodes created even if the remaining steps were run.
if ($innerParent === null) {
return;
}
# 3. If parent is a Document, throw a "NoModificationAllowedError" DOMException.
if ($innerParent instanceof \DOMDocument) {
throw new NoModificationAllowedError();
}
# 4. If parent is a DocumentFragment, let parent be a new Element with:
# • body as its local name,
# • The HTML namespace as its namespace, and
# • The context object's node document as its node document.
if ($innerParent instanceof \DOMDocumentFragment) {
$innerParent = $innerOwnerDocument->createElement('body');
}
# 5. Let fragment be the result of invoking the fragment parsing algorithm with
# the new value as markup, and parent as the context element.
$innerFragment = Parser::parseFragment($innerParent, Parser::NO_QUIRKS_MODE, $value, 'UTF-8');
$fragment = $innerOwnerDocument->getWrapperNode($innerFragment);
$this->postParsingTemplatesFix($innerFragment);
# 6. Replace the context object with fragment within the context object's
# parent.
$parent->replaceChild($fragment, $this);
}
protected function __get_prefix(): ?string {
$prefix = $this->_innerNode->prefix;
return ($prefix !== '') ? $prefix : null;
}
protected function __get_tagName(): string {
return ($this->prefix === null) ? $this->localName : "{$this->prefix}:{$this->localName}";
}
protected function __construct(\DOMElement $element) {
parent::__construct($element);
}
public function closest(string $selectors): ?Element {
# The closest(selectors) method steps are:
# 1. Let s be the result of parse a selector from selectors. [SELECTORS4]
try {
$converter = new CssSelectorConverter();
$s = $converter->toXPath($selectors);
} catch (\Exception $e) {
# 2. If s is failure, throw a "SyntaxError" DOMException.
if ($e instanceof SymfonySyntaxErrorException) {
throw new SyntaxError();
}
}
# 3. Let elements be this’s inclusive ancestors that are elements, in reverse
# tree order.
# 4. For each element in elements, if match a selector against an element,
# using s, element, and :scope element this, returns success, return element.
# [SELECTORS4]
// Ideally we'd be able to wrap the XPath query in `ancestor-or-self::*[]`, but
// the queries generated by Symfony's library aren't conducive to that kind of
// query, so we must instead manually loop instead of having XPath do it for
// us...
$innerNode = $this->_innerNode;
$doc = $this->getInnerDocument();
$xpath = $doc->xpath;
$n = $innerNode;
do {
if (!$n instanceof \DOMElement) {
break;
}
$result = $xpath->query($s, $n->parentNode);
// If there is a match iterate through the results. If $this is one of them
// return that, otherwise return the first item.
if ($result->length > 0) {
foreach ($result as $r) {
if ($r === $innerNode) {
return $this;
}
}
return $doc->getWrapperNode($result->item(0));
}
} while ($n = $n->parentNode);
# 5. Return null.
return null;
}
public function getAttribute(string $qualifiedName): ?string {
# The getAttribute(qualifiedName) method steps are:
#
# 1. Let attr be the result of getting an attribute given qualifiedName and this.
$attr = $this->getAttributeNode($qualifiedName);
# 2. If attr is null, return null.
if ($attr === null) {
return null;
}
# 3. Return attr’s value.
// Uncoerce the value if necessary
return $attr->value;
}
public function getAttributeNames(): array {
# The getAttributeNames() method steps are to return the qualified names of the
# attributes in this’s attribute list, in order; otherwise a new list.
$list = [];
$attributes = $this->_innerNode->attributes;
if ($attributes !== null) {
foreach ($attributes as $attr) {
$list[] = $attr->nodeName;
}
}
return $list;
}
public function getAttributeNode(string $qualifiedName): ?Attr {
# The getAttributeNode(qualifiedName) method steps are to return the result of
# getting an attribute given qualifiedName and this.
#
# To get an attribute by name given a qualifiedName and element element, run
# these steps:
#
# 1. If element is in the HTML namespace and its node document is an HTML document,
# then set qualifiedName to qualifiedName in ASCII lowercase.
if (!$this instanceof XMLDocument && $this->namespaceURI === self::HTML_NAMESPACE) {
$qualifiedName = strtolower($qualifiedName);
}
$qualifiedName = $this->coerceName($qualifiedName);
/** @var InnerDocument */
$innerOwnerDocument = $this->_innerNode->ownerDocument;
# 2. Return the first attribute in element’s attribute list whose qualified name is
# qualifiedName; otherwise null.
// Manually going through the attributes because PHP DOM has issues, returning
// odd "DOMNamespaceNode" objects sometimes instead of the actual attributes...
// yeah...
$attributes = $this->_innerNode->attributes;
foreach ($attributes as $attr) {
if ($attr->nodeName === $qualifiedName) {
return $innerOwnerDocument->getWrapperNode($attr);
}
}
return null;
}
public function getAttributeNodeNS(?string $namespace, string $localName): ?Attr {
static $count = 0;
$count++;
# The getAttributeNodeNS(namespace, localName) method steps are to return the
# result of getting an attribute given namespace, localName, and this.
#
# To get an attribute by namespace and local name given a namespace, localName,
# and element element, run these steps:
#
# 1. If namespace is the empty string, then set it to null.
if ($namespace === '') {
$namespace = null;
}
$localName = $this->coerceName($localName);
/** @var InnerDocument */
$innerOwnerDocument = $this->_innerNode->ownerDocument;
# 2. Return the attribute in element’s attribute list whose namespace is namespace
# and local name is localName, if any; otherwise null.
// Manually going through the attributes because PHP DOM has issues, returning
// odd "DOMNamespaceNode" objects sometimes instead of the actual attributes...
// yeah...
$attributes = $this->_innerNode->attributes;
foreach ($attributes as $attr) {
if ($attr->namespaceURI === $namespace && $attr->localName === $localName) {
return $innerOwnerDocument->getWrapperNode($attr);
}
}
return null;
}
public function getAttributeNS(?string $namespace, string $localName): ?string {
# The getAttributeNS(namespace, localName) method steps are:
#
# 1. Let attr be the result of getting an attribute given namespace, localName,
# and this.
$attr = $this->getAttributeNodeNS($namespace, $localName);
# 2. If attr is null, return null.
if ($attr === null) {
return null;
}
# 3. Return attr’s value.
// Uncoerce the value if necessary
return $attr->value;
}
public function hasAttribute(string $qualifiedName): bool {
# The hasAttribute(qualifiedName) method steps are:
#
# 1. If this is in the HTML namespace and its node document is an HTML document,
# then set qualifiedName to qualifiedName in ASCII lowercase.
if (!$this->ownerDocument instanceof XMLDocument && $this->namespaceURI === self::HTML_NAMESPACE) {
$qualifiedName = strtolower($qualifiedName);
}
# 2. Return true if this has an attribute whose qualified name is qualifiedName;
# otherwise false.
# An element has an attribute A if its attribute list contains A.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster.
$value = $this->_innerNode->hasAttribute($qualifiedName);
if (!$value) {
// PHP DOM does not acknowledge the presence of XMLNS-namespace attributes,
// so try it again just in case; getAttributeNode will coerce names if
// necessary, too.
$value = ($this->getAttributeNode($qualifiedName) !== null);
}
return $value;
}
public function hasAttributeNS(?string $namespace, string $localName): bool {
# The hasAttributeNS(namespace, localName) method steps are:
#
# 1. If namespace is the empty string, then set it to null.
if ($namespace === '') {
$namespace = null;
}
# 2. Return true if this has an attribute whose namespace is namespace and local
# name is localName; otherwise false.
# An element has an attribute A if its attribute list contains A.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster.
$value = $this->_innerNode->hasAttributeNS($namespace, $localName);
if (!$value) {
// PHP DOM does not acknowledge the presence of XMLNS-namespace attributes,
// so try it again just in case; getAttributeNode will coerce names if
// necessary, too.
$value = ($this->getAttributeNodeNS($namespace, $localName) !== null);
}
return $value;
}
public function hasAttributes(): bool {
# The hasAttributes() method steps are to return false if this’s attribute list
# is empty; otherwise true.
// PHP's DOM does this correctly already.
return $this->_innerNode->hasAttributes();
}
public function insertAdjacentElement(string $where, Element $element): ?Element {
# The insertAdjacentElement(where, element) method steps are to return the
# result of running insert adjacent, give this, where, and element.
return $this->insertAdjacent($this, $where, $element);
}
public function insertAdjacentHTML(string $where, string $data): void {
# The insertAdjacentHTML(position, text) method must run these steps:
// This is from the W3C specification which the WHATWG delegates to concerning
// innerHTML, outerHTML and this one. Using $where and $data for the parameters
// to keep things consistent with the WHATWG specification.
# 1. Use the first matching item from this list:
$where = strtolower($where);
switch ($where) {
case 'beforebegin':
case 'afterend':
# Let context be the context object's parent.
$context = $this->parentNode;
$innerContext = $this->_innerNode->parentNode;
# If context is null or a Document, throw a "NoModificationAllowedError" DOMException.
if ($context === null || $context instanceof Document) {
throw new NoModificationAllowedError();
}
break;
case 'afterbegin':
case 'beforeend':
# Let context be the context object.
$context = $this;
$innerContext = $this->_innerNode;
break;
default: throw new SyntaxError();
}
# 2. If context is not an Element or the following are all true:
# • context's node document is an HTML document,
# • context's local name is "html", and
# • context's namespace is the HTML namespace;
if (!$context instanceof Element || ($context->ownerDocument instanceof Document && $context->localName === 'html' && $context->namespaceURI === Node::HTML_NAMESPACE)) {
# let context be a new Element with
# • body as its local name,
# • The HTML namespace as its namespace, and
# • The context object's node document as its node document.
$context = $context->ownerDocument->createElement('body');
$innerContext = $context->innerNode;
}
# 3. Let fragment be the result of invoking the fragment parsing algorithm with
# text as markup, and context as the context element.
$innerFragment = Parser::parseFragment($innerContext, Parser::NO_QUIRKS_MODE, $data, 'UTF-8');
$fragment = $innerContext->ownerDocument->getWrapperNode($innerFragment);
# 4. Use the first matching item from this list:
switch ($where) {
case 'beforebegin':
# Insert fragment into the context object's parent before the context object.
$this->parentNode->insertBefore($fragment, $this);
break;
case 'afterbegin':
# Insert fragment into the context object before its first child.
$this->insertBefore($fragment, $this->firstChild);
break;
case 'beforeend':
# Append fragment to the context object.
$this->appendChild($fragment);
break;
case 'afterend':
# Insert fragment into the context object's parent before the context object's
# next sibling.
$this->parentNode->insertBefore($fragment, $this->nextSibling);
break;
}
}
public function insertAdjacentText(string $where, string $data): void {
# The insertAdjacentText(where, data) method steps are:
#
# 1. Let text be a new Text node whose data is data and node document is this’s
# node document.
$text = $this->ownerDocument->createTextNode($data);
# 2. Run insert adjacent, given this, where, and text.
$this->insertAdjacent($this, $where, $text);
}
public function matches(string $selectors): bool {
# The matches(selectors) and webkitMatchesSelector(selectors) method steps are:
# 1. Let s be the result of parse a selector from selectors. [SELECTORS4]
try {
$converter = new CssSelectorConverter();
$s = $converter->toXPath($selectors);
} catch (\Exception $e) {
# 2. If s is failure, throw a "SyntaxError" DOMException.
if ($e instanceof SymfonySyntaxErrorException) {
throw new SyntaxError();
}
}
# 3. If the result of match a selector against an element, using s, this, and
# :scope element this, returns success, then return true; otherwise, return
# false. [SELECTORS4]
$innerNode = $this->_innerNode;
/** @var InnerDocument */
$innerOwnerDocument = $innerNode->ownerDocument;
// Query the parent as the context node, yes. This is due to how the XPath
// queries are generated from Symfony.
return ($innerOwnerDocument->xpath->query($s, $innerNode->parentNode)->item(0) === $innerNode);
}
public function removeAttribute(string $qualifiedName): void {
# The removeAttribute(qualifiedName) method steps are to remove an attribute
# given qualifiedName and this, and then return undefined.
#
# To remove an attribute by name given a qualifiedName and element element, run
# these steps:
# 1. Let attr be the result of getting an attribute given qualifiedName and
# element.
# 2. If attr is non-null, then remove attr.
# 3. Return attr.
// Going to let PHP's DOM do the heavy lifting here instead
$this->_innerNode->removeAttribute($this->coerceName($qualifiedName));
}
public function removeAttributeNode(Attr $attr): Attr {
# The removeAttributeNode(attr) method steps are:
# 1. If this’s attribute list does not contain attr, then throw a
# "NotFoundError" DOMException.
// PHP's DOM does this already. Will catch its exceptions and rethrow as HTML-DOM
// DOMException.
# 2. Remove attr.
try {
$this->_innerNode->removeAttributeNode($attr->innerNode);
} catch (\DOMException $e) {
switch ($e->getCode()) {
case 8: throw new NotFoundError();
break;
case 9: throw new NotSupportedError();
break;
default: throw new UnknownException();
}
}
# 3. Return attr.
return $attr;
}
public function removeAttributeNS(?string $namespace, string $localName): void {
# The removeAttributeNS(namespace, localName) method steps are to remove an
# attribute given namespace, localName, and this, and then return undefined.
#
# To remove an attribute by namespace and local name given a namespace,
# localName, and element element, run these steps:
# 1. Let attr be the result of getting an attribute given namespace, localName,
# and element.
# 2. If attr is non-null, then remove attr.
# 3. Return attr.
// Going to let PHP's DOM do the heavy lifting here instead
$this->_innerNode->removeAttributeNS($namespace, $this->coerceName($localName));
}
public function setAttribute(string $qualifiedName, string $value): void {
# 1. If qualifiedName does not match the Name production in XML, then throw an
# "InvalidCharacterError" DOMException.
if (!preg_match(InnerDocument::NAME_PRODUCTION_REGEX, $qualifiedName)) {
throw new InvalidCharacterError();
}
# 2. If this is in the HTML namespace and its node document is an HTML document,
# then set qualifiedName to qualifiedName in ASCII lowercase.
if (!$this->ownerDocument instanceof XMLDocument && $this->namespaceURI === Node::HTML_NAMESPACE) {
$qualifiedName = strtolower($qualifiedName);
}
# 3. Let attribute be the first attribute in this’s attribute list whose
# qualified name is qualifiedName, and null otherwise.
# 4. If attribute is null, create an attribute whose local name is qualifiedName,
# value is value, and node document is this’s node document, then append this
# attribute to this, and then return.
# 5. Change attribute to value.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster.
try {
$this->_innerNode->setAttributeNS(null, $qualifiedName, $value);
} catch (\DOMException $e) {
// The attribute name is invalid for XML
// Replace any offending characters with "UHHHHHH" where H are the uppercase
// hexadecimal digits of the character's code point
$this->_innerNode->setAttributeNS(null, $this->coerceName($qualifiedName), $value);
}
// If you create an id attribute it won't be used by PHP in getElementById, so
// let's fix that.
if ($qualifiedName === 'id') {
$this->_innerNode->setIdAttribute($qualifiedName, true);
}
}
public function setAttributeNode(Attr $attr): ?Attr {
# The setAttributeNode(attr) and setAttributeNodeNS(attr) methods steps are to
# return the result of setting an attribute given attr and this.
#
# To set an attribute given an attr and element, run these steps:
#
# 1. If attr’s element is neither null nor element, throw an
# "InUseAttributeError" DOMException.
$ownerElement = $attr->ownerElement;
if ($ownerElement !== null && $ownerElement !== $this) {
throw new InUseAttributeError();
}
// PHP's DOM doesn't do this method correctly. It returns the old node if it is
// replaced and null otherwise. It's always supposed to return a node.
$oldAttr = $this->getAttributeNodeNS($attr->namespaceURI, $attr->localName);
# 3. If oldAttr is attr, return attr.
if ($oldAttr === $attr) {
return $attr;
}
# 4. If oldAttr is non-null, then replace oldAttr with attr.
if ($oldAttr !== null) {
$this->_innerNode->setAttributeNode($attr->innerNode);
return $attr;
}
# 5. Otherwise, append attr to element.
else {
$this->_innerNode->appendChild($attr->innerNode);
return $attr;
}
# 6. Return oldAttr.
return $oldAttr;
}
public function setAttributeNodeNS(Attr $attr): ?Attr {
return $this->setAttributeNode($attr);
}
public function setAttributeNS(?string $namespace, string $qualifiedName, string $value): void {
# 1. Let namespace, prefix, and localName be the result of passing namespace and
# qualifiedName to validate and extract.
[ 'namespace' => $namespace, 'prefix' => $prefix, 'localName' => $localName ] = $this->validateAndExtract($qualifiedName, $namespace);
$prefix = ($prefix === null) ? null : $this->coerceName($prefix);
$localName = $this->coerceName($localName);
$qualifiedName = ($prefix === null || $prefix === '') ? $localName : "{$prefix}:{$localName}";
# 2. Set an attribute value for this using localName, value, and also prefix and
# namespace.
// NOTE: We create attribute nodes so that xmlns attributes don't get lost;
// otherwise they cannot be serialized
if ($namespace === self::XMLNS_NAMESPACE) {
$attr = $this->ownerDocument->createAttributeNS($namespace, $qualifiedName);
$attr->value = $this->escapeString($value, true);
$this->setAttributeNodeNS($attr);
} else {
$this->_innerNode->setAttributeNS($namespace, $qualifiedName, $value);
}
// If you create an id attribute this way it won't be used by PHP in
// getElementById, so let's fix that.
if ($namespace === null && $qualifiedName === 'id') {
$this->_innerNode->setIdAttribute($qualifiedName, true);
}
}
public function webkitMatchesSelector(string $selectors): bool {
return $this->matches($selectors);
}
protected function getRenderedTextFragment(string $input): \DOMDocumentFragment {
# The rendered text fragment for a string input given a Document document is the
# result of running the following steps:
$innerNode = $this->_innerNode;
$fragment = $innerNode->ownerDocument->createDocumentFragment();
# 1. Let position be a position variable for input, initially pointing at the
# start of input.
$position = 0;
# 2. Let text be the empty string.
$text = '';
# 3. While position is not past the end of input:
$strlen = strlen($input);
while ($position < $strlen) {
# 1. Collect a sequence of code points that are not U+000A LF or U+000D CR from input
# given position, and set text to the result.
while (isset($input[$position]) && !in_array($input[$position], [ "\n", "\r" ])) {
$text .= $input[$position];
$position++;
}
# 2. If text is not the empty string, then append a new Text node whose data is
# text and node document is document to fragment.
if ($text !== '') {
$fragment->appendChild($innerNode->ownerDocument->createTextNode($text));
}
# 3. While position is not past the end of input, and the code point at position
# is either U+000A LF or U+000D CR:
while ($position < $strlen && in_array($input[$position], [ "\n", "\r" ])) {
# 1. If the code point at position is U+000D CR and the next code point is
# U+000A LF, then advance position to the next code point in input.
if ($input[$position] === "\r" && isset($input[$position + 1]) && $input[$position + 1] === "\n") {
$position++;
}
# 2. Advance position to the next code point in input.
$position++;
# 3. Append the result of creating an element given document, br, and the HTML
# namespace to fragment.
$fragment->appendChild($innerNode->ownerDocument->createElement('br'));
}
}
return $fragment;
}
protected function insertAdjacent(Element $element, string $where, Node $node): ?Node {
# To insert adjacent, given an element element, string where, and a node node,
# run the steps associated with the first ASCII case-insensitive match for
# where:
switch (strtolower($where)) {
case 'beforebegin':
# If element’s parent is null, return null.
if ($element->parentNode === null) {
return null;
}
# Return the result of pre-inserting node into element’s parent before element.
return $element->parentNode->insertBefore($node, $element);
break;
case 'afterbegin':
# Return the result of pre-inserting node into element before element’s first
# child.
return $element->insertBefore($node, $element->firstChild);
break;
case 'beforeend':
# Return the result of pre-inserting node into element before null.
// Isn't this just an appendChild?
return $element->appendChild($node);
break;
case 'afterend':
# If element’s parent is null, return null.
if ($element->parentNode === null) {
return null;
}
# Return the result of pre-inserting node into element’s parent before element’s
# next sibling.
return $element->parentNode->insertBefore($node, $element->nextSibling);
break;
default: throw new SyntaxError();
}
}
}