diff --git a/composer.lock b/composer.lock index 814ca6a..bc91cf8 100644 --- a/composer.lock +++ b/composer.lock @@ -1440,16 +1440,16 @@ }, { "name": "symfony/console", - "version": "v5.3.10", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3" + "reference": "3e7ab8f5905058984899b05a4648096f558bfeba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3", - "reference": "d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3", + "url": "https://api.github.com/repos/symfony/console/zipball/3e7ab8f5905058984899b05a4648096f558bfeba", + "reference": "3e7ab8f5905058984899b05a4648096f558bfeba", "shasum": "" }, "require": { @@ -1462,7 +1462,6 @@ "symfony/string": "^5.1" }, "conflict": { - "psr/log": ">=3", "symfony/dependency-injection": "<4.4", "symfony/dotenv": "<5.1", "symfony/event-dispatcher": "<4.4", @@ -1519,7 +1518,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.10" + "source": "https://github.com/symfony/console/tree/v5.3.11" }, "funding": [ { @@ -1535,20 +1534,20 @@ "type": "tidelift" } ], - "time": "2021-10-26T09:30:15+00:00" + "time": "2021-11-21T19:41:05+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", "shasum": "" }, "require": { @@ -1557,7 +1556,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1586,7 +1585,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" }, "funding": [ { @@ -1602,20 +1601,20 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2021-07-12T14:48:14+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.3.10", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9f34f02e8a5fdc7a56bafe011cea1ce97300e54c" + "reference": "d1e7059ebeb0b8f9fe5eb5b26eacd2e3c1f371cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9f34f02e8a5fdc7a56bafe011cea1ce97300e54c", - "reference": "9f34f02e8a5fdc7a56bafe011cea1ce97300e54c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d1e7059ebeb0b8f9fe5eb5b26eacd2e3c1f371cc", + "reference": "d1e7059ebeb0b8f9fe5eb5b26eacd2e3c1f371cc", "shasum": "" }, "require": { @@ -1659,7 +1658,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.3.10" + "source": "https://github.com/symfony/http-foundation/tree/v5.3.11" }, "funding": [ { @@ -1675,20 +1674,20 @@ "type": "tidelift" } ], - "time": "2021-10-11T15:41:55+00:00" + "time": "2021-11-04T16:37:19+00:00" }, { "name": "symfony/mime", - "version": "v5.3.8", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a756033d0a7e53db389618653ae991eba5a19a11" + "reference": "dffc0684f10526db12c52fcd6238c64695426d61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/a756033d0a7e53db389618653ae991eba5a19a11", - "reference": "a756033d0a7e53db389618653ae991eba5a19a11", + "url": "https://api.github.com/repos/symfony/mime/zipball/dffc0684f10526db12c52fcd6238c64695426d61", + "reference": "dffc0684f10526db12c52fcd6238c64695426d61", "shasum": "" }, "require": { @@ -1742,7 +1741,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.3.8" + "source": "https://github.com/symfony/mime/tree/v5.3.11" }, "funding": [ { @@ -1758,7 +1757,7 @@ "type": "tidelift" } ], - "time": "2021-09-10T12:30:38+00:00" + "time": "2021-11-20T16:42:42+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2415,16 +2414,16 @@ }, { "name": "symfony/process", - "version": "v5.3.7", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967" + "reference": "6c99204de85d04ca17f16c466fc61896960b0636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/38f26c7d6ed535217ea393e05634cb0b244a1967", - "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967", + "url": "https://api.github.com/repos/symfony/process/zipball/6c99204de85d04ca17f16c466fc61896960b0636", + "reference": "6c99204de85d04ca17f16c466fc61896960b0636", "shasum": "" }, "require": { @@ -2457,7 +2456,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.3.7" + "source": "https://github.com/symfony/process/tree/v5.3.11" }, "funding": [ { @@ -2473,25 +2472,29 @@ "type": "tidelift" } ], - "time": "2021-08-04T21:20:46+00:00" + "time": "2021-11-17T12:16:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.1" + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "suggest": { "symfony/service-implementation": "" @@ -2499,7 +2502,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -2536,7 +2539,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" }, "funding": [ { @@ -2552,7 +2555,7 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:43:52+00:00" + "time": "2021-11-04T16:48:04+00:00" }, { "name": "symfony/string", @@ -2639,16 +2642,16 @@ }, { "name": "symfony/yaml", - "version": "v5.3.6", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7" + "reference": "226638aa877bc4104e619a15f27d8141cd6b4e4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7", - "reference": "4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/226638aa877bc4104e619a15f27d8141cd6b4e4a", + "reference": "226638aa877bc4104e619a15f27d8141cd6b4e4a", "shasum": "" }, "require": { @@ -2694,7 +2697,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.3.6" + "source": "https://github.com/symfony/yaml/tree/v5.3.11" }, "funding": [ { @@ -2710,7 +2713,7 @@ "type": "tidelift" } ], - "time": "2021-07-29T06:20:01+00:00" + "time": "2021-11-20T16:42:42+00:00" }, { "name": "webuni/front-matter", diff --git a/lib/DOMTokenList.php b/lib/DOMTokenList.php new file mode 100644 index 0000000..f644fc1 --- /dev/null +++ b/lib/DOMTokenList.php @@ -0,0 +1,331 @@ +_length; + } + + protected function __get_value(): string { + # The value attribute must return the result of running this’s serialize steps. + return $this->__toString(); + } + + protected function __set_value(string $value) { + # Setting the value attribute must set an attribute value for the associated + # element using associated attribute’s local name and the given value. + $element = Reflection::getProtectedProperty($this->element->get(), 'innerNode'); + $element->setAttribute($this->localName, $value); + // Also update the token set and the length. + $this->tokenSet = $this->parseOrderedSet($value); + $this->_length = count($this->tokenSet); + } + + + protected function __construct(Element $element, string $attributeLocalName) { + # A DOMTokenList object also has an associated element and an attribute’s local + # name. + + # 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. + $this->element = \WeakReference::create($element); + # 2. Let localName be associated attribute’s local name. + $this->localName = $attributeLocalName; + # 3. Let value be the result of getting an attribute value given element and + # localName. + $element = Reflection::getProtectedProperty($element, 'innerNode'); + $value = $element->getAttribute($attributeLocalName); + # 4. Run the attribute change steps for element, localName, value, value, and + # null. + $this->attributeChange($attributeLocalName, $value, $value); + } + + + public function add(...$tokens): void { + # 1. For each token in tokens: + foreach ($tokens as $token) { + # 1. If token is the empty string, then throw a "SyntaxError" DOMException. + if ($token === '') { + throw new DOMException(DOMException::SYNTAX_ERROR); + } + + # 2. If token contains any ASCII whitespace, then throw an + # "InvalidCharacterError" DOMException. + if (preg_match(self::ASCII_WHITESPACE_REGEX, $token)) { + throw new DOMException(DOMException::INVALID_CHARACTER); + } + } + + # 2. For each token in tokens, append token to this’s token set. + foreach ($tokens as $token) { + if (!in_array($token, $this->tokenSet)) { + $this->tokenSet[] = $token; + $this->_length++; + } + } + + # 3. Run the update steps. + $this->update(); + } + + public function contains(string $token): bool { + return (in_array($token, $this->tokenSet)); + } + + public function count(): int { + return $this->_length; + } + + public function current(): ?string { + return $this->item($this->position); + } + + public function item(int $index): ?string { + # The item(index) method steps are: + # 1. If index is equal to or greater than this’s token set’s size, then return null. + if ($index >= $this->_length) { + return null; + } + # 2. Return this’s token set[index]. + return $this->tokenSet[$index]; + } + + public function key(): int { + return $this->position; + } + + public function next(): void { + ++$this->position; + } + + public function rewind(): void { + $this->position = 0; + } + + public function offsetExists($offset): bool { + return isset($this->tokenSet[$offset]); + } + + public function offsetGet($offset): string { + return $this->item($offset); + } + + public function offsetSet($offset, $value): void { + // Spec says nothing about setting values on DOMTokenList outside of add(); + // browsers silently fail here. + } + + public function offsetUnset($offset): void { + // Spec says nothing about unsetting values on DOMTokenList outside of remove(); + // browsers silently fail here. + } + + public function remove(...$tokens): void { + # 1. For each token in tokens: + foreach ($tokens as $token) { + # 1. If token is the empty string, then throw a "SyntaxError" DOMException. + if ($token === '') { + throw new DOMException(DOMException::SYNTAX_ERROR); + } + + # 2. If token contains any ASCII whitespace, then throw an + # "InvalidCharacterError" DOMException. + if (preg_match(self::ASCII_WHITESPACE_REGEX, $token)) { + throw new DOMException(DOMException::INVALID_CHARACTER); + } + } + + # For each token in tokens, remove token from this’s token set. + $changed = false; + foreach ($tokens as $token) { + if ($key = array_search($token, $this->tokenSet, true)) { + unset($this->tokenSet[$key]); + $this->_length--; + $changed = true; + } + } + + if ($changed) { + $this->tokenSet = array_values($this->tokenSet); + } + + # 3. Run the update steps. + $this->update(); + } + + public function replace(string $token, string $newToken): bool { + # 1. If either token or newToken is the empty string, then throw a "SyntaxError" + # DOMException. + if ($token === '' || $newToken === '') { + throw new DOMException(DOMException::SYNTAX_ERROR); + } + + # 2. If either token or newToken contains any ASCII whitespace, then throw an + # "InvalidCharacterError" DOMException. + if (preg_match(self::ASCII_WHITESPACE_REGEX, $token) || preg_match(self::ASCII_WHITESPACE_REGEX, $newToken)) { + throw new DOMException(DOMException::INVALID_CHARACTER); + } + + # 3. If this’s token set does not contain token, then return false. + if (!($key = array_search($token, $this->tokenSet))) { + return false; + } + + # 4. Replace token in this’s token set with newToken. + $this->tokenSet[$key] = $newToken; + + # 5. Run the update steps. + $this->update(); + + # 6. Return true. + return true; + } + + public function supports(string $token): bool { + # 1. Let result be the return value of validation steps called with token. + # 2. Return result. + # + # A DOMTokenList object’s validation steps for a given token are: + # + # 1. If the associated attribute’s local name does not define supported tokens, + # throw a TypeError. + # 2. Let lowercase token be a copy of token, in ASCII lowercase. + # 3. If lowercase token is present in supported tokens, return true. + # 4. Return false. + + // This class is presently only used for Element::classList, and it supports any + // valid class name as a token. So, there's nothing to do here at the moment. + // Just return true. + return true; + } + + public function toggle(string $token, ?bool $force = null): bool { + # 1. If token is the empty string, then throw a "SyntaxError" DOMException. + if ($token === '') { + throw new DOMException(DOMException::SYNTAX_ERROR); + } + + # 2. If token contains any ASCII whitespace, then throw an + # "InvalidCharacterError" DOMException. + if (preg_match(self::ASCII_WHITESPACE_REGEX, $token)) { + throw new DOMException(DOMException::INVALID_CHARACTER); + } + + # 3. If this’s token set[token] exists, then: + if (in_array($token, $this->tokenSet)) { + # 1. If force is either not given or is false, then remove token from this’s + # token set, run the update steps and return false. + if (!$force) { + $this->remove($token); + return false; + } + + # 2. Return true. + return true; + } + # 4. Otherwise, if force not given or is true, append token to this’s token set, + # run the update steps, and return true. + elseif ($force === null || $force === true) { + $this->add($token); + return true; + } + + # 5. Return false. + return false; + } + + public function valid(): bool { + return array_key_exists($this->position, $this->tokenSet); + } + + + protected function attributeChange(string $localName, ?string $oldValue = null, ?string $value = null, ?string $namespace = null): void { + # A DOMTokenList object has these attribute change steps for its associated + # element: + # + # 1. If localName is associated attribute’s local name, namespace is null, and + # value is null, then empty token set. + if ($localName === $this->localName && $namespace === null && $value === null) { + $this->tokenSet = []; + $this->_length = 0; + } + # 2. Otherwise, if localName is associated attribute’s local name, namespace is + # null, then set token set to value, parsed. + elseif ($localName === $this->localName && $namespace === null) { + $this->tokenSet = $this->parseOrderedSet($value); + $this->_length = count($this->tokenSet); + } + } + + protected function parseOrderedSet(string $input): array { + if ($input === '') { + return []; + } + + # The ordered set parser takes a string input and then runs these steps: + # + # 1. Let inputTokens be the result of splitting input on ASCII whitespace. + // There isn't a Set object in php, so make sure all the tokens are unique. + $inputTokens = array_unique(preg_split(self::ASCII_WHITESPACE_REGEX, $input)); + + # 2. Let tokens be a new ordered set. + # 3. For each token in inputTokens, append token to tokens. + # 4. Return tokens. + // There isn't a Set object in php, so just return the uniqued input tokens. + return $inputTokens; + } + + protected function update(): void { + # A DOMTokenList object’s update steps are: + # + # 1. If the associated element does not have an associated attribute and token + # set is empty, then return. + $element = Reflection::getProtectedProperty($this->element->get(), 'innerNode'); + if (!$element->hasAttribute('class') && count($this->tokenSet) === 0) { + return; + } + + # 2. Set an attribute value for the associated element using associated + # attribute’s local name and the result of running the ordered set serializer + # for token set. + $class = $element->ownerDocument->createAttribute($this->localName); + $class->value = $this->__toString(); + $element->setAttributeNode($class); + } + + + public function __toString(): string { + # The ordered set serializer takes a set and returns the concatenation of set + # using U+0020 SPACE. + return implode(' ', $this->tokenSet); + } +} diff --git a/lib/Element.php b/lib/Element.php index 75272ee..3a0348b 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -22,6 +22,15 @@ class Element extends Node { 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. @@ -339,39 +348,6 @@ class Element extends Node { $this->innerNode->setAttributeNS($namespace, $qualifiedName, $value); } - /*# 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. - // NOTE: We create attribute nodes so that xmlns attributes don't get lost; - // otherwise they cannot be serialized - if ($namespace === self::XMLNS_NAMESPACE) { - // Xmlns attributes have special bugs just for them. How lucky! Xmlns attribute - // nodes won't stick and can actually cause segmentation faults if created on a - // no longer existing document element, appended to another element, and then - // retrieved. So, use the methods used in Document::createAttributeNS to get an - // attribute node. - $a = $this->ownerDocument->createAttributeNS($namespace, $qualifiedName); - - $a->value = $this->escapeString($value, true); - $this->setAttributeNodeNS($a); - } else { - try { - $this->innerNode->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 ($prefix !== null) { - $qualifiedName = $this->coerceName($prefix) . ':' . $this->coerceName($localName); - } else { - $qualifiedName = $this->coerceName($qualifiedName); - } - - $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') { diff --git a/tests/cases/TestDOMTokenList.php b/tests/cases/TestDOMTokenList.php new file mode 100644 index 0000000..44bcda4 --- /dev/null +++ b/tests/cases/TestDOMTokenList.php @@ -0,0 +1,224 @@ +createElement('html'); + $e->classList->add(''); + }, DOMException::SYNTAX_ERROR ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->remove(''); + }, DOMException::SYNTAX_ERROR ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->replace('ack', ''); + }, DOMException::SYNTAX_ERROR ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->toggle(''); + }, DOMException::SYNTAX_ERROR ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->add('fail fail'); + }, DOMException::INVALID_CHARACTER ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->remove('fail fail'); + }, DOMException::INVALID_CHARACTER ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->replace('ack', 'fail fail'); + }, DOMException::INVALID_CHARACTER ], + [ function() { + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->toggle('fail fail'); + }, DOMException::INVALID_CHARACTER ] + ]; + } + + /** @dataProvider provideMethod_add_remove_replace_toggle__errors */ + public function testMethod_add_remove_replace_toggle__errors(\Closure $closure, int $errorCode): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode($errorCode); + $closure(); + } + + + /** + * @covers \MensBeam\HTML\DOM\DOMTokenList::contains + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__toString + * @covers \MensBeam\HTML\DOM\DOMTokenList::add + * @covers \MensBeam\HTML\DOM\DOMTokenList::attributeChange + * @covers \MensBeam\HTML\DOM\DOMTokenList::parseOrderedSet + * @covers \MensBeam\HTML\DOM\DOMTokenList::update + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_classList + * @covers \MensBeam\HTML\DOM\Node::__construct + * @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\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 + * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty + */ + public function testMethod_contains() { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + $this->assertTrue($e->classList->contains('ack')); + $this->assertFalse($e->classList->contains('fail')); + } + + + public function testMethod_count(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + $this->assertSame(4, count($e->classList)); + } + + + public function testMethod_item(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + $this->assertNull($e->classList->item(42)); + } + + + public function testMethod_replace(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->setAttribute('class', 'ook eek ack ookeek'); + $this->assertTrue($e->classList->replace('ack', 'what')); + $this->assertSame('ook eek what ookeek', $e->classList->value); + $this->assertSame('ook eek what ookeek', $e->getAttribute('class')); + $this->assertFalse($e->classList->replace('fail', 'eekook')); + } + + + public function testMethod_remove(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->setAttribute('class', 'ook eek ack ookeek'); + $e->classList->remove('ack'); + $this->assertSame('ook eek ookeek', $e->classList->value); + $this->assertSame('ook eek ookeek', $e->getAttribute('class')); + } + + + public function testMethod_supports(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + $this->assertTrue($e->classList->supports('ack')); + } + + + public function testMethod_toggle(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->setAttribute('class', 'ook eek ack ookeek'); + $this->assertFalse($e->classList->toggle('ack')); + $this->assertSame('ook eek ookeek', $e->classList->value); + $this->assertSame('ook eek ookeek', $e->getAttribute('class')); + $this->assertTrue($e->classList->toggle('ack')); + $this->assertSame('ook eek ookeek ack', $e->classList->value); + $this->assertSame('ook eek ookeek ack', $e->getAttribute('class')); + $this->assertTrue($e->classList->toggle('ack', true)); + $this->assertSame('ook eek ookeek ack', $e->classList->value); + $this->assertSame('ook eek ookeek ack', $e->getAttribute('class')); + $this->assertFalse($e->classList->toggle('eekook', false)); + $this->assertSame('ook eek ookeek ack', $e->classList->value); + $this->assertSame('ook eek ookeek ack', $e->getAttribute('class')); + $this->assertTrue($e->classList->toggle('eekook', true)); + $this->assertSame('ook eek ookeek ack eekook', $e->classList->value); + $this->assertSame('ook eek ookeek ack eekook', $e->getAttribute('class')); + } + + + public function testProcess_iteration(): void { + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + + foreach ($e->classList as $key => $className) { + $this->assertSame($className, $e->classList[$key]); + // test offsetExists + $this->assertTrue(isset($e->classList[$key])); + } + } + + + public function testProperty_value(): void { + // Test it with and without an attached document element + $d = new Document(); + $e = $d->createElement('html'); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + $this->assertSame('ook eek ack ookeek', $e->classList->value); + $this->assertSame('ook eek ack ookeek', $e->getAttribute('class')); + $e->classList->value = 'omg wtf bbq lol zor bor xxx'; + $this->assertSame('lol', $e->classList[3]); + $this->assertSame('omg wtf bbq lol zor bor xxx', $e->classList->value); + $this->assertSame('omg wtf bbq lol zor bor xxx', $e->getAttribute('class')); + $e->classList->value = ''; + $this->assertSame('', $e->classList->value); + $this->assertSame('', $e->getAttribute('class')); + + $d = new Document(); + $e = $d->appendChild($d->createElement('html')); + $e->classList->add('ook', 'eek', 'ack', 'ookeek'); + $this->assertSame('ook eek ack ookeek', $e->classList->value); + $this->assertSame('ook eek ack ookeek', $e->getAttribute('class')); + $e->classList->value = 'omg wtf bbq lol zor bor xxx'; + $this->assertSame('lol', $e->classList[3]); + $this->assertSame('omg wtf bbq lol zor bor xxx', $e->classList->value); + $this->assertSame('omg wtf bbq lol zor bor xxx', $e->getAttribute('class')); + $e->classList->value = ''; + $this->assertSame('', $e->classList->value); + $this->assertSame('', $e->getAttribute('class')); + } +} \ No newline at end of file diff --git a/tests/cases/TestElement.php b/tests/cases/TestElement.php index 9ed00e5..bb89fd9 100644 --- a/tests/cases/TestElement.php +++ b/tests/cases/TestElement.php @@ -514,6 +514,76 @@ class TestElement extends \PHPUnit\Framework\TestCase { } + /** + * @covers \MensBeam\HTML\DOM\Element::__get_classList + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::__get_documentElement + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__get_length + * @covers \MensBeam\HTML\DOM\DOMTokenList::attributeChange + * @covers \MensBeam\HTML\DOM\DOMTokenList::parseOrderedSet + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @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 + * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty + */ + public function testProperty_classList() { + $d = new Document('', 'UTF-8'); + $documentElement = $d->documentElement; + $this->assertEquals(5, $documentElement->classList->length); + } + + + /** + * @covers \MensBeam\HTML\DOM\Element::__get_className + * @covers \MensBeam\HTML\DOM\Element::__set_className + * + * @covers \MensBeam\HTML\DOM\Attr::__get_value + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_namespaceURI + * @covers \MensBeam\HTML\DOM\Element::getAttribute + * @covers \MensBeam\HTML\DOM\Element::getAttributeNode + * @covers \MensBeam\HTML\DOM\Element::setAttribute + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument + * @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\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_className() { + $d = new Document(); + $documentElement = $d->appendChild($d->createElement('html')); + $documentElement->className = 'ook'; + $this->assertSame('ook', $documentElement->className); + } + + /** * @covers \MensBeam\HTML\DOM\Element::__get_id * @covers \MensBeam\HTML\DOM\Element::__set_id diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index a054222..d84e8d0 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -18,6 +18,7 @@ cases/TestDocument.php cases/TestDocumentOrElement.php + cases/TestDOMTokenList.php cases/TestElement.php cases/TestNode.php diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index 8b462ee..31012f1 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -1801,6 +1801,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index 8325ff3..e118f79 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -855,16 +855,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", "shasum": "" }, "require": { @@ -873,7 +873,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -902,7 +902,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" }, "funding": [ { @@ -918,20 +918,20 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2021-07-12T14:48:14+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.3.7", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ce7b20d69c66a20939d8952b617506a44d102130" + "reference": "661a7a6e085394f8513945669e31f7c1338a7e69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ce7b20d69c66a20939d8952b617506a44d102130", - "reference": "ce7b20d69c66a20939d8952b617506a44d102130", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/661a7a6e085394f8513945669e31f7c1338a7e69", + "reference": "661a7a6e085394f8513945669e31f7c1338a7e69", "shasum": "" }, "require": { @@ -987,7 +987,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.3.7" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.3.11" }, "funding": [ { @@ -1003,20 +1003,20 @@ "type": "tidelift" } ], - "time": "2021-08-04T21:20:46+00:00" + "time": "2021-11-17T12:16:12+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11" + "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/69fee1ad2332a7cbab3aca13591953da9cdb7a11", - "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", + "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", "shasum": "" }, "require": { @@ -1029,7 +1029,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1066,7 +1066,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.0" }, "funding": [ { @@ -1082,7 +1082,7 @@ "type": "tidelift" } ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2021-07-12T14:48:14+00:00" }, { "name": "symfony/filesystem", @@ -1697,16 +1697,16 @@ }, { "name": "symfony/process", - "version": "v5.3.7", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967" + "reference": "6c99204de85d04ca17f16c466fc61896960b0636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/38f26c7d6ed535217ea393e05634cb0b244a1967", - "reference": "38f26c7d6ed535217ea393e05634cb0b244a1967", + "url": "https://api.github.com/repos/symfony/process/zipball/6c99204de85d04ca17f16c466fc61896960b0636", + "reference": "6c99204de85d04ca17f16c466fc61896960b0636", "shasum": "" }, "require": { @@ -1739,7 +1739,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.3.7" + "source": "https://github.com/symfony/process/tree/v5.3.11" }, "funding": [ { @@ -1755,25 +1755,29 @@ "type": "tidelift" } ], - "time": "2021-08-04T21:20:46+00:00" + "time": "2021-11-17T12:16:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.4.0", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.1" + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "suggest": { "symfony/service-implementation": "" @@ -1781,7 +1785,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1818,7 +1822,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" }, "funding": [ { @@ -1834,7 +1838,7 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:43:52+00:00" + "time": "2021-11-04T16:48:04+00:00" }, { "name": "symfony/string", @@ -1921,16 +1925,16 @@ }, { "name": "symfony/yaml", - "version": "v5.3.6", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7" + "reference": "226638aa877bc4104e619a15f27d8141cd6b4e4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7", - "reference": "4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/226638aa877bc4104e619a15f27d8141cd6b4e4a", + "reference": "226638aa877bc4104e619a15f27d8141cd6b4e4a", "shasum": "" }, "require": { @@ -1976,7 +1980,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.3.6" + "source": "https://github.com/symfony/yaml/tree/v5.3.11" }, "funding": [ { @@ -1992,7 +1996,7 @@ "type": "tidelift" } ], - "time": "2021-07-29T06:20:01+00:00" + "time": "2021-11-20T16:42:42+00:00" } ], "packages-dev": [],