Browse Source

More Element tests

wrapper-classes
Dustin Wilson 3 years ago
parent
commit
611bdd5d70
  1. 5
      lib/Document.php
  2. 519
      lib/Element-new.php
  3. 214
      lib/Element.php
  4. 2
      lib/TokenList.php
  5. 75
      tests/cases/TestElement.php
  6. 12
      vendor-bin/phpunit/composer.lock

5
lib/Document.php

@ -16,7 +16,6 @@ class Document extends AbstractDocument {
/** Nonstandard */
protected $_documentEncoding = null;
protected $_quirksMode = Parser::NO_QUIRKS_MODE;
protected $_type = 'html';
/** Nonstandard */
protected $_xpath = null;
@ -111,10 +110,6 @@ class Document extends AbstractDocument {
return $this->_quirksMode;
}
protected function __get_type(): int {
return $this->_type;
}
protected function __get_xpath(): \DOMXPath {
if ($this->_xpath === null) {
$this->_xpath = new \DOMXPath($this);

519
lib/Element-new.php

@ -1,519 +0,0 @@
<?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\Parser;
class Element extends \DOMElement {
use DocumentOrElement, MagicProperties, Moonwalk, ParentNode, ToString, Walk;
protected $_classList;
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', '>=')) {
// Only create the class list if it is actually used.
if ($this->_classList === null) {
$this->_classList = new TokenList($this, 'class');
}
return $this->_classList;
}
return null; // @codeCoverageIgnore
}
protected function __get_innerHTML(): string {
### DOM Parsing Specification ###
# 2.3 The InnerHTML mixin
#
# 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).
// DEVIATION: Parsing of XML documents will not be handled by this
// implementation, so there's no need for the well-formed flag.
return $this->ownerDocument->saveHTML($this);
}
protected function __set_innerHTML(string $value) {
### DOM Parsing Specification ###
# 2.3 The InnerHTML mixin
#
# 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.
// DEVIATION: There is no scripting in this implementation.
# 2. Let fragment be the result of invoking the fragment parsing algorithm with
# the new value as markup, and with context element.
$fragment = Parser::parseFragment($this, $this->ownerDocument->quirksMode, $value, 'UTF-8');
$fragment = $this->ownerDocument->importNode($fragment);
# 3. If the context object is a template element, then let context object be the
# template's template contents (a DocumentFragment).
if ($this->nodeName === 'template') {
$this->content = $fragment;
}
# 4. Replace all with fragment within the context object.
else {
# To replace all with a node within a parent, run these steps:
#
# 1. Let removedNodes be parent’s children.
// DEVIATION: removedNodes is used below for scripting. There is no scripting in
// this implementation.
# 2. Let addedNodes be parent’s children.
// DEVIATION: addedNodes is used below for scripting. There is no scripting in
// this implementation.
# 3. If node is a DocumentFragment node, then set addedNodes to node’s
# children.
// DEVIATION: Again, there is no scripting in this implementation.
# 4. Otherwise, if node is non-null, set addedNodes to « node ».
// DEVIATION: Yet again, there is no scripting in this implementation.
# 5. Remove all parent’s children, in tree order, with the suppress observers
# flag set.
// DEVIATION: There are no observers to suppress as there is no scripting in
// this implementation.
while ($this->hasChildNodes()) {
$this->removeChild($this->firstChild);
}
# 6. Otherwise, if node is non-null, set addedNodes to « node ».
# If node is non-null, then insert node into parent before null with the
# suppress observers flag set.
// DEVIATION: Yet again, there is no scripting in this implementation.
# 7. If either addedNodes or removedNodes is not empty, then queue a tree
# mutation record for parent with addedNodes, removedNodes, null, and null.
// DEVIATION: Normally the tree mutation record would do the actual replacement,
// but there is no scripting in this implementation. Going to simply append the
// fragment instead.
$this->appendChild($fragment);
}
}
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) {
$start = false;
foreach ($this->parentNode->childNodes as $child) {
if (!$start) {
if ($child->isSameNode($this)) {
$start = true;
}
continue;
}
if (!$child instanceof Element) {
continue;
}
return $child;
}
}
return null;
}
protected function __get_outerHTML(): string {
### DOM Parsing Specification ###
# 2.4 Extensions to the Element interface
# outerHTML
#
# 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).
// DEVIATION: Parsing of XML documents will not be handled by this
// implementation, so there's no need for the well-formed flag.
return $this->__toString();
}
protected function __set_outerHTML(string $value) {
### DOM Parsing Specification ###
# 2.4 Extensions to the Element interface
# outerHTML
#
# On setting, the following steps must be run:
# 1. Let parent be the context object's parent.
$parent = $this->parentNode;
# 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.
// The spec is unclear here as to what to do. What do you return? Most browsers
// throw an exception here, so that's what we're going to do.
if ($parent === null) {
throw new DOMException(DOMException::OUTER_HTML_FAILED_NOPARENT);
}
# 3. If parent is a Document, throw a "NoModificationAllowedError" DOMException.
elseif ($parent instanceof Document) {
throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED);
}
# 4. 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.
elseif ($parent instanceof DocumentFragment) {
$parent = $this->ownerDocument->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.
$fragment = Parser::parseFragment($parent, $this->ownerDocument->quirksMode, $value, 'UTF-8');
$fragment = $this->ownerDocument->importNode($fragment);
# 6. Replace the context object with fragment within the context object's
# parent.
$this->parentNode->replaceChild($fragment, $this);
}
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) {
foreach ($this->parentNode->childNodes as $child) {
if ($child->isSameNode($this)) {
return null;
}
if (!$child instanceof Element) {
continue;
}
return $child;
}
}
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 (!strpos($attr->value, 'U')) ? $attr->value : $this->uncoerceName($attr->value);
}
public function getAttributeNode(string $qualifiedName): ?\DOMAttr {
# 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.
// Document will always be an HTML document
if ($this->isHTMLNamespace()) {
$qualifiedName = strtolower($qualifiedName);
}
# 2. Return the first attribute in element’s attribute list whose qualified name is
# qualifiedName; otherwise null.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster. Also, check to see if the node's name was
// coerced.
try {
$value = parent::getAttributeNode($qualifiedName);
} 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
$qualifiedName = $this->coerceName($qualifiedName);
$value = parent::getAttributeNode($qualifiedName);
}
if ($value === false && !parent::hasAttribute($qualifiedName)) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes.
foreach ($this->attributes as $a) {
if ($a->nodeName === $qualifiedName) {
return $a;
}
}
return null;
}
return ($value !== false) ? $value : null;
}
public function getAttributeNodeNS(?string $namespace = null, string $localName): ?\DOMAttr {
# 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;
}
# 2. Return the attribute in element’s attribute list whose namespace is namespace
# and local name is localName, if any; otherwise null.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster. Also, check to see if the node's namespace
// and localName was coerced.
try {
$value = parent::getAttributeNodeNS($namespace, $localName);
} 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
$namespace = $this->coerceName($namespace);
$localName = $this->coerceName($localName);
$value = parent::getAttributeNodeNS($namespace, $localName);
}
if ($value === false && !parent::hasAttributeNS($namespace, $localName)) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes.
foreach ($this->attributes as $a) {
if ($a->namespaceURI === $namespace && $a->localName === $localName) {
return $a;
}
}
return null;
}
return ($value !== false) ? $value : null;
}
public function getAttributeNS($namespaceURI, $localName) {
// Newer versions of the DOM spec have getAttributeNS return an empty string
// only when the attribute exists and is empty, otherwise null. This fixes that.
$value = parent::getAttributeNS($namespaceURI, $localName);
if ($value === '' && !$this->hasAttributeNS($namespaceURI, $localName)) {
return null;
}
return $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.
// Document will always be an HTML document
if ($this->isHTMLNamespace()) {
$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. Also, check to see if the node's name was
// coerced.
$value = parent::hasAttribute($this->coerceName($qualifiedName));
if (!$value) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes,
// so try it again.
$value = ($this->getAttributeNode($qualifiedName) !== null);
}
return $value;
}
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(self::$nameProductionRegex, $qualifiedName) !== 1) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
# 2. If this is in the HTML namespace and its node document is an HTML document,
# then set qualifiedName to qualifiedName in ASCII lowercase.
// Document will always be an HTML document
if ($this->isHTMLNamespace()) {
$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. But, first, we have to hack in classList
// support and then work around a couple of PHP bugs.
if ($this->isHTMLNamespace()) {
$qualifiedName = strtolower($qualifiedName);
if ($qualifiedName === 'class' && $this->_classList !== null) {
$this->_classList->value = $value;
return;
}
}
try {
parent::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
parent::setAttributeNS(null, $this->coerceName($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 ($qualifiedName === 'id' && $namespaceURI === null) {
$this->setIdAttribute($qualifiedName, true);
}
}
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);
$qualifiedName = ($prefix === null) ? $localName : "{$prefix}:{$localName}";
# 2. Set an attribute value for this using localName, value, and also prefix and
# namespace.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster. But, first, we have to hack in classList
// support and then work around a couple of PHP bugs.
if ($this->isHTMLNamespace() && $qualifiedName === 'class' && $this->_classList !== null) {
$this->_classList->value = $value;
} elseif ($namespace === Parser::XMLNS_NAMESPACE) {
// NOTE: We create attribute nodes so that xmlns attributes
// don't get lost; otherwise they cannot be serialized
$a = @$this->ownerDocument->createAttributeNS($namespace, $qualifiedName);
if ($a === false) {
// The document element does not exist yet, so we need
// to insert this element into the document
$this->ownerDocument->appendChild($this);
$a = $this->ownerDocument->createAttributeNS($namespace, $qualifiedName);
$this->ownerDocument->removeChild($this);
}
$a->value = $this->escapeString($value, true);
$this->setAttributeNodeNS($a);
} else {
try {
parent::setAttributeNS($namespace, $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
if ($namespace !== null && $namespace !== Parser::HTML_NAMESPACE) {
$qualifiedName = implode(':', array_map([$this, 'coerceName'], explode(':', $qualifiedName, 2)));
} else {
$qualifiedName = $this->coerceName($qualifiedName);
}
parent::setAttributeNS($namespace, $qualifiedName, $value);
}
}
if ($qualifiedName === 'id' && $namespaceURI === null) {
$this->setIdAttribute($qualifiedName, true);
}
}
/*public function hasAttributeNS(?string $namespace = null, string $localName) {
if (!parent::hasAttributeNS($namespace, $localName)) {
foreach ($this->attributes as $a) {
if (($a->namespaceURI === $namespace || (strpos($a->namespaceURI, 'U') !== false && $a->namespaceURI === $this->coerceName($namespaceURI))) && ($a->localName === $localName || (strpos($a->localName, 'U') !== false && $a->localName === $this->coerceName($localName)))) {
return true;
}
}
return false;
}
return true;
}
public function setAttribute($name, $value) {
$this->setAttributeNS(null, $name, $value);
}
public function setAttributeNS($namespaceURI, $qualifiedName, $value) {
// Normalize the attribute name and namespace URI per modern DOM specifications.
if ($namespaceURI !== null) {
$namespaceURI = trim($namespaceURI);
}
$qualifiedName = trim($qualifiedName);
if ($namespaceURI === null && ($this->namespaceURI ?? Parser::HTML_NAMESPACE) === Parser::HTML_NAMESPACE && !$this->hasAttributeNS($namespaceURI, $qualifiedName)) {
$qualifiedName = trim(strtolower($qualifiedName));
}
// If setting a class attribute and classList has been invoked use classList to
// set it.
if ($qualifiedName === 'class' && $namespaceURI === null && $this->_classList !== null) {
$this->_classList->value = $value;
} elseif ($namespaceURI === Parser::XMLNS_NAMESPACE) {
// NOTE: We create attribute nodes so that xmlns attributes
// don't get lost; otherwise they cannot be serialized
$a = @$this->ownerDocument->createAttributeNS($namespaceURI, $qualifiedName);
if ($a === false) {
// The document element does not exist yet, so we need
// to insert this element into the document
$this->ownerDocument->appendChild($this);
$a = $this->ownerDocument->createAttributeNS($namespaceURI, $qualifiedName);
$this->ownerDocument->removeChild($this);
}
$a->value = $this->escapeString($value, true);
$this->setAttributeNodeNS($a);
} else {
try {
parent::setAttributeNS($namespaceURI, $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
if ($namespaceURI !== null) {
$qualifiedName = implode(":", array_map([$this, "coerceName"], explode(":", $qualifiedName, 2)));
} else {
$qualifiedName = $this->coerceName($qualifiedName);
}
parent::setAttributeNS($namespaceURI, $qualifiedName, $value);
}
if ($qualifiedName === "id" && $namespaceURI === null) {
$this->setIdAttribute($qualifiedName, true);
}
}
}
public function setAttributeNode(\DOMAttr $attribute) {
return $this->setAttributeNodeNS($attribute, null);
}
public function setAttributeNodeNS(\DOMAttr $attribute) {
$fixId = false;
if ($attribute->namespaceURI === null) {
if ($attribute->name === 'id') {
$fixId = true;
}
// If appending a class attribute node, and classList has been invoked set
// the class using classList instead of appending the attribute node. Will
// return the created node instead. TokenList appends an attribute node
// internally to set the class attribute, so to prevent an infinite call loop
// from occurring, a check between the normalized value and classList's
// serialized value is performed. The spec is vague on how this is supposed to
// be handled.
elseif ($this->_classList !== null && $attribute->name === 'class' && preg_replace(Data::WHITESPACE_REGEX, ' ', $attribute->value) !== $this->_classList->value) {
$this->_classList->value = $attribute->value;
return $this->getAttributeNode('class');
}
}
$result = parent::setAttributeNodeNS($attribute);
if ($fixId) {
$this->setIdAttribute($attribute->name, true);
}
return $result;
}*/
}

214
lib/Element.php

@ -209,10 +209,20 @@ class Element extends \DOMElement {
return null;
}
# 3. Return attr’s value.
// Uncoerce the value if necessary.
// Uncoerce the value if necessary
return (!strpos($attr->value, 'U')) ? $attr->value : $this->uncoerceName($attr->value);
}
public function getAttributeNames(): array {
$result = [];
foreach ($this->attributes as $a) {
// Uncoerce names if necessary
$result[] = (!strpos($a->nodeName, 'U')) ? $a->nodeName : $this->uncoerceName($a->nodeName);
}
return $result;
}
public function getAttributeNode(string $qualifiedName): ?\DOMAttr {
# The getAttributeNode(qualifiedName) method steps are to return the result of
# getting an attribute given qualifiedName and this.
@ -230,19 +240,25 @@ class Element extends \DOMElement {
# 2. Return the first attribute in element’s attribute list whose qualified name is
# qualifiedName; otherwise null.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster. Also, check to see if the node's name was
// coerced.
$qualifiedName = $this->coerceName($qualifiedName);
// when we can because it's faster.
$value = parent::getAttributeNode($qualifiedName);
if ($value === null || $value === false) {
// Replace any offending characters with "UHHHHHH" where H are the uppercase
// hexadecimal digits of the character's code point
$qualifiedName = $this->coerceName($qualifiedName);
if ($value === false && !parent::hasAttribute($qualifiedName)) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes.
foreach ($this->attributes as $a) {
if ($a->nodeName === $qualifiedName) {
return $a;
if ($value === null) {
$value = parent::getAttributeNode($qualifiedName);
}
if ($value === false) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes.
foreach ($this->attributes as $a) {
if ($a->nodeName === $qualifiedName) {
return $a;
}
}
return null;
}
return null;
}
return ($value !== false) ? $value : null;
@ -263,33 +279,46 @@ class Element extends \DOMElement {
# 2. Return the attribute in element’s attribute list whose namespace is namespace
# and local name is localName, if any; otherwise null.
// Going to try to handle this by getting the PHP DOM to do the heavy lifting
// when we can because it's faster. Also, check to see if the node's namespace
// and localName was coerced.
$namespace = $this->coerceName($namespace);
$localName = $this->coerceName($localName);
// when we can because it's faster.
$value = parent::getAttributeNodeNS($namespace, $localName);
if ($value === false && !parent::hasAttributeNS($namespace, $localName)) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes.
foreach ($this->attributes as $a) {
if ($a->namespaceURI === $namespace && $a->localName === $localName) {
return $a;
if ($value === null || $value === false) {
// Replace any offending characters with "UHHHHHH" where H are the uppercase
// hexadecimal digits of the character's code point
$namespace = $this->coerceName($namespace);
$localName = $this->coerceName($localName);
if ($value === null) {
$value = parent::getAttributeNodeNS($namespace, $localName);
}
if ($value === false) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes.
foreach ($this->attributes as $a) {
if ($a->namespaceURI === $namespace && $a->localName === $localName) {
return $a;
}
}
return null;
}
return null;
}
return ($value !== false) ? $value : null;
}
public function getAttributeNS($namespaceURI, $localName) {
// Newer versions of the DOM spec have getAttributeNS return an empty string
// only when the attribute exists and is empty, otherwise null. This fixes that.
$value = parent::getAttributeNS($namespaceURI, $localName);
if ($value === '' && !$this->hasAttributeNS($namespaceURI, $localName)) {
public function getAttributeNS(?string $namespace = null, 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;
}
return $value;
# 3. Return attr’s value.
// Uncoerce the value if necessary
return (!strpos($attr->value, 'U')) ? $attr->value : $this->uncoerceName($attr->value);
}
public function hasAttribute(string $qualifiedName): bool {
@ -306,18 +335,42 @@ class Element extends \DOMElement {
# 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. Also, check to see if the node's name was
// coerced.
$value = parent::hasAttribute($this->coerceName($qualifiedName));
// when we can because it's faster.
$value = parent::hasAttribute($qualifiedName);
if (!$value) {
// The PHP DOM does not acknowledge the presence of XMLNS-namespace attributes,
// so try it again.
// 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 = null, 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 = parent::hasAttributeNS($namespace, $localName);
if (!$value) {
// The 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 setAttribute(string $qualifiedName, string $value): void {
# 1. If qualifiedName does not match the Name production in XML, then throw an
# "InvalidCharacterError" DOMException.
@ -353,8 +406,8 @@ class Element extends \DOMElement {
parent::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
// Replace any offending characters with "UHHHHHH" where H are the uppercase
// hexadecimal digits of the character's code point
parent::setAttributeNS(null, $this->coerceName($qualifiedName), $value);
}
@ -369,7 +422,7 @@ class Element extends \DOMElement {
# 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);
$qualifiedName = ($prefix === null) ? $localName : "{$prefix}:{$localName}";
$qualifiedName = ($prefix === null || $prefix === '') ? $localName : "{$prefix}:{$localName}";
# 2. Set an attribute value for this using localName, value, and also prefix and
# namespace.
@ -398,7 +451,7 @@ class Element extends \DOMElement {
// 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
if ($namespace !== null && $namespace !== Parser::HTML_NAMESPACE) {
if ($namespace !== null) {
$qualifiedName = implode(':', array_map([$this, 'coerceName'], explode(':', $qualifiedName, 2)));
} else {
$qualifiedName = $this->coerceName($qualifiedName);
@ -411,95 +464,4 @@ class Element extends \DOMElement {
$this->setIdAttribute($qualifiedName, true);
}
}
/*public function hasAttributeNS(?string $namespace = null, string $localName) {
if (!parent::hasAttributeNS($namespace, $localName)) {
foreach ($this->attributes as $a) {
if (($a->namespaceURI === $namespace || (strpos($a->namespaceURI, 'U') !== false && $a->namespaceURI === $this->coerceName($namespaceURI))) && ($a->localName === $localName || (strpos($a->localName, 'U') !== false && $a->localName === $this->coerceName($localName)))) {
return true;
}
}
return false;
}
return true;
}
public function setAttribute($name, $value) {
$this->setAttributeNS(null, $name, $value);
}
public function setAttributeNS($namespaceURI, $qualifiedName, $value) {
// Normalize the attribute name and namespace URI per modern DOM specifications.
if ($namespaceURI !== null) {
$namespaceURI = trim($namespaceURI);
}
$qualifiedName = trim($qualifiedName);
if ($namespaceURI === null && ($this->namespaceURI ?? Parser::HTML_NAMESPACE) === Parser::HTML_NAMESPACE && !$this->hasAttributeNS($namespaceURI, $qualifiedName)) {
$qualifiedName = trim(strtolower($qualifiedName));
}
// If setting a class attribute and classList has been invoked use classList to
// set it.
if ($qualifiedName === 'class' && $namespaceURI === null && $this->_classList !== null) {
$this->_classList->value = $value;
} elseif ($namespaceURI === Parser::XMLNS_NAMESPACE) {
// NOTE: We create attribute nodes so that xmlns attributes
// don't get lost; otherwise they cannot be serialized
$a = @$this->ownerDocument->createAttributeNS($namespaceURI, $qualifiedName);
if ($a === false) {
// The document element does not exist yet, so we need
// to insert this element into the document
$this->ownerDocument->appendChild($this);
$a = $this->ownerDocument->createAttributeNS($namespaceURI, $qualifiedName);
$this->ownerDocument->removeChild($this);
}
$a->value = $this->escapeString($value, true);
$this->setAttributeNodeNS($a);
} else {
try {
parent::setAttributeNS($namespaceURI, $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
if ($namespaceURI !== null) {
$qualifiedName = implode(":", array_map([$this, "coerceName"], explode(":", $qualifiedName, 2)));
} else {
$qualifiedName = $this->coerceName($qualifiedName);
}
parent::setAttributeNS($namespaceURI, $qualifiedName, $value);
}
if ($qualifiedName === "id" && $namespaceURI === null) {
$this->setIdAttribute($qualifiedName, true);
}
}
}
public function setAttributeNode(\DOMAttr $attribute) {
return $this->setAttributeNodeNS($attribute, null);
}
public function setAttributeNodeNS(\DOMAttr $attribute) {
$fixId = false;
if ($attribute->namespaceURI === null) {
if ($attribute->name === 'id') {
$fixId = true;
}
// If appending a class attribute node, and classList has been invoked set
// the class using classList instead of appending the attribute node. Will
// return the created node instead. TokenList appends an attribute node
// internally to set the class attribute, so to prevent an infinite call loop
// from occurring, a check between the normalized value and classList's
// serialized value is performed. The spec is vague on how this is supposed to
// be handled.
elseif ($this->_classList !== null && $attribute->name === 'class' && preg_replace(Data::WHITESPACE_REGEX, ' ', $attribute->value) !== $this->_classList->value) {
$this->_classList->value = $attribute->value;
return $this->getAttributeNode('class');
}
}
$result = parent::setAttributeNodeNS($attribute);
if ($fixId) {
$this->setIdAttribute($attribute->name, true);
}
return $result;
}*/
}

2
lib/TokenList.php

@ -31,7 +31,7 @@ class TokenList implements \ArrayAccess, \Countable, \Iterator {
return $this->__toString();
}
protected function __get_value(string $value) {
protected function __set_value(string $value) {
$this->tokenSet = $this->parseOrderedSet($value);
$this->_length = count($this->tokenSet);
}

75
tests/cases/TestElement.php

@ -20,42 +20,24 @@ use MensBeam\HTML\Parser;
/** @covers \MensBeam\HTML\DOM\Element */
class TestElement extends \PHPUnit\Framework\TestCase {
/*public function provideAttributeNodeSettings(): iterable {
return [
[true, null, null, "test"],
[true, null, null, "TEST"],
[true, "http://www.w3.org/1999/xhtml", null, "test"],
[true, "http://www.w3.org/1999/xhtml", null, "TEST"],
[true, null, null, "testU00003Atest"],
[true, null, null, "TESTU00003ATEST"],
[true, "http://www.w3.org/1999/xhtml", null, "testU00003Atest"],
[true, "http://www.w3.org/1999/xhtml", null, "TESTU00003ATEST"],
[true, null, "http://www.w3.org/1999/xhtml", "test:test"],
[true, null, "http://www.w3.org/1999/xhtml", "TEST:TEST"],
[true, "http://www.w3.org/1998/Math/MathML", null, "test"],
[true, "http://www.w3.org/1998/Math/MathML", null, "TEST"],
[true, null, "http://www.w3.org/2000/xmlns/", "xmlns:xlink"],
[true, null, "http://www.w3.org/2000/xmlns/", "xmlns:XLINK"],
[true, null, "fake_ns", "test:testU00003Atest"],
[true, null, "fake_ns", "TEST:TESTU00003ATEST"],
[false, null, null, "test"],
[false, null, null, "TEST"],
[false, "http://www.w3.org/1999/xhtml", null, "test"],
[false, "http://www.w3.org/1999/xhtml", null, "TEST"],
[false, null, null, "testU00003Atest"],
[false, null, null, "TESTU00003ATEST"],
[false, "http://www.w3.org/1999/xhtml", null, "testU00003Atest"],
[false, "http://www.w3.org/1999/xhtml", null, "TESTU00003ATEST"],
[false, null, "http://www.w3.org/1999/xhtml", "test:test"],
[false, null, "http://www.w3.org/1999/xhtml", "TEST:TEST"],
[false, "http://www.w3.org/1998/Math/MathML", null, "test"],
[false, "http://www.w3.org/1998/Math/MathML", null, "TEST"],
[false, null, "http://www.w3.org/2000/xmlns/", "xmlns:xlink"],
[false, null, "http://www.w3.org/2000/xmlns/", "xmlns:XLINK"],
[false, null, "fake_ns", "test:testU00003Atest"],
[false, null, "fake_ns", "TEST:TESTU00003ATEST"],
];
}*/
/** @covers \MensBeam\HTML\DOM\Element::getAttributeNames */
public function testGetAttributeNames(): void {
$d = new Document();
$e = $d->createElement('html');
$d->appendChild($e);
$this->assertSame([], $e->getAttributeNames());
$e->setAttribute('ook:eek', 'ook');
$e->setAttributeNS(Parser::XMLNS_NAMESPACE, 'xmlns:xlink', Parser::XLINK_NAMESPACE);
$e->setAttribute('ook', 'eek');
$this->assertSame([
'ook:eek',
'xmlns:xlink',
'ook'
], $e->getAttributeNames());
}
public function provideGetHasSetAttribute(): iterable {
return [
@ -81,26 +63,27 @@ class TestElement extends \PHPUnit\Framework\TestCase {
}
/*public function provideGetHasSetAttributeNS(): iterable {
public function provideGetHasSetAttributeNS(): iterable {
return [
[ 'http://www.w3.org/1999/xhtml', null, 'ook', 'eek', null, 'ook', 'eek' ],
[ 'fake_ns', 'ook', 'eek', 'ookeek', 'ook', 'eek', 'ookeek' ],
[ 'another_fake_ns', 'steaming💩', 'poop💩', 'soccer', 'steaming💩', 'poop💩', 'soccer' ]
[ 'another_fake_ns', 'steaming💩', 'poop💩', 'soccer', 'steaming💩', 'poop💩', 'soccer' ],
[ Parser::XMLNS_NAMESPACE, 'xmlns', 'xlink', Parser::XLINK_NAMESPACE, 'xmlns', 'xlink', Parser::XLINK_NAMESPACE ]
];
}
**
/**
* @dataProvider provideGetHasSetAttributeNS
* @covers \MensBeam\HTML\DOM\Element::getAttributeNS
* @covers \MensBeam\HTML\DOM\Element::hasAttributeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNS
*
public function testGetHasSetAttributeNS(string $namespaceIn, string $prefixIn, string $localNameIn, string $valueIn, string $prefixExpected, string $localNameExpected, string $valueExpected): void {
*/
public function testGetHasSetAttributeNS(?string $namespaceIn, ?string $prefixIn, string $localNameIn, string $valueIn, ?string $prefixExpected, string $localNameExpected, string $valueExpected): void {
$d = new Document();
$d->appendChild($d->createElement('html'));
$e = $d->documentElement;
$e->setAttributeNS($namespaceURI, "{$prefixIn}:{$localNameIn}", $valueIn);
$this->assertTrue($e->hasAttribute($localNameExpected));
$this->assertSame($valueExpected, $e->getAttribute($localNameExpected));
}*/
$qualifiedNameIn = ($prefixIn === null || $prefixIn === '') ? $localNameIn : "{$prefixIn}:{$localNameIn}";
$e->setAttributeNS($namespaceIn, $qualifiedNameIn, $valueIn);
$this->assertTrue($e->hasAttributeNS($namespaceIn, $localNameExpected));
$this->assertSame($valueExpected, $e->getAttributeNS($namespaceIn, $localNameExpected));
}
}

12
vendor-bin/phpunit/composer.lock

@ -411,16 +411,16 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.5.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f"
"reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30f38bffc6f24293dadd1823936372dfa9e86e2f",
"reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae",
"reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae",
"shasum": ""
},
"require": {
@ -455,9 +455,9 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.0"
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1"
},
"time": "2021-09-17T15:28:14+00:00"
"time": "2021-10-02T14:08:47+00:00"
},
{
"name": "phpspec/prophecy",

Loading…
Cancel
Save