diff --git a/RoboFile.php b/RoboFile.php deleted file mode 100644 index 966c3d3..0000000 --- a/RoboFile.php +++ /dev/null @@ -1,150 +0,0 @@ -runTests(escapeshellarg(\PHP_BINARY), "typical", $args); - } - - /** Runs the full test suite - * - * This includes pedantic tests which may help to identify problems. - * See help for the "test" task for more details. - */ - public function testFull(array $args): Result { - return $this->runTests(escapeshellarg(\PHP_BINARY), "full", $args); - } - - /** - * Runs a quick subset of the test suite - * - * See help for the "test" task for more details. - */ - public function testQuick(array $args): Result { - return $this->runTests(escapeshellarg(\PHP_BINARY), "quick", $args); - } - - /** Manually updates the imported html5lib test suite */ - public function testUpdate(): Result { - $dir = BASE_TEST."html5lib-tests"; - if (is_dir($dir)) { - return $this->taskGitStack()->dir($dir)->pull()->run(); - } else { - return $this->taskGitStack()->cloneRepo("https://github.com/html5lib/html5lib-tests", $dir)->run(); - } - } - - /** Produces a code coverage report - * - * By default this task produces an HTML-format coverage report in - * tests/coverage/. Additional reports may be produced by passing - * arguments to this task as one would to PHPUnit. - */ - public function coverage(array $args): Result { - // run tests with code coverage reporting enabled - $exec = $this->findCoverageEngine(); - return $this->runTests($exec, "coverage", array_merge(["--coverage-html", BASE_TEST."coverage"], $args)); - } - - /** Produces a code coverage report, with redundant tests - * - * Depending on the environment, some tests that normally provide - * coverage may be skipped, while working alternatives are normally - * suppressed for reasons of time. This coverage report will try to - * run all tests which may cover code. - * - * See also help for the "coverage" task for more details. - */ - public function coverageFull(array $args): Result { - // run tests with code coverage reporting enabled - $exec = $this->findCoverageEngine(); - return $this->runTests($exec, "typical", array_merge(["--coverage-html", BASE_TEST."coverage"], $args)); - } - - protected function findCoverageEngine(): string { - $dir = rtrim(ini_get("extension_dir"), "/").\DIRECTORY_SEPARATOR; - $ext = IS_WIN ? "dll" : "so"; - $php = escapeshellarg(\PHP_BINARY); - $code = escapeshellarg(BASE."lib"); - if (extension_loaded("pcov")) { - return "$php -d opcache.enable_cli=0 -d pcov.enabled=1 -d pcov.directory=$code"; - } elseif (extension_loaded("xdebug")) { - return "$php -d opcache.enable_cli=0 -d xdebug.mode=coverage"; - } elseif (file_exists($dir."pcov.$ext")) { - return "$php -d opcache.enable_cli=0 -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code"; - } elseif (file_exists($dir."xdebug.$ext")) { - return "$php -d opcache.enable_cli=0 -d zend_extension=xdebug.$ext -d xdebug.mode=coverage"; - } else { - if (IS_WIN) { - $dbg = dirname(\PHP_BINARY)."\\phpdbg.exe"; - $dbg = file_exists($dbg) ? $dbg : ""; - } else { - $dbg = trim(`which phpdbg 2>/dev/null`); - } - if ($dbg) { - return escapeshellarg($dbg)." -qrr"; - } else { - return $php; - } - } - } - - protected function blackhole(bool $all = false): string { - $hole = IS_WIN ? "nul" : "/dev/null"; - return $all ? ">$hole 2>&1" : "2>$hole"; - } - - protected function runTests(string $executor, string $set, array $args) : Result { - switch ($set) { - case "typical": - $set = ["--exclude-group", "optional"]; - break; - case "quick": - $set = ["--exclude-group", "optional,slow"]; - break; - case "coverage": - $set = ["--exclude-group", "optional,coverageOptional"]; - break; - case "full": - $set = []; - break; - default: - throw new \Exception; - } - $execpath = norm(BASE."vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit"); - $confpath = realpath(BASE_TEST."phpunit.dist.xml") ?: norm(BASE_TEST."phpunit.xml"); - // clone the html5lib test suite if it's not already present - if (!is_dir(BASE_TEST."html5lib-tests")) { - $this->testUpdate(); - } - return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run(); - } -} \ No newline at end of file diff --git a/composer.json b/composer.json index 7462af5..8ce4733 100644 --- a/composer.json +++ b/composer.json @@ -1,22 +1,14 @@ { "name": "mensbeam/html-dom", - "description": "Modern DOM library written in PHP for HTML documents", - "type": "library", - "require": { - "php": ">=8.0.2", - "ext-dom": "*", - "mensbeam/html-parser": ">=1.2.1", - "symfony/css-selector": ">=5.3", - "mensbeam/getters-and-setters": ">=1.1" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.3", - "mikey179/vfsstream": "^1.6", - "nikic/php-parser": "^4.13" - }, - "scripts": { - "post-install-cmd": ["@composer bin all install"], - "post-update-cmd": ["@composer bin all update"] + "autoload": { + "psr-4": { + "MensBeam\\HTML\\DOM\\": [ + "lib/", + "lib/DOMException", + "lib/Exception", + "lib/HTMLElement" + ] + } }, "license": "MIT", "authors": [ @@ -31,27 +23,9 @@ "homepage": "https://jkingweb.ca/" } ], - "autoload": { - "psr-4": { - "MensBeam\\HTML\\DOM\\": [ - "lib/", - "lib/HTMLElement" - ] - } - }, - "autoload-dev": { - "psr-4": { - "MensBeam\\HTML\\DOM\\Test\\": "tests/lib/", - "MensBeam\\HTML\\DOM\\TestCase\\": [ - "tests/cases/", - "tests/cases/Document", - "tests/cases/Serializer" - ] - } - }, - "config": { - "allow-plugins": { - "bamarni/composer-bin-plugin": true - } + "require": { + "mensbeam/html-parser": "^1.3", + "symfony/css-selector": "^6.3", + "mensbeam/getters-and-setters": "^1.1" } } diff --git a/composer.lock b/composer.lock index d10c45b..6f10a26 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dc49ff7a1b170e6b1f368a4f5145543e", + "content-hash": "b4a731d7d5cb3bb8b0520670bb74f4c5", "packages": [ { "name": "mensbeam/getters-and-setters", @@ -51,16 +51,16 @@ }, { "name": "mensbeam/html-parser", - "version": "1.2.6", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/mensbeam/HTML-Parser.git", - "reference": "fa0591bd9ce241631894bda290cb99f30b7c288d" + "reference": "2b8a31ce472190013faab710f6f7ad41a486a740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mensbeam/HTML-Parser/zipball/fa0591bd9ce241631894bda290cb99f30b7c288d", - "reference": "fa0591bd9ce241631894bda290cb99f30b7c288d", + "url": "https://api.github.com/repos/mensbeam/HTML-Parser/zipball/2b8a31ce472190013faab710f6f7ad41a486a740", + "reference": "2b8a31ce472190013faab710f6f7ad41a486a740", "shasum": "" }, "require": { @@ -115,10 +115,9 @@ "parsing" ], "support": { - "issues": "https://github.com/mensbeam/HTML-Parser/issues", - "source": "https://github.com/mensbeam/HTML-Parser/tree/1.2.6" + "source": "https://github.com/mensbeam/HTML-Parser/tree/1.3.1" }, - "time": "2023-01-25T22:49:58+00:00" + "time": "2023-04-20T19:55:47+00:00" }, { "name": "mensbeam/intl", @@ -228,25 +227,25 @@ }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -275,22 +274,22 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "symfony/css-selector", - "version": "v6.2.5", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "bf1b9d4ad8b1cf0dbde8b08e0135a2f6259b9ba1" + "reference": "883d961421ab1709877c10ac99451632a3d6fa57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/bf1b9d4ad8b1cf0dbde8b08e0135a2f6259b9ba1", - "reference": "bf1b9d4ad8b1cf0dbde8b08e0135a2f6259b9ba1", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/883d961421ab1709877c10ac99451632a3d6fa57", + "reference": "883d961421ab1709877c10ac99451632a3d6fa57", "shasum": "" }, "require": { @@ -326,7 +325,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.2.5" + "source": "https://github.com/symfony/css-selector/tree/v6.3.2" }, "funding": [ { @@ -342,184 +341,16 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:38:09+00:00" - } - ], - "packages-dev": [ - { - "name": "bamarni/composer-bin-plugin", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/bamarni/composer-bin-plugin.git", - "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", - "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^2.0", - "php": "^7.2.5 || ^8.0" - }, - "require-dev": { - "composer/composer": "^2.0", - "ext-json": "*", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.5", - "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", - "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" - }, - "autoload": { - "psr-4": { - "Bamarni\\Composer\\Bin\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "No conflicts for your bin dependencies", - "keywords": [ - "composer", - "conflict", - "dependency", - "executable", - "isolation", - "tool" - ], - "support": { - "issues": "https://github.com/bamarni/composer-bin-plugin/issues", - "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2" - }, - "time": "2022-10-31T08:38:03+00:00" - }, - { - "name": "mikey179/vfsstream", - "version": "v1.6.11", - "source": { - "type": "git", - "url": "https://github.com/bovigo/vfsStream.git", - "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", - "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.5|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-0": { - "org\\bovigo\\vfs\\": "src/main/php" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Frank Kleine", - "homepage": "http://frankkleine.de/", - "role": "Developer" - } - ], - "description": "Virtual file system to mock the real file system in unit tests.", - "homepage": "http://vfs.bovigo.org/", - "support": { - "issues": "https://github.com/bovigo/vfsStream/issues", - "source": "https://github.com/bovigo/vfsStream/tree/master", - "wiki": "https://github.com/bovigo/vfsStream/wiki" - }, - "time": "2022-02-23T02:02:42+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.15.3", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" - }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2023-07-12T16:00:22+00:00" } ], + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": { - "php": ">=8.0.2", - "ext-dom": "*" - }, + "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/lib/ArgumentCountError.php b/lib/ArgumentCountError.php new file mode 100644 index 0000000..8c7ec82 --- /dev/null +++ b/lib/ArgumentCountError.php @@ -0,0 +1,11 @@ +_innerNode->ownerDocument->getWrapperNode($this->_innerNode->ownerElement); + /** @var MensBeam\HTML\DOM\Inner\Document $innerOwnerDocument */ + $innerOwnerDocument = $this->_innerNode->ownerDocument; + return $innerOwnerDocument->getWrapperNode($this->_innerNode->ownerElement); } protected function __get_prefix(): string { diff --git a/lib/CharacterData.php b/lib/CharacterData.php index 489c833..2ff7b4b 100644 --- a/lib/CharacterData.php +++ b/lib/CharacterData.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; +/** @property \DOMCharacterData $_innerNode */ abstract class CharacterData extends Node { use ChildNode, NonDocumentTypeChildNode; diff --git a/lib/ChildNode.php b/lib/ChildNode.php index b30802f..6a9fdb8 100644 --- a/lib/ChildNode.php +++ b/lib/ChildNode.php @@ -7,10 +7,6 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\Inner\{ - Document as InnerDocument, - Reflection -}; trait ChildNode { diff --git a/lib/DOMException.php b/lib/DOMException.php index 67635ac..71bd3a7 100644 --- a/lib/DOMException.php +++ b/lib/DOMException.php @@ -8,46 +8,4 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; - -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; - public const INVALID_CHARACTER = 5; - public const NO_MODIFICATION_ALLOWED = 7; - public const NOT_FOUND = 8; - public const NOT_SUPPORTED = 9; - public const IN_USE_ATTRIBUTE = 10; - public const SYNTAX_ERROR = 12; - public const INVALID_MODIFICATION = 13; - public const NAMESPACE_ERROR = 14; - public const INVALID_ACCESS = 15; - - public const FILE_NOT_FOUND = 301; - - - public function __construct(int $code, ...$args) { - self::$messages = array_replace(parent::$messages, [ - 1 => 'Invalid index size', - 3 => 'Hierarchy request error', - 4 => 'Supplied node does not belong to this document', - 5 => 'Invalid character', - 7 => 'Modification not allowed here', - 8 => 'Not found error', - 9 => 'Feature is not supported', - 10 => 'The attribute is in use', - 12 => 'Syntax error', - 13 => 'Invalid modification error', - 14 => 'Namespace error', - 15 => 'Invalid access error', - - 301 => 'File not found' - ]); - - parent::__construct($code, ...$args); - } -} +abstract class DOMException extends \Exception {} \ No newline at end of file diff --git a/lib/DOMException/HierarchyRequestError.php b/lib/DOMException/HierarchyRequestError.php new file mode 100644 index 0000000..5e70f6d --- /dev/null +++ b/lib/DOMException/HierarchyRequestError.php @@ -0,0 +1,15 @@ + $contentType ]); + Reflection::setProtectedProperties($document, [ '_contentType' => $contentType ]); # 8. Return document. return $document; @@ -95,7 +95,7 @@ class DOMImplementation { # To validate a qualifiedName, throw an "InvalidCharacterError" DOMException if # qualifiedName does not match the QName production. if (!preg_match(InnerDocument::QNAME_PRODUCTION_REGEX, $qualifiedName)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 2. Return a new doctype, with qualifiedName as its name, publicId as its @@ -145,7 +145,7 @@ class DOMImplementation { $documentElement->appendChild($doc->createElement('body')); # 8. doc’s origin is this’s associated document’s origin. - // Not necessary. No scripting in this implementation. + // DEVIATION: There is no scripting in this implementation. # 9. Return doc. return $doc; diff --git a/lib/DOMTokenList.php b/lib/DOMTokenList.php index ada7ce6..1b6a2d6 100644 --- a/lib/DOMTokenList.php +++ b/lib/DOMTokenList.php @@ -8,8 +8,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\HTML\Parser\Data, - MensBeam\GettersAndSetters, - MensBeam\HTML\DOM\Inner\Reflection; + MensBeam\GettersAndSetters; class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { @@ -95,13 +94,13 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { foreach ($tokens as $token) { # 1. If token is the empty string, then throw a "SyntaxError" DOMException. if ($token === '') { - throw new DOMException(DOMException::SYNTAX_ERROR); + throw new SyntaxError(); } # 2. If token contains any ASCII whitespace, then throw an # "InvalidCharacterError" DOMException. if (preg_match(Data::WHITESPACE_REGEX, $token)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } } @@ -176,13 +175,13 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { foreach ($tokens as $token) { # 1. If token is the empty string, then throw a "SyntaxError" DOMException. if ($token === '') { - throw new DOMException(DOMException::SYNTAX_ERROR); + throw new SyntaxError(); } # 2. If token contains any ASCII whitespace, then throw an # "InvalidCharacterError" DOMException. if (preg_match(Data::WHITESPACE_REGEX, $token)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } } @@ -210,13 +209,13 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { # 1. If either token or newToken is the empty string, then throw a "SyntaxError" # DOMException. if ($token === '' || $newToken === '') { - throw new DOMException(DOMException::SYNTAX_ERROR); + throw new SyntaxError(); } # 2. If either token or newToken contains any ASCII whitespace, then throw an # "InvalidCharacterError" DOMException. if (preg_match(Data::WHITESPACE_REGEX, $token) || preg_match(Data::WHITESPACE_REGEX, $newToken)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 3. If this’s token set does not contain token, then return false. @@ -268,13 +267,13 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { 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); + throw new SyntaxError(); } # 2. If token contains any ASCII whitespace, then throw an # "InvalidCharacterError" DOMException. if (preg_match(Data::WHITESPACE_REGEX, $token)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 3. If this’s token set[token] exists, then: diff --git a/lib/Document.php b/lib/Document.php index 3126208..26b8ccb 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -20,6 +20,7 @@ use MensBeam\HTML\Parser\{ }; +/** @property InnerDocument $_innerNode */ class Document extends Node implements \ArrayAccess { use DocumentOrElement, NonElementParentNode, ParentNode, XPathEvaluatorBase; @@ -45,7 +46,7 @@ class Document extends Node implements \ArrayAccess { if ($n !== null) { do { if ($n instanceof \DOMElement && $n->namespaceURI === null && ($n->nodeName === 'body' || $n->nodeName === 'frameset')) { - return $n->ownerDocument->getWrapperNode($n); + return $this->_innerNode->getWrapperNode($n); } } while ($n = $n->nextSibling); } @@ -328,19 +329,19 @@ class Document extends Node implements \ArrayAccess { $element = $title; } # 3. Otherwise: - else { + elseif ($head !== null) { # 1. Let element be the result of creating an element given the document # element's node document, title, and the HTML namespace. $element = $this->_innerNode->createElementNS(Node::SVG_NAMESPACE, 'title'); # 2. Append element to the head element. $head->appendChild($element); - } - # 4. String replace all with the given value within element. - // This is basically what textContent will do for us... - if ($element !== null) { - $element->textContent = $value; + # 4. String replace all with the given value within element. + // This is basically what textContent will do for us... + if ($element !== null) { + $element->textContent = $value; + } } } @@ -380,7 +381,7 @@ class Document extends Node implements \ArrayAccess { # # 1. If node is a document, then throw a "NotSupportedError" DOMException. if ($node instanceof Document) { - throw new DOMException(DOMException::NOT_SUPPORTED); + throw new NotSupportedError(); } # 2. If node is a shadow root, then throw a "HierarchyRequestError" DOMException. @@ -390,7 +391,7 @@ class Document extends Node implements \ArrayAccess { // DEVIATION: One can't just return here? if ($node instanceof DocumentFragment) { $host = Reflection::getProtectedProperty($node, 'host'); - if ($host !== null || $host->get() !== null) { + if ($host instanceof \WeakReference && $host->get() !== null || $host !== null) { return $node; } } @@ -419,7 +420,7 @@ class Document extends Node implements \ArrayAccess { # 1. If localName does not match the Name production in XML, then throw an # "InvalidCharacterError" DOMException. if (preg_match(InnerDocument::NAME_PRODUCTION_REGEX, $localName) !== 1) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 2. If this is an HTML document, then set localName to localName in ASCII @@ -509,13 +510,13 @@ class Document extends Node implements \ArrayAccess { # # 1. If this is an HTML document, then throw a "NotSupportedError" DOMException. if (!$this instanceof XMLDocument) { - throw new DOMException(DOMException::NOT_SUPPORTED); + throw new NotSupportedError(); } # 2. If data contains the string "]]>", then throw an "InvalidCharacterError" # DOMException. if (str_contains(needle: ']]>', haystack: $data)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 3. Return a new CDATASection node with its data set to data and node document @@ -539,12 +540,12 @@ class Document extends Node implements \ArrayAccess { # 1. If localName does not match the Name production, then throw an # "InvalidCharacterError" DOMException. if (!preg_match(InnerDocument::NAME_PRODUCTION_REGEX, $localName)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 2. If this is an HTML document, then set localName to localName in ASCII # lowercase. - if (!$this instanceof XMLElement) { + if (!$this instanceof XMLDocument) { $localName = strtolower($localName); } @@ -642,7 +643,7 @@ class Document extends Node implements \ArrayAccess { # DOMException. // Because this can import from PHP's DOM we must check for more stuff. if ($node instanceof Document || $node instanceof \DOMDocument || ($node instanceof \DOMNode && $node->ownerDocument::class !== 'DOMDocument') || $node instanceof \DOMEntityReference) { - throw new DOMException(DOMException::NOT_SUPPORTED); + throw new NotSupportedError(); } # 2. Return a clone of node, with this and the clone children flag set if deep @@ -652,7 +653,7 @@ class Document extends Node implements \ArrayAccess { public function load(string $source = null, ?string $charset = null): void { if ($this->hasChildNodes()) { - throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED); + throw new NoModificationAllowedError(); } $config = new ParserConfig(); @@ -675,7 +676,7 @@ class Document extends Node implements \ArrayAccess { public function loadFile(string $filename, ?string $charset = null): void { $f = @fopen($filename, 'r'); if (!$f) { - throw new DOMException(DOMException::FILE_NOT_FOUND); + throw new FileNotFoundException(); } $data = stream_get_contents($f); @@ -810,7 +811,7 @@ class Document extends Node implements \ArrayAccess { public function serialize(?Node $node = null, array $config = []): string { $node = $node ?? $this; if ($node !== $this && $node->ownerDocument !== $this) { - throw new DOMException(DOMException::WRONG_DOCUMENT); + throw new WrongDocumentError(); } return Serializer::serialize($node->innerNode, $config); @@ -819,7 +820,7 @@ class Document extends Node implements \ArrayAccess { public function serializeInner(?Node $node = null, array $config = []): string { $node = $node ?? $this; if ($node !== $this && $node->ownerDocument !== $this) { - throw new DOMException(DOMException::WRONG_DOCUMENT); + throw new WrongDocumentError(); } return Serializer::serializeInner($node->innerNode, $config); diff --git a/lib/DocumentOrElement.php b/lib/DocumentOrElement.php index 5d1bf58..681e3af 100644 --- a/lib/DocumentOrElement.php +++ b/lib/DocumentOrElement.php @@ -160,7 +160,7 @@ trait DocumentOrElement { # To validate a qualifiedName, throw an "InvalidCharacterError" DOMException if # qualifiedName does not match the QName production. if (preg_match(InnerDocument::QNAME_PRODUCTION_REGEX, $qualifiedName) !== 1) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 3. Let prefix be null. @@ -190,7 +190,7 @@ trait DocumentOrElement { (($qualifiedName === 'xmlns' || $prefix === 'xmlns') && $namespace !== self::XMLNS_NAMESPACE) || ($namespace === self::XMLNS_NAMESPACE && $qualifiedName !== 'xmlns' && $prefix !== 'xmlns') ) { - throw new DOMException(DOMException::NAMESPACE_ERROR); + throw new NamespaceError(); } # 10. Return namespace, prefix, and localName. diff --git a/lib/DocumentType.php b/lib/DocumentType.php index c233fdd..f2a70e0 100644 --- a/lib/DocumentType.php +++ b/lib/DocumentType.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; +/** @property \DOMDocumentType $_innerNode */ class DocumentType extends Node { use ChildNode; diff --git a/lib/Element.php b/lib/Element.php index d77a82a..8b58e4d 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -16,6 +16,7 @@ use MensBeam\HTML\Parser, Symfony\Component\CssSelector\Exception\SyntaxErrorException as SymfonySyntaxErrorException; +/** @property \DOMElement $_innerNode */ class Element extends Node { use ChildNode, DocumentOrElement, NonDocumentTypeChildNode, ParentNode; @@ -76,11 +77,13 @@ class Element extends Node { // object. $context = $this; $innerContext = $this->_innerNode; + /** @var InnerDocument */ + $innerOwnerDocument = $innerContext->ownerDocument; # 2. Let fragment be the result of invoking the fragment parsing algorithm with # the new value as markup, and with context element. $innerFragment = Parser::parseFragment($innerContext, Parser::NO_QUIRKS_MODE, $value, 'UTF-8'); - $fragment = $innerContext->ownerDocument->getWrapperNode($innerFragment); + $fragment = $innerOwnerDocument->getWrapperNode($innerFragment); $this->postParsingTemplatesFix($innerFragment); # 3. If the context object is a template element, then let context object be the @@ -136,6 +139,8 @@ class Element extends Node { # 1. Let parent be the context object's parent. $parent = $this->parentNode; $innerParent = $this->_innerNode->parentNode; + /** @var InnerDocument */ + $innerOwnerDocument = $this->_innerNode->ownerDocument; # 2. If parent is null, terminate these steps. There would be no way to obtain a # reference to the nodes created even if the remaining steps were run. @@ -145,7 +150,7 @@ class Element extends Node { # 3. If parent is a Document, throw a "NoModificationAllowedError" DOMException. if ($innerParent instanceof \DOMDocument) { - throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED); + throw new NoModificationAllowedError(); } # 4. If parent is a DocumentFragment, let parent be a new Element with: @@ -153,13 +158,13 @@ class Element extends Node { # • The HTML namespace as its namespace, and # • The context object's node document as its node document. if ($innerParent instanceof \DOMDocumentFragment) { - $innerParent = $this->_innerNode->ownerDocument->createElement('body'); + $innerParent = $innerOwnerDocument->createElement('body'); } # 5. Let fragment be the result of invoking the fragment parsing algorithm with # the new value as markup, and parent as the context element. $innerFragment = Parser::parseFragment($innerParent, Parser::NO_QUIRKS_MODE, $value, 'UTF-8'); - $fragment = $this->_innerNode->ownerDocument->getWrapperNode($innerFragment); + $fragment = $innerOwnerDocument->getWrapperNode($innerFragment); $this->postParsingTemplatesFix($innerFragment); # 6. Replace the context object with fragment within the context object's @@ -192,7 +197,7 @@ class Element extends Node { } catch (\Exception $e) { # 2. If s is failure, throw a "SyntaxError" DOMException. if ($e instanceof SymfonySyntaxErrorException) { - throw new DOMException(DOMException::SYNTAX_ERROR); + throw new SyntaxError(); } } @@ -276,6 +281,8 @@ class Element extends Node { } $qualifiedName = $this->coerceName($qualifiedName); + /** @var InnerDocument */ + $innerOwnerDocument = $this->_innerNode->ownerDocument; # 2. Return the first attribute in element’s attribute list whose qualified name is # qualifiedName; otherwise null. @@ -285,7 +292,7 @@ class Element extends Node { $attributes = $this->_innerNode->attributes; foreach ($attributes as $attr) { if ($attr->nodeName === $qualifiedName) { - return $this->_innerNode->ownerDocument->getWrapperNode($attr); + return $innerOwnerDocument->getWrapperNode($attr); } } @@ -307,6 +314,8 @@ class Element extends Node { } $localName = $this->coerceName($localName); + /** @var InnerDocument */ + $innerOwnerDocument = $this->_innerNode->ownerDocument; # 2. Return the attribute in element’s attribute list whose namespace is namespace # and local name is localName, if any; otherwise null. @@ -316,7 +325,7 @@ class Element extends Node { $attributes = $this->_innerNode->attributes; foreach ($attributes as $attr) { if ($attr->namespaceURI === $namespace && $attr->localName === $localName) { - return $this->_innerNode->ownerDocument->getWrapperNode($attr); + return $innerOwnerDocument->getWrapperNode($attr); } } @@ -419,7 +428,7 @@ class Element extends Node { # If context is null or a Document, throw a "NoModificationAllowedError" DOMException. if ($context === null || $context instanceof Document) { - throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED); + throw new NoModificationAllowedError(); } break; case 'afterbegin': @@ -428,7 +437,7 @@ class Element extends Node { $context = $this; $innerContext = $this->_innerNode; break; - default: throw new DOMException(DOMException::SYNTAX_ERROR); + default: throw new SyntaxError(); } # 2. If context is not an Element or the following are all true: @@ -495,7 +504,7 @@ class Element extends Node { } catch (\Exception $e) { # 2. If s is failure, throw a "SyntaxError" DOMException. if ($e instanceof SymfonySyntaxErrorException) { - throw new DOMException(DOMException::SYNTAX_ERROR); + throw new SyntaxError(); } } @@ -503,9 +512,11 @@ class Element extends Node { # :scope element this, returns success, then return true; otherwise, return # false. [SELECTORS4] $innerNode = $this->_innerNode; + /** @var InnerDocument */ + $innerOwnerDocument = $innerNode->ownerDocument; // Query the parent as the context node, yes. This is due to how the XPath // queries are generated from Symfony. - return ($innerNode->ownerDocument->xpath->query($s, $innerNode->parentNode)->item(0) === $innerNode); + return ($innerOwnerDocument->xpath->query($s, $innerNode->parentNode)->item(0) === $innerNode); } public function removeAttribute(string $qualifiedName): void { @@ -527,14 +538,20 @@ class Element extends Node { # The removeAttributeNode(attr) method steps are: # 1. If this’s attribute list does not contain attr, then throw a # "NotFoundError" DOMException. - // PHP's DOM does this already. Will catch its exception and rethrow as HTML-DOM + // PHP's DOM does this already. Will catch its exceptions and rethrow as HTML-DOM // DOMException. # 2. Remove attr. try { $this->_innerNode->removeAttributeNode($attr->innerNode); } catch (\DOMException $e) { - throw new DOMException($e->code); + switch ($e->getCode()) { + case 8: throw new NotFoundError(); + break; + case 9: throw new NotSupportedError(); + break; + default: throw new UnknownException(); + } } # 3. Return attr. @@ -560,7 +577,7 @@ class Element extends Node { # 1. If qualifiedName does not match the Name production in XML, then throw an # "InvalidCharacterError" DOMException. if (!preg_match(InnerDocument::NAME_PRODUCTION_REGEX, $qualifiedName)) { - throw new DOMException(DOMException::INVALID_CHARACTER); + throw new InvalidCharacterError(); } # 2. If this is in the HTML namespace and its node document is an HTML document, @@ -603,7 +620,7 @@ class Element extends Node { # "InUseAttributeError" DOMException. $ownerElement = $attr->ownerElement; if ($ownerElement !== null && $ownerElement !== $this) { - throw new DOMException(DOMException::IN_USE_ATTRIBUTE); + throw new InUseAttributeError(); } // PHP's DOM doesn't do this method correctly. It returns the old node if it is @@ -756,7 +773,7 @@ class Element extends Node { return $element->parentNode->insertBefore($node, $element->nextSibling); break; - default: throw new DOMException(DOMException::SYNTAX_ERROR); + default: throw new SyntaxError(); } } } \ No newline at end of file diff --git a/lib/Exception.php b/lib/Exception.php deleted file mode 100644 index 7dcb04a..0000000 --- a/lib/Exception.php +++ /dev/null @@ -1,73 +0,0 @@ - 'Invalid error code', - 101 => 'Unknown error; escaping', - 102 => 'Incorrect number of parameters for Exception message; %s expected', - 103 => 'Unreachable code', - - 201 => 'Property %s does not exist', - 202 => 'Cannot write readonly property %s', - 203 => 'Argument #%s ($%s) must be of type %s, %s given', - 204 => 'Call to undefined method %s::%s()', - 205 => 'Function should return type %s, %s given' - ]; - - public function __construct(int $code, ...$args) { - if (!isset(self::$messages[$code])) { - throw new Exception(Exception::INVALID_CODE); - } - - $message = self::$messages[$code]; - $previous = null; - - // @codeCoverageIgnoreStart - if ($args) { - // Grab a previous exception if there is one. - if ($args[0] instanceof \Throwable) { - $previous = array_shift($args); - } elseif (end($args) instanceof \Throwable) { - $previous = array_pop($args); - } - } - // @codeCoverageIgnoreEnd - - // Count the number of replacements needed in the message. - preg_match_all('/(\%(?:\d+\$)?s)/', $message, $matches); - $count = count($matches[1]); - - // If the number of replacements don't match the arguments then oops. - if (count($args) !== $count) { - throw new Exception(Exception::INCORRECT_PARAMETERS_FOR_MESSAGE, $count); - } - - if ($count > 0) { - // Go through each of the arguments and run sprintf on the strings. - $message = call_user_func_array('sprintf', array_merge([$message], $args)); - } - - parent::__construct($message, $code, $previous); - } -} diff --git a/lib/FileNotFoundException.php b/lib/FileNotFoundException.php new file mode 100644 index 0000000..dd175a0 --- /dev/null +++ b/lib/FileNotFoundException.php @@ -0,0 +1,15 @@ +setAttribute('contenteditable', $value); break; - default: throw new DOMException(DOMException::SYNTAX_ERROR); + default: throw new SyntaxError(); } } @@ -225,9 +225,10 @@ class HTMLElement extends Element { } $n = $this->_innerNode; + /** @var InnerDocument */ $doc = $n->ownerDocument; while ($n = $n->parentNode) { - if ($doc->getWrapperNode($n) instanceof HTMLElement && $n->hasAttribute('hidden')) { + if ($doc->getWrapperNode($n) instanceof HTMLElement && $n instanceof \DOMElement && $n->hasAttribute('hidden')) { return true; } } @@ -334,7 +335,9 @@ class HTMLElement extends Element { $n = $this->_innerNode; while ($n = $n->parentNode) { if ($n instanceof \DOMElement) { - if ($n->getAttribute('contenteditable') === 'true' && $n->ownerDocument->getWrapperNode($n) instanceof HTMLElement) { + /** @var InnerDocument */ + $ownerDocument = $n->ownerDocument; + if ($n->getAttribute('contenteditable') === 'true' && $ownerDocument->getWrapperNode($n) instanceof HTMLElement) { return true; } } @@ -366,7 +369,7 @@ class HTMLElement extends Element { # DOMException. $innerNode = $this->_innerNode; if ($this->parentNode === null) { - throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED); + throw new NoModificationAllowedError(); } # 2. Let next be this's next sibling. @@ -391,9 +394,14 @@ class HTMLElement extends Element { # 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: + // This gets confusing because this is a process that's in another section of + // the spec, but in this situation _node_ is next's previous sibling, so _node's + // next sibling_ will be next. # 1. Let next be node's next sibling. # 2. If next is not a Text node, then return. - // Already checked for + if (!$next instanceof \DOMText) { + return; + } # 3. Replace data with node, node's data's length, 0, and next's data. $next->previousSibling->data .= $next->data; @@ -476,11 +484,13 @@ class HTMLElement extends Element { } $n = $this->_innerNode; + /** @var InnerDocument */ $doc = $n->ownerDocument; while ($n = $n->parentNode) { // This looks weird but it's faster to check for the method here first because // getting a wrapper node causes a wrapper element to be created if it doesn't // already exist. Don't want to create unnecessary wrappers. + /** @var \DOMElement $n */ if (method_exists($n, 'getAttribute') && $n->getAttribute('translate') === 'yes' && $doc->getWrapperNode($n) instanceof HTMLElement) { return true; } @@ -519,7 +529,9 @@ class HTMLElement extends Element { $n = $element; while ($n = $n->parentNode) { - if ($n->tagName === 'form' && $n->ownerDocument->getWrapperNode($n) instanceof HTMLElement) { + /** @var InnerDocumente */ + $ownerDocument = $n->ownerDocument; + if ($ownerDocument->getWrapperNode($n) instanceof HTMLElement && $n instanceof \DOMElement && $n->tagName === 'form') { return $this->autoCapitalizationHint($n); } } diff --git a/lib/IOException.php b/lib/IOException.php new file mode 100644 index 0000000..46ed791 --- /dev/null +++ b/lib/IOException.php @@ -0,0 +1,11 @@ +ownerDocument !== $this) { - throw new DOMException(DOMException::WRONG_DOCUMENT); + throw new WrongDocumentError(); } // If the wrapper node already exists then return that. diff --git a/lib/NamedNodeMap.php b/lib/NamedNodeMap.php index 2ed616b..e2309f6 100644 --- a/lib/NamedNodeMap.php +++ b/lib/NamedNodeMap.php @@ -7,11 +7,8 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\Inner\{ - Document as InnerDocument, - Reflection -}; -use MensBeam\HTML\Parser\NameCoercion; +use MensBeam\HTML\DOM\Inner\Document as InnerDocument, + MensBeam\HTML\Parser\NameCoercion; class NamedNodeMap extends Collection { @@ -124,7 +121,7 @@ class NamedNodeMap extends Collection { # 2. If attr is null, then throw a "NotFoundError" DOMException. if ($attr === null) { - throw new DOMException(DOMException::NOT_FOUND); + throw new NotFoundError(); } # 3. Return attr. diff --git a/lib/Node.php b/lib/Node.php index 606a86b..8d62161 100644 --- a/lib/Node.php +++ b/lib/Node.php @@ -8,7 +8,6 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\GettersAndSetters, - MensBeam\HTML\Parser, MensBeam\HTML\Parser\NameCoercion; use MensBeam\HTML\DOM\Inner\{ Document as InnerDocument, @@ -227,11 +226,12 @@ abstract class Node implements \Stringable { # otherwise this’s node document. // PHP's DOM does this correctly on everything but document types. That's taken // care of in DocumentType. - if ($this instanceof Document || !$ownerDocument = $this->_innerNode->ownerDocument) { + /** @var InnerDocument $ownerDocument */ + if ($this instanceof Document || !($ownerDocument = $this->_innerNode->ownerDocument)) { return null; } - return $this->_innerNode->ownerDocument->getWrapperNode($ownerDocument); + return $ownerDocument->getWrapperNode($ownerDocument); } protected function __get_parentElement(): ?Element { @@ -376,6 +376,7 @@ abstract class Node implements \Stringable { # 5. If node2 is an attribute, then: if ($node2 instanceof Attr) { # 1. Set attr2 to node2 and node2 to attr2’s element. + /** @var \DOMAttr $innerNode2 */ $attr2 = $innerNode2; $node2 = $attr2->ownerElement; $innerNode2 = $innerNode2->ownerElement; @@ -480,7 +481,13 @@ abstract class Node implements \Stringable { while ($n = $n->parentNode) { $root = $n; } - return (!$root instanceof InnerDocument) ? $root->ownerDocument->getWrapperNode($root) : $root->wrapperNode; + + if (!$root instanceof InnerDocument) { + /** @var Node $root */ + return $root->ownerDocument->getWrapperNode($root); + } + + return $root->wrapperNode; } public function hasChildNodes(): bool { @@ -612,7 +619,7 @@ abstract class Node implements \Stringable { # 1. If parent is not a Document, DocumentFragment, or Element node, then throw # a "HierarchyRequestError" DOMException. if (!$inner instanceof InnerDocument && !$inner instanceof \DOMDocumentFragment && !$inner instanceof \DOMElement) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 2. If node is a host-including inclusive ancestor of parent, then throw a @@ -621,25 +628,25 @@ abstract class Node implements \Stringable { // host-including inclusive ancestor of child, but it should. All browsers check // for this. if ($this->containsInner($node, $inner) || $this->containsInner($node, $child)) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 3. If child’s parent is not parent, then throw a "NotFoundError" DOMException. if ($child->parentNode !== $inner) { - throw new DOMException(DOMException::NOT_FOUND); + throw new NotFoundError(); } # 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData # node, then throw a "HierarchyRequestError" DOMException. if (!$node instanceof \DOMDocumentFragment && !$node instanceof \DOMDocumentType && !$node instanceof \DOMElement && !$node instanceof \DOMCharacterData) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 5. If either node is a Text node and parent is a document, or node is a # doctype and parent is not a document, then throw a "HierarchyRequestError" # DOMException. if (($node instanceof \DOMText && $inner instanceof InnerDocument) || ($node instanceof \DOMDocumentType && !$inner instanceof InnerDocument)) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 6. If parent is a document, and any of the statements below, switched on the @@ -653,14 +660,14 @@ abstract class Node implements \Stringable { if ($node instanceof \DOMDocumentFragment) { $nodeChildElementCount = $node->childElementCount; if ($nodeChildElementCount > 1) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } $n = $node->firstChild; if ($n !== null) { do { if ($n instanceof \DOMText) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } while ($n = $n->nextSibling); } @@ -671,7 +678,7 @@ abstract class Node implements \Stringable { $beforeChild = ($n !== $child); do { if (($n instanceof \DOMElement && $n !== $child) || (!$beforeChild && $n instanceof \DOMDocumentType)) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } elseif ($n === $child) { $beforeChild = false; } @@ -689,7 +696,7 @@ abstract class Node implements \Stringable { $beforeChild = ($n !== $child); do { if (($n instanceof \DOMElement && $n !== $child) || (!$beforeChild && $n instanceof \DOMDocumentType)) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } elseif ($n === $child) { $beforeChild = false; } @@ -706,7 +713,7 @@ abstract class Node implements \Stringable { $beforeChild = ($n !== $child); do { if (($n instanceof \DOMDocumentType && $n !== $child) || $beforeChild && $n instanceof \DOMElement) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } elseif ($n === $child) { $beforeChild = false; } @@ -824,14 +831,18 @@ abstract class Node implements \Stringable { # with the clone children flag set. # 3. Append copied contents to copy's template contents. // Template contents are stored in the wrapper nodes. - $copyWrapperContent = $copy->ownerDocument->getWrapperNode($copy)->content; + /** @var InnerDocument */ + $copyOwnerDocument = $copy->ownerDocument; + $copyWrapperContent = $copyOwnerDocument->getWrapperNode($copy)->content; // If the cloning is called for as a result of parsing serialized markup the // contents of the node should be appended to the wrapper element's content // document fragment. Otherwise, clone the content document fragment instead. if (!$parsing) { $copyWrapperContentInner = $copyWrapperContent->innerNode; - $nodeWrapperContent = $node->ownerDocument->getWrapperNode($node)->content->innerNode; + /** @var InnerDocument */ + $nodeWrapperOwnerDocument = $node->ownerDocument; + $nodeWrapperContent = $nodeWrapperOwnerDocument->getWrapperNode($node)->content->innerNode; $childNodes = $nodeWrapperContent->childNodes; if ($childNodes->length > 0) { // This garbage is necessary because the appendChildInner method @@ -1063,6 +1074,7 @@ abstract class Node implements \Stringable { # ↪ DocumentType # Its name, public ID, and system ID. if ($thisNode instanceof \DOMDocumentType) { + /** @var \DOMDocumentType $otherNode */ if ($thisNode->name !== $otherNode->name || $thisNode->publicId !== $otherNode->publicId || $thisNode->systemId !== $thisNode->publicId) { return false; } @@ -1070,6 +1082,7 @@ abstract class Node implements \Stringable { # ↪ Element # Its namespace, namespace prefix, local name, and its attribute list’s size. elseif ($thisNode instanceof \DOMElement) { + /** @var \DOMElement $otherNode */ $otherAttributes = $otherNode->attributes; if ($thisNode->namespaceURI !== $otherNode->namespaceURI || $thisNode->prefix !== $otherNode->prefix || $thisNode->localName !== $otherNode->localName || $thisNode->attributes->length !== $otherAttributes->length) { return false; @@ -1091,6 +1104,7 @@ abstract class Node implements \Stringable { # ↪ Attr # Its namespace, local name, and value. elseif ($thisNode instanceof \DOMAttr) { + /** @var \DOMAttr $otherNode */ if ($thisNode->namespaceURI !== $otherNode->namespaceURI || $thisNode->localName !== $otherNode->localName || $thisNode->value !== $otherNode->value) { return false; } @@ -1099,12 +1113,14 @@ abstract class Node implements \Stringable { # ↪ Comment # Its data. elseif ($thisNode instanceof \DOMText || $thisNode instanceof \DOMComment) { + /** @var \DOMText|\DOMComment $otherNode */ if ($thisNode->data !== $otherNode->data) { return false; } } if ($thisNode instanceof \DOMDocument || $thisNode instanceof \DOMDocumentFragment || $thisNode instanceof \DOMElement) { + /** @var \DOMDocument|\DOMDocumentFragment|\DOMElement $otherNode */ # • A and B have the same number of children. if ($thisNode->childNodes->length !== $otherNode->childNodes->length) { return false; @@ -1129,7 +1145,9 @@ abstract class Node implements \Stringable { # ↪ Element if ($node instanceof \DOMElement) { // Work around PHP DOM HTML namespace bug - if ($node->namespaceURI === null && !$node->ownerDocument->getWrapperNode($node->ownerDocument) instanceof XMLDocument) { + /** @var \InnerDocument */ + $ownerDocument = $node->ownerDocument; + if ($node->namespaceURI === null && !$ownerDocument->getWrapperNode($ownerDocument) instanceof XMLDocument) { $namespace = self::HTML_NAMESPACE; } else { $namespace = $node->namespaceURI; @@ -1289,7 +1307,9 @@ abstract class Node implements \Stringable { // below walks through this node and temporarily replaces foreign descendants // with bullshit elements which are then replaced once the node is inserted. if ($element->namespaceURI === null && ($this instanceof DocumentFragment || $this->getRootNode() !== null) && $element->hasChildNodes()) { - $foreign = $element->ownerDocument->xpath->query('.//*[parent::*[namespace-uri()=""] and not(namespace-uri()="") and name()=local-name()]', $element); + /** @var InnerDocument */ + $ownerDocument = $element->ownerDocument; + $foreign = $ownerDocument->xpath->query('.//*[parent::*[namespace-uri()=""] and not(namespace-uri()="") and name()=local-name()]', $element); $this->bullshitReplacements = []; if ($foreign->length > 0) { $count = 0; @@ -1317,7 +1337,7 @@ abstract class Node implements \Stringable { # 1. If parent is not a Document, DocumentFragment, or Element node, then throw # a "HierarchyRequestError" Exception. if (!$parent instanceof InnerDocument && !$parent instanceof \DOMDocumentFragment && !$parent instanceof \DOMElement) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 2. If node is a host-including inclusive ancestor of parent, then throw a @@ -1328,7 +1348,7 @@ abstract class Node implements \Stringable { # host-including inclusive ancestor of B’s root’s host. if ($node->parentNode !== null) { if ($parent->parentNode !== null && ($parent === $node || $this->containsInner($node, $parent))) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } else { $n = $parent; do { @@ -1336,10 +1356,12 @@ abstract class Node implements \Stringable { } while ($n = $n->parentNode); if ($parentRoot instanceof \DOMDocumentFragment) { - $wrappedParentRoot = $parentRoot->ownerDocument->getWrapperNode($parentRoot); + /** @var InnerDocument */ + $ownerDocument = $parentRoot->ownerDocument; + $wrappedParentRoot = $ownerDocument->getWrapperNode($parentRoot); $parentRootHost = Reflection::getProtectedProperty($wrappedParentRoot, 'host'); if ($parentRootHost !== null && ($parentRootHost === $node || $this->containsInner($node, $parentRootHost->get()->innerNode))) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } } @@ -1348,21 +1370,21 @@ abstract class Node implements \Stringable { # 3. If child is non-null and its parent is not parent, then throw a # "NotFoundError" Exception. if ($child !== null && ($child->parentNode === null || $child->parentNode !== $parent)) { - throw new DOMException(DOMException::NOT_FOUND); + throw new NotFoundError(); } # 4. If node is not a DocumentFragment, DocumentType, Element, Text, # ProcessingInstruction, or Comment node, then throw a "HierarchyRequestError" # Exception. if (!$node instanceof \DOMDocumentFragment && !$node instanceof \DOMDocumentType && !$node instanceof \DOMElement && !$node instanceof \DOMText && !$node instanceof \DOMProcessingInstruction && !$node instanceof \DOMComment) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 5. If either node is a Text node and parent is a document, or node is a # doctype and parent is not a document, then throw a "HierarchyRequestError" # Exception. if (($node instanceof \DOMText && $parent instanceof \DOMDocument) || ($node instanceof \DOMDocumentType && !$parent instanceof InnerDocument)) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } # 6. If parent is a document, and any of the statements below, switched on the @@ -1377,13 +1399,13 @@ abstract class Node implements \Stringable { if ($node instanceof \DOMDocumentFragment) { $nodeChildElementCount = $node->childElementCount; if ($nodeChildElementCount > 1) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } else { $n = $node->firstChild; if ($n !== null) { do { if ($n instanceof \DOMText) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } while ($n = $n->nextSibling); } @@ -1391,14 +1413,14 @@ abstract class Node implements \Stringable { if ($nodeChildElementCount === 1) { if ($parent->childElementCount > 0 || $child instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } if ($child !== null) { $n = $child; while ($n = $n->nextSibling) { if ($n instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } } @@ -1410,14 +1432,14 @@ abstract class Node implements \Stringable { # doctype is following child. elseif ($node instanceof \DOMElement) { if ($child instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } if ($child !== null) { $n = $child; while ($n = $n->nextSibling) { if ($n instanceof \DOMDocumentType) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } } @@ -1426,7 +1448,7 @@ abstract class Node implements \Stringable { $n = $parent->firstChild; do { if ($n instanceof \DOMElement) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } while ($n = $n->nextSibling); } @@ -1441,7 +1463,7 @@ abstract class Node implements \Stringable { $n = $firstChild; do { if ($n instanceof \DOMDocumentType || ($child === null && $n instanceof \DOMElement)) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } while ($n = $n->nextSibling); } @@ -1450,7 +1472,7 @@ abstract class Node implements \Stringable { $n = $child; while ($n = $n->previousSibling) { if ($n instanceof \DOMElement) { - throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR); + throw new HierarchyRequestError(); } } } diff --git a/lib/NonDocumentTypeChildNode.php b/lib/NonDocumentTypeChildNode.php index 9bbc256..5cd5285 100644 --- a/lib/NonDocumentTypeChildNode.php +++ b/lib/NonDocumentTypeChildNode.php @@ -7,10 +7,6 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\Inner\{ - Document as InnerDocument, - Reflection -}; trait NonDocumentTypeChildNode { diff --git a/lib/NonElementParentNode.php b/lib/NonElementParentNode.php index 4d59902..ed508a9 100644 --- a/lib/NonElementParentNode.php +++ b/lib/NonElementParentNode.php @@ -7,11 +7,6 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -use MensBeam\HTML\DOM\Inner\{ - Document as InnerDocument, - Reflection -}; -use MensBeam\HTML\Parser; trait NonElementParentNode { diff --git a/lib/OutOfBoundsException.php b/lib/OutOfBoundsException.php new file mode 100644 index 0000000..7187e2d --- /dev/null +++ b/lib/OutOfBoundsException.php @@ -0,0 +1,11 @@ +parentNode !== null && $node->hasChildNodes()) { @@ -146,7 +143,7 @@ trait ParentNode { // so only throw exception when an actual syntax error, otherwise return an // empty nodelist. if ($e instanceof SymfonySyntaxErrorException) { - throw new DOMException(DOMException::SYNTAX_ERROR); + throw new SyntaxError(); } return new \DOMNodeList; diff --git a/lib/ProcessingInstruction.php b/lib/ProcessingInstruction.php index 8dc305c..f0b430b 100644 --- a/lib/ProcessingInstruction.php +++ b/lib/ProcessingInstruction.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; +/** @property \DOMProcessingInstruction $_innerNode */ class ProcessingInstruction extends CharacterData { protected function __get_target(): string { // Need to uncoerce string if necessary. diff --git a/lib/Serializer.php b/lib/Serializer.php index 4699ea2..fc008c0 100644 --- a/lib/Serializer.php +++ b/lib/Serializer.php @@ -8,20 +8,21 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; use MensBeam\HTML\DOM\Inner\Reflection, - MensBeam\HTML\Parser; -use MensBeam\HTML\Parser\{ - Config, - Serializer as ParserSerializer -}; + MensBeam\HTML\Parser, + MensBeam\HTML\Parser\Serializer as ParserSerializer; class Serializer extends ParserSerializer { protected static function fragmentHasHost(\DOMDocumentFragment $fragment): bool { - return (Reflection::getProtectedProperty($fragment->ownerDocument->getWrapperNode($fragment), 'host') !== null); + /** @var InnerDocument */ + $ownerDocument = $fragment->ownerDocument; + return (Reflection::getProtectedProperty($ownerDocument->getWrapperNode($fragment), 'host') !== null); } protected static function getTemplateContent(\DOMElement $node): \DOMNode { - return $node->ownerDocument->getWrapperNode($node)->content->innerNode; + /** @var InnerDocument */ + $ownerDocument = $node->ownerDocument; + return $ownerDocument->getWrapperNode($node)->content->innerNode; } protected static function isPreformattedContent(\DOMNode $node): bool { @@ -32,7 +33,9 @@ class Serializer extends ParserSerializer { return true; } } elseif ($n instanceof \DOMDocumentFragment) { - $host = Reflection::getProtectedProperty($node->ownerDocument->getWrapperNode($n), 'host'); + /** @var InnerDocument */ + $ownerDocument = $node->ownerDocument; + $host = Reflection::getProtectedProperty($ownerDocument->getWrapperNode($n), 'host'); if ($host !== null) { $n = $host->get()->innerNode; } @@ -43,6 +46,7 @@ class Serializer extends ParserSerializer { } protected static function treatAsBlockWithTemplates(\DOMNode $node): bool { + /** @var InnerDocument */ $document = $node->ownerDocument; $xpath = $document->xpath; $templates = $xpath->query('.//template[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"][not(ancestor::iframe[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::listing[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::noembed[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::noframes[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::noscript[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::plaintext[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::pre[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::style[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::script[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::textarea[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::title[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"] or ancestor::xmp[namespace-uri()="" or namespace-uri()="http://www.w3.org/1999/xhtml"])]', $node); diff --git a/lib/Text.php b/lib/Text.php index 0538f52..5586f03 100644 --- a/lib/Text.php +++ b/lib/Text.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; +/** @property \DOMText $_innerNode */ class Text extends CharacterData { protected function __get_wholeText(): string { // PHP's DOM does this correctly already. @@ -19,11 +20,21 @@ class Text extends CharacterData { public function splitText(int $offset): Text { // PHP DOM mostly handles this correctly with the exception of not throwing an // exception when the offset is greater than the length, so let's fix that. - if ($offset > $this->length) { - throw new DOMException(DOMException::INDEX_SIZE_ERROR); + // DEVIATION?: All browsers error when supplying negative numbers here, so let's check for that, too. + if ($offset < 0 || $offset > $this->length) { + # 2. If offset is greater than length, then throw an "IndexSizeError" DOMException. + // DEVIATION?: WebIDL standard + // (https://webidl.spec.whatwg.org/#idl-DOMException-error-names) says + // IndexSizeError is deprecated and to use RangeError instead which isn't a + // DOMException but specified to be an analog of ECMAScript's built-in errors. + // Because of this we're going to use OutOfBoundsException which is PHP's + // equivalent. + throw new OutOfBoundsException(sprintf('Offset %s is outside of Text node\'s range of %s to %s', $offset, 0, $this->length)); } - return $this->_innerNode->ownerDocument->getWrapperNode($this->_innerNode->splitText($offset)); + /** @var \MensBeam\HTML\DOM\Inner\Document */ + $ownerDocument = $this->_innerNode->ownerDocument; + return $ownerDocument->getWrapperNode($this->_innerNode->splitText($offset)); } diff --git a/lib/UnknownException.php b/lib/UnknownException.php new file mode 100644 index 0000000..d4a5ace --- /dev/null +++ b/lib/UnknownException.php @@ -0,0 +1,15 @@ +hasChildNodes()) { - throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED); + throw new NoModificationAllowedError(); } $this->_innerNode->encoding = Charset::fromCharset((string)$charset) ?? 'UTF-8'; diff --git a/lib/XPathEvaluate.php b/lib/XPathEvaluate.php index 073daad..8c0c66d 100644 --- a/lib/XPathEvaluate.php +++ b/lib/XPathEvaluate.php @@ -82,7 +82,7 @@ trait XPathEvaluate { $resultType = XPathResult::ORDERED_NODE_ITERATOR_TYPE; break; default: - throw new DOMException(DOMException::NOT_SUPPORTED); + throw new NotSupportedError(); } } else { switch ($type) { @@ -148,7 +148,7 @@ trait XPathEvaluate { $result = $result->item(0); break; - default: throw new DOMException(DOMException::NOT_SUPPORTED); + default: throw new NotSupportedError(); } $resultType = $type; diff --git a/lib/XPathException.php b/lib/XPathException.php index 7165751..64e7348 100644 --- a/lib/XPathException.php +++ b/lib/XPathException.php @@ -9,18 +9,22 @@ declare(strict_types=1); namespace MensBeam\HTML\DOM; -class XPathException extends Exception { +class XPathException extends \Exception { public const INVALID_EXPRESSION = 51; public const TYPE_ERROR = 52; public const UNRESOLVABLE_NAMESPACE_PREFIX = 53; - public function __construct(int $code, ...$args) { - self::$messages = array_replace(parent::$messages, [ - 51 => 'Invalid expression error', - 52 => 'Expression cannot be converted to the specified type', - 53 => 'Unresolvable namespace prefix' - ]); + public function __construct(int $code = 0, ?\Throwable $previous = null) { + switch ($code) { + case self::INVALID_EXPRESSION: $message = 'Invalid expression error'; + break; + case self::TYPE_ERROR: $message = 'Expression cannot be converted to the specified type'; + break; + case self::UNRESOLVABLE_NAMESPACE_PREFIX: $message = 'Unresolvable namespace prefix'; + break; + default: throw new UnknownException(); + } - parent::__construct($code, ...$args); + parent::__construct($message, $code, $previous); } } diff --git a/robo b/robo deleted file mode 100755 index e096c36..0000000 --- a/robo +++ /dev/null @@ -1,14 +0,0 @@ -#! /bin/sh -base=`dirname "$0"` -roboCommand="$1" -if [ $# -eq 0 ]; then - "$base/vendor/bin/robo" -else - shift - ulimit -n 2048 - if [ "$1" = "clean" ]; then - "$base/vendor/bin/robo" "$roboCommand" "$@" - else - "$base/vendor/bin/robo" "$roboCommand" -- "$@" - fi -fi diff --git a/robo.bat b/robo.bat deleted file mode 100644 index d50b8a7..0000000 --- a/robo.bat +++ /dev/null @@ -1,21 +0,0 @@ -@echo off -setlocal -set base=%~dp0 -set roboCommand=%1 - -rem get all arguments except the first -shift -set "args=" -:parse -if "%~1" neq "" ( - set args=%args% %1 - shift - goto :parse -) -if defined args set args=%args:~1% - -if "%1"=="clean" ( - call "%base%vendor\bin\robo" "%roboCommand%" %args% -) else ( - call "%base%vendor\bin\robo" "%roboCommand%" -- %args% -)