diff --git a/lib/DOMException.php b/lib/DOMException.php
index b0211ed..0382065 100644
--- a/lib/DOMException.php
+++ b/lib/DOMException.php
@@ -11,6 +11,10 @@ use MensBeam\Framework\Exception;
class DOMException extends Exception {
+ // DEVIATION (kinda): The specification states clearly that using the old
+ // integer values for exceptions has been deprecated, but all the browsers
+ // continue to honor them even if they're not explicitly used. Besides, it's
+ // bonkers to create numerous exception classes for each error type.
public const INDEX_SIZE_ERROR = 1;
public const HIERARCHY_REQUEST_ERROR = 3;
public const WRONG_DOCUMENT = 4;
diff --git a/lib/DOMTokenList.php b/lib/DOMTokenList.php
index 778622f..b159cc7 100644
--- a/lib/DOMTokenList.php
+++ b/lib/DOMTokenList.php
@@ -56,9 +56,7 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator {
# When a DOMTokenList object is created, then:
#
# 1. Let element be associated element.
- // Using a weak reference here to prevent a circular reference. We should be
- // creating a weak reference to the inner element, but there are memory
- // management issues concerning PHP's garbage reference counting.
+ // Using a weak reference here to prevent a circular reference.
$this->element = \WeakReference::create($element);
# 2. Let localName be associated attribute’s local name.
$this->localName = $attributeLocalName;
diff --git a/lib/Document.php b/lib/Document.php
index 7a61921..62ca9fa 100644
--- a/lib/Document.php
+++ b/lib/Document.php
@@ -68,6 +68,40 @@ class Document extends Node implements \ArrayAccess {
return $this->_contentType;
}
+ protected function __get_dir(): string {
+ # The dir IDL attribute on Document objects must reflect the dir content attribute
+ # of the html element, if any, limited to only known values. If there is no such
+ # element, then the attribute must return the empty string and do nothing on
+ # setting.
+
+ # If a reflecting IDL attribute is a DOMString attribute whose content attribute
+ # is an enumerated attribute, and the IDL attribute is limited to only known
+ # values, then, on getting, the IDL attribute must return the keyword value
+ # associated with the state the attribute is in, if any, or the empty string if
+ # the attribute is in a state that has no associated keyword value or if the
+ # attribute is not in a defined state (e.g. the attribute is missing and there
+ # is no missing value default). If there are multiple keyword values for the
+ # state, then return the conforming one. If there are multiple conforming
+ # keyword values, then one will be designated the canonical keyword; choose that
+ # one.
+
+ $documentElement = $this->documentElement;
+ return ($documentElement !== null && $documentElement->namespaceURI === Node::HTML_NAMESPACE && $documentElement->tagName === 'html') ? $documentElement->dir : '';
+ }
+
+ protected function __set_dir(string $value): void {
+ # The dir IDL attribute on Document objects must reflect the dir content attribute
+ # of the html element, if any, limited to only known values. If there is no such
+ # element, then the attribute must return the empty string and do nothing on
+ # setting.
+
+ # On setting, the content attribute must be set to the specified new value.
+ $documentElement = $this->documentElement;
+ if ($documentElement !== null && $documentElement->namespaceURI === Node::HTML_NAMESPACE && $documentElement->tagName === 'html') {
+ $documentElement->dir = $value;
+ }
+ }
+
protected function __get_doctype(): ?DocumentType {
// PHP's DOM does this correctly already.
$doctype = $this->innerNode->doctype;
diff --git a/lib/Element.php b/lib/Element.php
index e369941..09670ca 100644
--- a/lib/Element.php
+++ b/lib/Element.php
@@ -106,33 +106,6 @@ class Element extends Node {
}
}
- protected function __get_innerText(): ?string {
- # The innerText and outerText getter steps are:
- # 1. If this is not being rendered or if the user agent is a non-CSS user agent,
- # then return this's descendant text content.
- // This is a non-CSS user agent. Nothing else to do here.
- return $this->__get_textContent();
- }
-
- protected function __set_innerText(string $value): void {
- # The innerText setter steps are:
- # 1. Let fragment be the rendered text fragment for the given value given this's node
- # document.
- $fragment = $this->getRenderedTextFragment($value);
-
- # 2. Replace all with fragment within this.
- $innerNode = $this->innerNode;
- $children = $innerNode->childNodes;
- while ($innerNode->hasChildNodes()) {
- $innerNode->removeChild($innerNode->firstChild);
- }
-
- // Check for child nodes before appending to prevent a stupid warning.
- if ($fragment->hasChildNodes()) {
- $innerNode->appendChild($fragment);
- }
- }
-
protected function __get_localName(): ?string {
// PHP's DOM does this correctly already.
return $this->innerNode->localName;
@@ -194,77 +167,6 @@ class Element extends Node {
$parent->replaceChild($fragment, $this);
}
- protected function __get_outerText(): ?string {
- # The innerText and outerText getter steps are:
- # 1. If this is not being rendered or if the user agent is a non-CSS user agent,
- # then return this's descendant text content.
- // This is a non-CSS user agent. Nothing else to do here.
- return $this->__get_textContent();
- }
-
- protected function __set_outerText(string $value): void {
- # The outerText setter steps are:
- # 1. If this's parent is null, then throw a "NoModificationAllowedError"
- # DOMException.
- $innerNode = $this->innerNode;
- if ($this->parentNode === null) {
- throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED);
- }
-
- # 2. Let next be this's next sibling.
- $next = $innerNode->nextSibling;
-
- # 3. Let previous be this's previous sibling.
- $previous = $innerNode->previousSibling;
-
- # 4. Let fragment be the rendered text fragment for the given value given this's node
- # document.
- $fragment = $this->getRenderedTextFragment($value);
-
- # 5. Replace this with fragment within this's parent.
- // Check for child nodes before appending to prevent a stupid warning.
- if ($fragment->hasChildNodes()) {
- $innerNode->parentNode->replaceChild($fragment, $innerNode);
- } else {
- $innerNode->parentNode->removeChild($innerNode);
- }
-
- # 6. If next is non-null and next's previous sibling is a Text node, then merge
- # with the next text node given next's previous sibling.
- if ($next !== null && $next->previousSibling instanceof \DOMText) {
- # To merge with the next text node given a Text node node:
- # 1. Let next be node's next sibling.
- # 2. If next is not a Text node, then return.
- // Already checked for
-
- # 3. Replace data with node, node's data's length, 0, and next's data.
- $next->previousSibling->data .= $next->data;
-
- # 4. If next's parent is non-null, then remove next.
- // DEVIATION: There are no mutation events in this implementation, so there's no
- // reason to check for a parent here.
- $next->parentNode->removeChild($next);
- }
-
- # 7. If previous is a Text node, then merge with the next text node given previous.
- if ($previous instanceof \DOMText) {
- # To merge with the next text node given a Text node node:
- # 1. Let next be node's next sibling.
- $next = $previous->nextSibling;
-
- # 2. If next is not a Text node, then return.
- if ($next instanceof \DOMText) {
- # 3. Replace data with node, node's data's length, 0, and next's data.
- $previous->data .= $next->data;
-
- # 4. If next's parent is non-null, then remove next.
- // DEVIATION: There are no mutation events in this implementation, so there's no
- // reason to check for a parent here.
- $next->parentNode->removeChild($next);
- }
- }
- }
-
protected function __get_prefix(): ?string {
$prefix = $this->innerNode->prefix;
return ($prefix !== '') ? $prefix : null;
diff --git a/lib/HTMLElement.php b/lib/HTMLElement.php
index 672fed2..1dafd05 100644
--- a/lib/HTMLElement.php
+++ b/lib/HTMLElement.php
@@ -9,4 +9,505 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM;
-class HTMLElement extends Element {}
\ No newline at end of file
+class HTMLElement extends Element {
+ use HTMLOrSVGElement;
+
+
+ protected function __get_accessKey(): string {
+ # The accessKey IDL attribute must reflect the accesskey content attribute.
+ return $this->getAttribute('accesskey') ?? '';
+ }
+
+ protected function __set_accessKey(string $value): void {
+ $this->setAttribute('accesskey', $value);
+ }
+
+ protected function __get_autocapitalize(): string {
+ # The autocapitalize getter steps are to:
+ #
+ # 1. Let state be the own autocapitalization hint of this.
+ ## To compute the own autocapitalization hint of an element element, run the
+ ## following steps:
+ ## 1. If the autocapitalize content attribute is present on element, and its
+ ## value is not the empty string, return the state of the attribute.
+ $value = $this->getAttribute('autocapitalize');
+ if ($value !== null && $value !== '') {
+ $state = $value;
+ }
+
+ ## 2. If element is an autocapitalize-inheriting element and has a non-null form
+ ## owner, return the own autocapitalization hint of element's form owner.
+ // button fieldset input output select textarea
+ elseif (in_array($this->tagName, [ 'button', 'fieldset', 'input', 'output', 'select', 'textarea' ]))
+
+ ## 3. Return default.
+
+
+ # 2. If state is default, then return the empty string.
+
+ # 3. If state is none, then return "none".
+
+ # 4. If state is sentences, then return "sentences".
+
+ # 5. Return the keyword value corresponding to state.
+ }
+
+ protected function autoCapitalizationHint(HTMLElement $element): string {
+ # To compute the own autocapitalization hint of an element element, run the
+ # following steps:
+ # 1. If the autocapitalize content attribute is present on element, and its
+ # value is not the empty string, return the state of the attribute.
+ $value = $this->getAttribute('autocapitalize');
+ if ($value !== null && $value !== '') {
+ return $value;
+ }
+
+ # 2. If element is an autocapitalize-inheriting element and has a non-null form
+ # owner, return the own autocapitalization hint of element's form owner.
+ elseif (in_array($this->tagName, [ 'button', 'fieldset', 'input', 'output', 'select', 'textarea' ])) {
+ # A form-associated element can have a relationship with a form element, which
+ # is called the element's form owner. If a form-associated element is not
+ # associated with a form element, its form owner is said to be null.
+
+ # A form-associated element is, by default, associated with its nearest ancestor
+ # form element (as described below), but, if it is listed, may have a form
+ # attribute specified to override this.
+
+ $n = $this;
+ while ($n = $this->parentNode) {
+ if ($n instanceof HTMLElement && $n->tagName === 'form') {
+ return $this->autoCapitalizationHint($n);
+ }
+ }
+ }
+
+ ## 3. Return default.
+ // The default of this user agent is 'off'
+ return 'off';
+ }
+
+ protected function __get_contentEditable(): string {
+ # The contenteditable content attribute is an enumerated attribute whose
+ # keywords are the empty string, true, and false. The empty string and the true
+ # keyword map to the true state. The false keyword maps to the false state. In
+ # addition, there is a third state, the inherit state, which is the missing
+ # value default and the invalid value default.
+
+ # The true state indicates that the element is editable. The inherit state
+ # indicates that the element is editable if its parent is. The false state
+ # indicates that the element is not editable.
+
+ # The contentEditable IDL attribute, on getting, must return the string "true"
+ # if the content attribute is set to the true state, "false" if the content
+ # attribute is set to the false state, and "inherit" otherwise.
+
+ # On setting, if the new value is an ASCII case-insensitive match for the string
+ # "inherit" then the content attribute must be removed, if the new value is an
+ # ASCII case-insensitive match for the string "true" then the content attribute
+ # must be set to the string "true", if the new value is an ASCII
+ # case-insensitive match for the string "false" then the content attribute must
+ # be set to the string "false", and otherwise the attribute setter must throw a
+ # "SyntaxError" DOMException.
+
+ $value = strtolower($this->getAttribute('contenteditable'));
+ return ($value === 'true' || $value === 'false') ? $value : 'inherit';
+ }
+
+ protected function __set_contentEditable(string $value): void {
+ # On setting, if the new value is an ASCII case-insensitive match for the string
+ # "inherit" then the content attribute must be removed, if the new value is an
+ # ASCII case-insensitive match for the string "true" then the content attribute
+ # must be set to the string "true", if the new value is an ASCII
+ # case-insensitive match for the string "false" then the content attribute must
+ # be set to the string "false", and otherwise the attribute setter must throw a
+ # "SyntaxError" DOMException.
+ $value = strtolower($value);
+ switch ($value) {
+ case 'inherit':
+ $this->removeAttribute('contenteditable');
+ break;
+ case 'true':
+ case 'false':
+ $this->setAttribute('contenteditable', $value);
+ break;
+ default: throw new DOMException(DOMException::SYNTAX_ERROR);
+ }
+ }
+
+ protected function __get_dir(): string {
+ # The dir IDL attribute on an element must reflect the dir content attribute of
+ # that element, limited to only known values.
+
+ # If a reflecting IDL attribute is a DOMString attribute whose content attribute
+ # is an enumerated attribute, and the IDL attribute is limited to only known
+ # values, then, on getting, the IDL attribute must return the keyword value
+ # associated with the state the attribute is in, if any, or the empty string if
+ # the attribute is in a state that has no associated keyword value or if the
+ # attribute is not in a defined state (e.g. the attribute is missing and there
+ # is no missing value default). If there are multiple keyword values for the
+ # state, then return the conforming one. If there are multiple conforming
+ # keyword values, then one will be designated the canonical keyword; choose that
+ # one.
+
+ $value = $this->getAttribute('dir');
+ return (in_array($value, [ 'auto', 'ltr', 'rtl' ])) ? $value : '';
+ }
+
+ protected function __set_dir(string $value): void {
+ # On setting, the content attribute must be set to the specified new value.
+ $this->setAttribute('dir', $value);
+ }
+
+ protected function __get_draggable(): bool {
+ # The draggable IDL attribute, whose value depends on the content attribute's in
+ # the way described below, controls whether or not the element is draggable.
+ # Generally, only text selections are draggable, but elements whose draggable
+ # IDL attribute is true become draggable as well.
+
+ # If an element's draggable content attribute has the state true, the draggable
+ # IDL attribute must return true.
+
+ # Otherwise, if the element's draggable content attribute has the state false,
+ # the draggable IDL attribute must return false.
+
+ # Otherwise, the element's draggable content attribute has the state auto. If
+ # the element is an img element, an object element that represents an image, or
+ # an a element with an href content attribute, the draggable IDL attribute must
+ # return true; otherwise, the draggable IDL attribute must return false.
+
+ $value = $this->getAttribute('draggable');
+ $value = ($value === 'true' || $value === 'false') ? $value : 'auto';
+ if ($value === 'true') {
+ return true;
+ } elseif ($value === 'false') {
+ return false;
+ }
+
+ $tagName = $this->tagName;
+ if ($tagName === 'img' || $this->hasAttribute('href')) {
+ return true;
+ }
+ // Without actually being able to read the image in question it's impossible to
+ // completely tell if an object element is representing an image. What we're
+ // going to do here is check for a type attribute with a mimetype associated
+ // with a known web image type, and, failing that, check the file extension of
+ // the data attribute.
+ elseif ($tagName === 'object') {
+ $type = $this->getAttribute('type');
+ if ($type !== null && in_array($type, [ 'image/apng', 'image/avif', 'image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp' ])) {
+ return true;
+ }
+
+ $data = $this->getAttribute('data');
+ if ($data !== null && in_array(strtolower(substr(strrchr($fileName, '.'), 1)), [ 'apng', 'avif', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp' ])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function __set_draggable(bool $value): void {
+ # If the draggable IDL attribute is set to the value false, the draggable
+ # content attribute must be set to the literal value "false". If the draggable
+ # IDL attribute is set to the value true, the draggable content attribute must
+ # be set to the literal value "true".
+ $this->setAttribute('draggable', ($value) ? 'true' : 'false');
+ }
+
+ protected function __get_enterKeyHint(): string {
+ # The enterKeyHint IDL attribute must reflect the enterkeyhint content
+ # attribute, limited to only known values.
+
+ # If a reflecting IDL attribute is a DOMString attribute whose content attribute
+ # is an enumerated attribute, and the IDL attribute is limited to only known
+ # values, then, on getting, the IDL attribute must return the keyword value
+ # associated with the state the attribute is in, if any, or the empty string if
+ # the attribute is in a state that has no associated keyword value or if the
+ # attribute is not in a defined state (e.g. the attribute is missing and there
+ # is no missing value default). If there are multiple keyword values for the
+ # state, then return the conforming one. If there are multiple conforming
+ # keyword values, then one will be designated the canonical keyword; choose that
+ # one.
+
+ $value = $this->getAttribute('enterkeyhint');
+ return (in_array($value, [ 'done', 'enter', 'go', 'next', 'previous', 'search', 'send' ])) ? $value : '';
+ }
+
+ protected function __set_enterKeyHint(string $value): void {
+ # On setting, the content attribute must be set to the specified new value.
+ $this->setAttribute('enterkeyhint', $value);
+ }
+
+ protected function __get_hidden(): bool {
+ # The hidden getter steps are to return true if this's visibility state is
+ # "hidden", otherwise false.
+
+ $value = ($this->getAttribute('hidden') !== null);
+ if ($value) {
+ return true;
+ }
+
+ $n = $this;
+ while ($n = $n->parentNode) {
+ if (property_exists($n, 'hidden') && ($n->getAttribute('hidden') !== null)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function __set_hidden(bool $value): void {
+ # The autofocus IDL attribute must reflect the content attribute of the same
+ # name.
+
+ # If a reflecting IDL attribute is a boolean attribute, then on getting the IDL
+ # attribute must return true if the content attribute is set, and false if it is
+ # absent. On setting, the content attribute must be removed if the IDL attribute
+ # is set to false, and must be set to the empty string if the IDL attribute is
+ # set to true. (This corresponds to the rules for boolean content attributes.)
+
+ if ($value) {
+ $this->setAttribute('hidden', '');
+ } else {
+ $this->removeAttribute('hidden');
+ }
+ }
+
+ protected function __get_innerText(): ?string {
+ # The innerText and outerText getter steps are:
+ # 1. If this is not being rendered or if the user agent is a non-CSS user agent,
+ # then return this's descendant text content.
+ // This is a non-CSS user agent. Nothing else to do here.
+ return $this->__get_textContent();
+ }
+
+ protected function __set_innerText(string $value): void {
+ # The innerText setter steps are:
+ # 1. Let fragment be the rendered text fragment for the given value given this's node
+ # document.
+ $fragment = $this->getRenderedTextFragment($value);
+
+ # 2. Replace all with fragment within this.
+ $innerNode = $this->innerNode;
+ $children = $innerNode->childNodes;
+ while ($innerNode->hasChildNodes()) {
+ $innerNode->removeChild($innerNode->firstChild);
+ }
+
+ // Check for child nodes before appending to prevent a stupid warning.
+ if ($fragment->hasChildNodes()) {
+ $innerNode->appendChild($fragment);
+ }
+ }
+
+ protected function __get_inputMode(): string {
+ # The inputMode IDL attribute must reflect the inputmode content attribute,
+ # limited to only known values.
+
+ # If a reflecting IDL attribute is a DOMString attribute whose content attribute
+ # is an enumerated attribute, and the IDL attribute is limited to only known
+ # values, then, on getting, the IDL attribute must return the keyword value
+ # associated with the state the attribute is in, if any, or the empty string if
+ # the attribute is in a state that has no associated keyword value or if the
+ # attribute is not in a defined state (e.g. the attribute is missing and there
+ # is no missing value default). If there are multiple keyword values for the
+ # state, then return the conforming one. If there are multiple conforming
+ # keyword values, then one will be designated the canonical keyword; choose that
+ # one.
+
+ $value = $this->getAttribute('inputmode');
+ return (in_array($value, [ 'decimal', 'email', 'none', 'numeric', 'search', 'tel', 'text', 'url' ])) ? $value : '';
+ }
+
+ protected function __set_inputMode(string $value): void {
+ # On setting, the content attribute must be set to the specified new value.
+ $this->setAttribute('inputmode', $value);
+ }
+
+ protected function __get_isContentEditable(): string {
+ # The isContentEditable IDL attribute, on getting, must return true if the
+ # element is either an editing host or editable, and false otherwise.
+
+ # An editing host is either an HTML element with its contenteditable attribute
+ # in the true state, or a child HTML element of a Document whose design mode
+ # enabled is true.
+
+ # Something is editable if it is a node; it is not an editing host; it does not
+ # have a contenteditable attribute set to the false state; its parent is an
+ # editing host or editable; and either it is an HTML element, or it is an svg or
+ # math element, or it is not an Element and its parent is an HTML element.
+
+ $doc = ($this instanceof Document) ? $this : $this->ownerDocument;
+ if ($doc->designMode) {
+ return true;
+ }
+
+ $value = strtolower($this->getAttribute('contenteditable'));
+ if ($value === 'true') {
+ return true;
+ } elseif ($value === 'false') {
+ return false;
+ }
+
+ if ($doc !== this) {
+ $n = $this;
+ while ($n = $n->parentNode) {
+ if (property_exists($n, 'contentEditable') && $n->contentEditable === 'true') {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ protected function __get_lang(): string {
+ # The accessKey IDL attribute must reflect the title content attribute in no namespace.
+ return $this->getAttribute('lang') ?? '';
+ }
+
+ protected function __set_lang(string $value): void {
+ $this->setAttribute('lang', $value);
+ }
+
+ protected function __get_outerText(): ?string {
+ # The innerText and outerText getter steps are:
+ # 1. If this is not being rendered or if the user agent is a non-CSS user agent,
+ # then return this's descendant text content.
+ // This is a non-CSS user agent. Nothing else to do here.
+ return $this->__get_textContent();
+ }
+
+ protected function __set_outerText(string $value): void {
+ # The outerText setter steps are:
+ # 1. If this's parent is null, then throw a "NoModificationAllowedError"
+ # DOMException.
+ $innerNode = $this->innerNode;
+ if ($this->parentNode === null) {
+ throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED);
+ }
+
+ # 2. Let next be this's next sibling.
+ $next = $innerNode->nextSibling;
+
+ # 3. Let previous be this's previous sibling.
+ $previous = $innerNode->previousSibling;
+
+ # 4. Let fragment be the rendered text fragment for the given value given this's node
+ # document.
+ $fragment = $this->getRenderedTextFragment($value);
+
+ # 5. Replace this with fragment within this's parent.
+ // Check for child nodes before appending to prevent a stupid warning.
+ if ($fragment->hasChildNodes()) {
+ $innerNode->parentNode->replaceChild($fragment, $innerNode);
+ } else {
+ $innerNode->parentNode->removeChild($innerNode);
+ }
+
+ # 6. If next is non-null and next's previous sibling is a Text node, then merge
+ # with the next text node given next's previous sibling.
+ if ($next !== null && $next->previousSibling instanceof \DOMText) {
+ # To merge with the next text node given a Text node node:
+ # 1. Let next be node's next sibling.
+ # 2. If next is not a Text node, then return.
+ // Already checked for
+
+ # 3. Replace data with node, node's data's length, 0, and next's data.
+ $next->previousSibling->data .= $next->data;
+
+ # 4. If next's parent is non-null, then remove next.
+ // DEVIATION: There are no mutation events in this implementation, so there's no
+ // reason to check for a parent here.
+ $next->parentNode->removeChild($next);
+ }
+
+ # 7. If previous is a Text node, then merge with the next text node given previous.
+ if ($previous instanceof \DOMText) {
+ # To merge with the next text node given a Text node node:
+ # 1. Let next be node's next sibling.
+ $next = $previous->nextSibling;
+
+ # 2. If next is not a Text node, then return.
+ if ($next instanceof \DOMText) {
+ # 3. Replace data with node, node's data's length, 0, and next's data.
+ $previous->data .= $next->data;
+
+ # 4. If next's parent is non-null, then remove next.
+ // DEVIATION: There are no mutation events in this implementation, so there's no
+ // reason to check for a parent here.
+ $next->parentNode->removeChild($next);
+ }
+ }
+ }
+
+ protected function __get_spellcheck(): bool {
+ # The spellcheck IDL attribute, on getting, must return true if the element's
+ # spellcheck content attribute is in the true state, or if the element's
+ # spellcheck content attribute is in the default state and the element's default
+ # behavior is true-by-default, or if the element's spellcheck content attribute
+ # is in the default state and the element's default behavior is
+ # inherit-by-default and the element's parent element's spellcheck IDL attribute
+ # would return true; otherwise, if none of those conditions applies, then the
+ # attribute must instead return false.
+
+ // This user agent will be false-by-default.
+ return ($this->getAttribute('spellcheck') === 'true');
+ }
+
+ protected function __set_spellcheck(bool $value): bool {
+ # On setting, if the new value is true, then the element's spellcheck content
+ # attribute must be set to the literal string "true", otherwise it must be set
+ # to the literal string "false".
+ $this->setAttribute('spellcheck', ($value) ? 'true' : 'false');
+ }
+
+ protected function __get_title(): string {
+ # The accessKey IDL attribute must reflect the title content attribute.
+ return $this->getAttribute('title') ?? '';
+ }
+
+ protected function __set_title(string $value): void {
+ $this->setAttribute('title', $value);
+ }
+
+ protected function __get_translate(): bool {
+ # The translate IDL attribute must, on getting, return true if the element's
+ # translation mode is translate-enabled, and false otherwise.
+
+ # Each element (even non-HTML elements) has a translation mode, which is in
+ # either the translate-enabled state or the no-translate state. If an HTML
+ # element's translate attribute is in the yes state, then the element's
+ # translation mode is in the translate-enabled state; otherwise, if the
+ # element's translate attribute is in the no state, then the element's
+ # translation mode is in the no-translate state. Otherwise, either the element's
+ # translate attribute is in the inherit state, or the element is not an HTML
+ # element and thus does not have a translate attribute; in either case, the
+ # element's translation mode is in the same state as its parent element's, if
+ # any, or in the translate-enabled state, if the element is a document element.
+
+ $value = strtolower($this->getAttribute('translate'));
+ if ($value === 'yes') {
+ return true;
+ } elseif ($value === 'no') {
+ return false;
+ }
+
+ $n = $this;
+ while ($n = $n->parentNode) {
+ if (property_exists($n, 'translate') && $n->getAttribute('translate') === 'yes') {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function __set_translate(bool $value): void {
+ # On setting, it must set the content attribute's value to "yes" if the new
+ # value is true, and set the content attribute's value to "no" otherwise.
+ $this->setAttribute('translate', ($value) ? 'yes' : 'no');
+ }
+}
\ No newline at end of file
diff --git a/lib/HTMLOrSVGElement.php b/lib/HTMLOrSVGElement.php
new file mode 100644
index 0000000..49dae90
--- /dev/null
+++ b/lib/HTMLOrSVGElement.php
@@ -0,0 +1,42 @@
+getAttribute('autofocus') !== null);
+ }
+
+ protected function __set_autofocus(bool $value): void {
+ # The autofocus IDL attribute must reflect the content attribute of the same
+ # name.
+
+ # If a reflecting IDL attribute is a boolean attribute, then on getting the IDL
+ # attribute must return true if the content attribute is set, and false if it is
+ # absent. On setting, the content attribute must be removed if the IDL attribute
+ # is set to false, and must be set to the empty string if the IDL attribute is
+ # set to true. (This corresponds to the rules for boolean content attributes.)
+
+ if ($value) {
+ $this->setAttribute('autofocus', '');
+ } else {
+ $this->removeAttribute('autofocus');
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/SVGElement.php b/lib/SVGElement.php
index d44d48d..24d605d 100644
--- a/lib/SVGElement.php
+++ b/lib/SVGElement.php
@@ -9,4 +9,6 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM;
-class SVGElement extends Element {}
\ No newline at end of file
+class SVGElement extends Element {
+ use HTMLOrSVGElement;
+}
\ No newline at end of file
diff --git a/tests/cases/TestElement.php b/tests/cases/TestElement.php
index 407adcd..0007229 100644
--- a/tests/cases/TestElement.php
+++ b/tests/cases/TestElement.php
@@ -1372,101 +1372,6 @@ class TestElement extends \PHPUnit\Framework\TestCase {
}
- /**
- * @covers \MensBeam\HTML\DOM\Element::__get_innerText
- * @covers \MensBeam\HTML\DOM\Element::__set_innerText
- * @covers \MensBeam\HTML\DOM\Element::__get_outerText
- * @covers \MensBeam\HTML\DOM\Element::__set_outerText
- *
- * @covers \MensBeam\HTML\DOM\Collection::__construct
- * @covers \MensBeam\HTML\DOM\Collection::__get_length
- * @covers \MensBeam\HTML\DOM\Collection::count
- * @covers \MensBeam\HTML\DOM\Document::__construct
- * @covers \MensBeam\HTML\DOM\Document::__get_body
- * @covers \MensBeam\HTML\DOM\Document::__get_documentElement
- * @covers \MensBeam\HTML\DOM\Document::createElement
- * @covers \MensBeam\HTML\DOM\Document::createTextNode
- * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
- * @covers \MensBeam\HTML\DOM\Element::__construct
- * @covers \MensBeam\HTML\DOM\Element::__get_innerHTML
- * @covers \MensBeam\HTML\DOM\Element::getRenderedTextFragment
- * @covers \MensBeam\HTML\DOM\Node::__construct
- * @covers \MensBeam\HTML\DOM\Node::__get_childNodes
- * @covers \MensBeam\HTML\DOM\Node::__get_parentNode
- * @covers \MensBeam\HTML\DOM\Node::__get_textContent
- * @covers \MensBeam\HTML\DOM\Node::appendChild
- * @covers \MensBeam\HTML\DOM\Node::getInnerDocument
- * @covers \MensBeam\HTML\DOM\Node::getInnerNode
- * @covers \MensBeam\HTML\DOM\Node::getRootNode
- * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes
- * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes
- * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
- * @covers \MensBeam\HTML\DOM\Text::__construct
- * @covers \MensBeam\HTML\DOM\Inner\Document::__construct
- * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode
- * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
- * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
- * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty
- */
- public function testProperty_innerText_outerText() {
- $d = new Document();
- $d->appendChild($d->createElement('html'));
- $d->documentElement->appendChild($d->createElement('body'));
- $body = $d->body;
- $body->appendChild($d->createTextNode('ook '));
- $s = $body->appendChild($d->createElement('span'));
- $s->appendChild($d->createTextNode('ook'));
- $body->appendChild($d->createTextNode(' eek'));
- $this->assertSame('ook ook eek', $body->innerHTML);
-
- $s->innerText = <<assertSame('ook ookook eek ook eek', $body->innerText);
- $this->assertSame('ook
ook eek ook', $s->innerHTML);
-
- $s->outerText = 'ack';
- $this->assertSame('ook ack eek', $body->outerText);
- $this->assertEquals(1, $body->childNodes->length);
-
- $s = $body->appendChild($d->createElement('span'));
- $s->outerText = '';
- $this->assertSame('ook ack eek', $body->outerText);
- }
-
-
- /**
- * @covers \MensBeam\HTML\DOM\Element::__set_outerText
- *
- * @covers \MensBeam\HTML\DOM\Document::__construct
- * @covers \MensBeam\HTML\DOM\Document::createElement
- * @covers \MensBeam\HTML\DOM\DOMException::__construct
- * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
- * @covers \MensBeam\HTML\DOM\Element::__construct
- * @covers \MensBeam\HTML\DOM\Node::__construct
- * @covers \MensBeam\HTML\DOM\Node::__get_parentNode
- * @covers \MensBeam\HTML\DOM\Inner\Document::__construct
- * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
- * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
- * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
- */
- public function testProperty_outerText__errors() {
- $this->expectException(DOMException::class);
- $this->expectExceptionCode(DOMException::NO_MODIFICATION_ALLOWED);
- $d = new Document();
- $h = $d->createElement('html');
- $h->outerText = 'fail';
- }
-
-
/**
* @covers \MensBeam\HTML\DOM\Element::__get_outerHTML
* @covers \MensBeam\HTML\DOM\Element::__set_outerHTML
diff --git a/tests/cases/TestHTMLElement.php b/tests/cases/TestHTMLElement.php
new file mode 100644
index 0000000..cf2187e
--- /dev/null
+++ b/tests/cases/TestHTMLElement.php
@@ -0,0 +1,112 @@
+appendChild($d->createElement('html'));
+ $d->documentElement->appendChild($d->createElement('body'));
+ $body = $d->body;
+ $body->appendChild($d->createTextNode('ook '));
+ $s = $body->appendChild($d->createElement('span'));
+ $s->appendChild($d->createTextNode('ook'));
+ $body->appendChild($d->createTextNode(' eek'));
+ $this->assertSame('ook ook eek', $body->innerHTML);
+
+ $s->innerText = <<assertSame('ook ookook eek ook eek', $body->innerText);
+ $this->assertSame('ook
ook eek ook', $s->innerHTML);
+
+ $s->outerText = 'ack';
+ $this->assertSame('ook ack eek', $body->outerText);
+ $this->assertEquals(1, $body->childNodes->length);
+
+ $s = $body->appendChild($d->createElement('span'));
+ $s->outerText = '';
+ $this->assertSame('ook ack eek', $body->outerText);
+ }
+
+
+ /**
+ * @covers \MensBeam\HTML\DOM\HTMLElement::__set_outerText
+ *
+ * @covers \MensBeam\HTML\DOM\Document::__construct
+ * @covers \MensBeam\HTML\DOM\Document::createElement
+ * @covers \MensBeam\HTML\DOM\DOMException::__construct
+ * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
+ * @covers \MensBeam\HTML\DOM\Element::__construct
+ * @covers \MensBeam\HTML\DOM\Node::__construct
+ * @covers \MensBeam\HTML\DOM\Node::__get_parentNode
+ * @covers \MensBeam\HTML\DOM\Inner\Document::__construct
+ * @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode
+ * @covers \MensBeam\HTML\DOM\Inner\NodeCache::get
+ * @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
+ * @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
+ * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
+ * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
+ */
+ public function testProperty_outerText__errors() {
+ $this->expectException(DOMException::class);
+ $this->expectExceptionCode(DOMException::NO_MODIFICATION_ALLOWED);
+ $d = new Document();
+ $h = $d->createElement('html');
+ $h->outerText = 'fail';
+ }
+}
\ No newline at end of file
diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml
index 48901c6..8f05381 100644
--- a/tests/phpunit.dist.xml
+++ b/tests/phpunit.dist.xml
@@ -26,6 +26,7 @@
cases/TestDOMTokenList.php
cases/TestElement.php
cases/TestHTMLCollection.php
+ cases/TestHTMLElement.php
cases/TestInnerDocument.php
cases/TestNamedNodeMap.php
cases/TestNode.php