From 5f1b388cf79dc05bb7de8db0cfb324d42ee31617 Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Mon, 27 Sep 2021 07:22:57 -0500 Subject: [PATCH] Trying out HTMLElement --- lib/Document.php | 10 ++--- lib/Element.php | 14 +++---- lib/HTMLElement.php | 86 +++++++++++++++++++++++++++++++++++++++ lib/TokenList.php | 6 +-- lib/traits/ParentNode.php | 26 ++++++------ 5 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 lib/HTMLElement.php diff --git a/lib/Document.php b/lib/Document.php index 207290f..e430521 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -30,7 +30,7 @@ class Document extends AbstractDocument { protected const VOID_ELEMENTS = [ 'area', 'base', 'basefont', 'bgsound', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]; - public function __get_body(): \DOMNode { + protected function __get_body(): \DOMNode { if ($this->documentElement === null || $this->documentElement->childNodes->length === 0) { return null; } @@ -63,7 +63,7 @@ class Document extends AbstractDocument { return null; } - public function __set_body($value) { + protected function __set_body($value) { # On setting, the following algorithm must be run: # # 1. If the new value is not a body or frameset element, then throw a @@ -100,15 +100,15 @@ class Document extends AbstractDocument { $this->_body = $value; } - public function __get_documentEncoding(): ?string { + protected function __get_documentEncoding(): ?string { return $this->_documentEncoding; } - public function __get_quirksMode(): int { + protected function __get_quirksMode(): int { return $this->_quirksMode; } - public function __get_xpath(): \DOMXPath { + protected function __get_xpath(): \DOMXPath { if ($this->_xpath === null) { $this->_xpath = new \DOMXPath($this); } diff --git a/lib/Element.php b/lib/Element.php index 50969c2..153f923 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -12,7 +12,7 @@ class Element extends \DOMElement { protected $_classList; - public function __get_classList(): ?TokenList { + protected function __get_classList(): ?TokenList { // MensBeam\HTML\DOM\TokenList uses WeakReference to prevent a circular reference, // so it requires PHP 7.4 to work. if (version_compare(\PHP_VERSION, '7.4.0', '>=')) { @@ -25,7 +25,7 @@ class Element extends \DOMElement { return null; // @codeCoverageIgnore } - public function __get_innerHTML(): string { + protected function __get_innerHTML(): string { ### DOM Parsing Specification ### # 2.3 The InnerHTML mixin # @@ -37,7 +37,7 @@ class Element extends \DOMElement { return $this->ownerDocument->serialize($this); } - public function __set_innerHTML(string $value) { + protected function __set_innerHTML(string $value) { ### DOM Parsing Specification ### # 2.3 The InnerHTML mixin # @@ -96,7 +96,7 @@ class Element extends \DOMElement { } } - public function __get_nextElementSibling(): Element { + protected function __get_nextElementSibling(): Element { # The nextElementSibling getter steps are to return the first following sibling # that is an element; otherwise null. if ($this->parentNode !== null) { @@ -121,7 +121,7 @@ class Element extends \DOMElement { return null; } - public function __get_outerHTML(): string { + protected function __get_outerHTML(): string { ### DOM Parsing Specification ### # 2.4 Extensions to the Element interface # outerHTML @@ -135,7 +135,7 @@ class Element extends \DOMElement { return $this->__toString(); } - public function __set_outerHTML(string $value) { + protected function __set_outerHTML(string $value) { ### DOM Parsing Specification ### # 2.4 Extensions to the Element interface # outerHTML @@ -172,7 +172,7 @@ class Element extends \DOMElement { $this->parentNode->replaceChild($fragment, $this); } - public function __get_previousElementSibling(): Element { + protected function __get_previousElementSibling(): Element { # The previousElementSibling getter steps are to return the first preceding # sibling that is an element; otherwise null. if ($this->parentNode !== null) { diff --git a/lib/HTMLElement.php b/lib/HTMLElement.php new file mode 100644 index 0000000..5a52b10 --- /dev/null +++ b/lib/HTMLElement.php @@ -0,0 +1,86 @@ +getAttribute('accesskey'); + } + + protected function __set_accessKey(string $value) { + return $this->setAttribute('accesskey', $value); + } + + protected function __get_contentEditable(): string { + # 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. + $result = $this->getAttribute('contenteditable'); + switch ($value) { + case 'false': + case 'true': + return $result; + default: + return 'inherit'; + } + } + + protected function __set_contentEditable(string $value) { + # 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. + switch ($value) { + case 'inherit' + $this->removeAttribute('contenteditable'); + case 'false': + case 'true': + return $this->setAttribute('contenteditable', $value); + default: + throw new DOMException(DOMException::SYNTAX_ERROR); + } + } + + protected function __get_isContentEditable(): bool { + # 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. + $contentEditable = $this->__get_contentEditable(); + $designMode = ($this->ownerDocument->designMode === 'on'); + if ($contentEditable === 'true' || $designMode) { + return true; + } elseif ($contentEditable !== 'false') { + // If the parent can be either an editing host or editable then all is needed + // is to see if there's an ancestor that's an editing host. Just seems absurd + // to word the specification like that. Since isContentEditable is a property + // of HTMLElement there's no need to check if it's an HTML element, svg, or + // non-element child of foreign content. There is also no need to check for + // design mode enabled on the document because it's checked above. + if ($this->moonwalk(function($n) { + if ($n instanceof HTMLElement && $n->contentEditable === 'true') { + return true; + } + })->current() !== null) { + return true; + } + } + + return false; + } +} diff --git a/lib/TokenList.php b/lib/TokenList.php index 710e4da..c6feb90 100644 --- a/lib/TokenList.php +++ b/lib/TokenList.php @@ -21,15 +21,15 @@ class TokenList implements \ArrayAccess, \Countable, \Iterator { private const ASCII_WHITESPACE_REGEX = '/[\t\n\x0c\r ]+/'; - public function __get_length(): int { + protected function __get_length(): int { return $this->_length; } - public function __get_value(): string { + protected function __get_value(): string { return $this->__toString(); } - public function __set_value(string $value) { + protected function __get_value(string $value) { $this->tokenSet = $this->parseOrderedSet($value); $this->_length = count($this->tokenSet); } diff --git a/lib/traits/ParentNode.php b/lib/traits/ParentNode.php index 16d9fc5..1994ee5 100644 --- a/lib/traits/ParentNode.php +++ b/lib/traits/ParentNode.php @@ -9,7 +9,7 @@ namespace MensBeam\HTML\DOM; if (version_compare(\PHP_VERSION, '8.0', '>=')) { # 4.2.6. Mixin ParentNode trait ParentNode { - public function __get_children(): \DOMNodeList { + protected function __get_children(): \DOMNodeList { # The children getter steps are to return an HTMLCollection collection rooted at # this matching only element children. // DEVIATION: HTMLCollection doesn't exist in PHP's DOM, and \DOMNodeList is @@ -97,7 +97,7 @@ if (version_compare(\PHP_VERSION, '8.0', '>=')) { } } else { trait ParentNode { - public function __get_childElementCount(): int { + protected function __get_childElementCount(): int { # The childElementCount getter steps are to return the number of children of # this that are elements. $count = 0; @@ -110,7 +110,7 @@ if (version_compare(\PHP_VERSION, '8.0', '>=')) { return $count; } - public function __get_children(): \DOMNodeList { + protected function __get_children(): \DOMNodeList { # The children getter steps are to return an HTMLCollection collection rooted at # this matching only element children. // DEVIATION: HTMLCollection doesn't exist in PHP's DOM, and \DOMNodeList is @@ -123,7 +123,7 @@ if (version_compare(\PHP_VERSION, '8.0', '>=')) { return $document->xpath->query('//*', (!$isDocument) ? $this : null); } - public function __get_firstElementChild(): Element { + protected function __get_firstElementChild(): Element { # The firstElementChild getter steps are to return the first child that is an # element; otherwise null. foreach ($this->childNodes as $child) { @@ -134,7 +134,7 @@ if (version_compare(\PHP_VERSION, '8.0', '>=')) { return null; } - public function __get_lastElementChild(): Element { + protected function __get_lastElementChild(): Element { # The lastElementChild getter steps are to return the last child that is an # element; otherwise null. for ($i = $this->childNodes->length - 1; $i >= 0; $i--) { @@ -191,14 +191,14 @@ if (version_compare(\PHP_VERSION, '8.0', '>=')) { } # 5. Remove all parent’s children, in tree order, with the suppress observers # flag set. - // DEVIATION: There is no scripting in this implementation, so cannnot set + // DEVIATION: There is no scripting in this implementation so cannnot set // suppress observers flag. while ($this->hasChildNodes()) { $this->removeChild($this->firstChild); } # 6. If node is non-null, then insert node into parent before null with the # suppress observers flag set. - // DEVIATION: There is no scripting in this implementation, so cannnot set + // DEVIATION: There is no scripting in this implementation so cannnot set // suppress observers flag. if ($node !== null) { $this->appendChild($node); @@ -220,20 +220,18 @@ if (version_compare(\PHP_VERSION, '8.0', '>=')) { // through them again to append. Let's optimize this a wee bit, shall we? $document = ($this instanceof Document) ? $this : $this->ownerDocument; $node = ($node->length > 1) ? $document->createDocumentFragment() : null; - foreach ($nodes as &$n) { + foreach ($nodes as $n) { // Can't do union types until PHP 8... OTL if (!$n instanceof \DOMNode && !is_string($n)) { - trigger_error(sprintf("Uncaught TypeError: %s::%s(): Argument #1 (\$%s) must be of type \DOMNode|string, %s given", __CLASS__, __METHOD__, 'nodes', gettype($n))); + throw new DOMException(DOMException::ARGUMENT_TYPE_ERROR, 1, 'nodes', '\DOMNode|string', gettype($n)); } - if (is_string($n)) { - $n = $this->ownerDocument->createTextNode($n); - } + $nn = (!is_string($n)) ? $n : $this->ownerDocument->createTextNode($n); if ($node !== null) { - $node->appendChild($n); + $node->appendChild($nn); } else { - $node = $n; + $node = $nn; } }