Browse Source

XPath done and fully tested

master
Dustin Wilson 2 years ago
parent
commit
17d6b8a2ed
  1. 6
      README.md
  2. 4
      lib/Document.php
  3. 37
      lib/XPathEvaluate.php
  4. 4
      lib/XPathEvaluator.php
  5. 9
      lib/XPathEvaluatorBase.php
  6. 4
      lib/XPathException.php
  7. 56
      lib/XPathExpression.php
  8. 14
      lib/XPathNSResolver.php
  9. 8
      lib/XPathResult.php
  10. 57
      tests/cases/TestXPathEvaluate.php
  11. 9
      tests/cases/TestXPathEvaluator.php
  12. 159
      tests/cases/TestXPathExpression.php
  13. 149
      tests/cases/TestXPathResult.php
  14. 2
      tests/phpunit.dist.xml

6
README.md

@ -57,6 +57,8 @@ The primary aim of this library is accuracy. However, due either to limitations
11. All of the `Range` APIs will also not be implemented due to the sheer complexity of creating them in userland and how it adds undue difficulty to node manipulation in the "core" DOM. Numerous operations reference in excrutiating detail what to do with Ranges when manipulating nodes and would have to be added here to be compliant or mostly so -- slowing everything else down in the process on an already extremely front-heavy library. 11. All of the `Range` APIs will also not be implemented due to the sheer complexity of creating them in userland and how it adds undue difficulty to node manipulation in the "core" DOM. Numerous operations reference in excrutiating detail what to do with Ranges when manipulating nodes and would have to be added here to be compliant or mostly so -- slowing everything else down in the process on an already extremely front-heavy library.
12. The `DOMParser` and `XMLSerializer` APIs will not be implemented because they are ridiculous and limited in their scope. For instance, `DOMParser::parseFromString` won't set a document's character set to anything but UTF-8. This library needs to be able to print to other encodings due to the nature of how it is used. `Document::__construct` will accept optional `$source` and `$charset` arguments, and there are both `Document::load` and `Document::loadFile` methods for loading DOM from a string or a file respectively. 12. The `DOMParser` and `XMLSerializer` APIs will not be implemented because they are ridiculous and limited in their scope. For instance, `DOMParser::parseFromString` won't set a document's character set to anything but UTF-8. This library needs to be able to print to other encodings due to the nature of how it is used. `Document::__construct` will accept optional `$source` and `$charset` arguments, and there are both `Document::load` and `Document::loadFile` methods for loading DOM from a string or a file respectively.
13. Aside from `HTMLElement`, `HTMLPreElement`, `HTMLTemplateElement`, `HTMLUnknownElement`, `MathMLElement`, and `SVGElement` none of the specific derived element classes (such as `HTMLAnchorElement` or `SVGSVGElement`) are implemented. The ones listed before are required for the element interface algorithm. The focus on this library will be on the core DOM before moving onto those -- if ever. 13. Aside from `HTMLElement`, `HTMLPreElement`, `HTMLTemplateElement`, `HTMLUnknownElement`, `MathMLElement`, and `SVGElement` none of the specific derived element classes (such as `HTMLAnchorElement` or `SVGSVGElement`) are implemented. The ones listed before are required for the element interface algorithm. The focus on this library will be on the core DOM before moving onto those -- if ever.
14. This class is meant to be used with HTML, but it will -MOSTLY- as needed work with XML. Loading of XML uses PHP DOM's XML parser which does not conform to the XML specification. Writing an actual conforming XML parser is outside of the scope of this library. 14. This class is meant to be used with HTML, but it will -MOSTLY- as needed work with XML. Loading of XML uses PHP DOM's XML parser which does not completely conform to the XML specification. Writing an actual conforming XML parser is outside of the scope of this library.
15. While there is implementation of much of the XPath extensions, there will only be support for XPath 1.0 because that is all PHP DOM's XPath supports. 15. While there is implementation of much of the XPath extensions, there will only be support for XPath 1.0 because that is all PHP DOM's XPath supports.
16. The XPath DOM specification allows for the use of the `XPathNSResolver` to automatically resolve namespaces for prefixes. To polyfill this behavior for use with PHP's XPath implementation would require writing at least partially a XPath 1.0 parser to grab any prefixes and then use `DOMXPath::registerNamespace` to associate namespaces. This might be something done at a later date. In the meantime this implementation instead exposes this ability to assocate namespaces with prefixes through the `Document::registerXPathNamespace` and `XPathEvaluator::registerXPathNamespace` methods. However, to eliminate common uses of namespace association the `xmlns` namespace is automatically associated. 16. This library's XPath API is -- like the rest of the library itself -- a wrapper that wraps PHP's implementation but instead works like the specification, so there is no need to manually register namespaces. Namespaces that are associated with prefixes will be looked up when evaluating the expression if a `XPathNSResolver` is specified. However, access to registering PHP functions for use within XPath isn't in the specification but is available through `Document::registerXPathFunctions` and `XPathEvaluator::registerXPathFunctions`.
17. `XPathEvaluatorBase::evaluate` has a `result` argument where one provides it with an existing result object to use. I can't find any usable documentation on what this is supposed to do, and the specifications on it are vague. So, at present it does nothing until what it needs to do can be deduced.
18. At present XPath expressions cannot select elements or attributes which use any valid non-ascii character. This is because those nodes are coerced internally to work within PHP's DOM which doesn't support those characters. This can be worked around by coercing names in XPath queries, but that can only be reliably accomplished through an XPath parser. Writing an entire XPath parser for what amounts to an edge case isn't desirable.

4
lib/Document.php

@ -738,10 +738,6 @@ class Document extends Node implements \ArrayAccess {
$this->xpathRegisterPhpFunctions($this, $restrict); $this->xpathRegisterPhpFunctions($this, $restrict);
} }
public function registerXPathNamespace(string $prefix, string $namespace): bool {
return $this->xpathRegisterNamespace($this, $prefix, $namespace);
}
public function serialize(?Node $node = null, array $config = []): string { public function serialize(?Node $node = null, array $config = []): string {
$node = $node ?? $this; $node = $node ?? $this;
if ($node !== $this) { if ($node !== $this) {

37
lib/XPathEvaluate.php

@ -11,21 +11,34 @@ use MensBeam\HTML\DOM\Inner\Reflection;
trait XPathEvaluate { trait XPathEvaluate {
protected function xpathEvaluate(string $expression, Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult { protected function xpathErrorHandler(int $errno, string $errstr, string $errfile, int $errline) {
$lowerErrstr = strtolower($errstr);
if (str_contains(needle: 'invalid expression', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::INVALID_EXPRESSION);
}
if (str_contains(needle: 'undefined namespace prefix', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::UNRESOLVABLE_NAMESPACE_PREFIX);
}
} // @codeCoverageIgnore
protected function xpathEvaluate(string $expression, Node $contextNode, ?XPathNSResolver $resolver = null, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
$innerContextNode = Reflection::getProtectedProperty($contextNode, 'innerNode'); $innerContextNode = Reflection::getProtectedProperty($contextNode, 'innerNode');
$doc = ($innerContextNode instanceof \DOMDocument) ? $innerContextNode : $innerContextNode->ownerDocument; $doc = ($innerContextNode instanceof \DOMDocument) ? $innerContextNode : $innerContextNode->ownerDocument;
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline) { if ($resolver !== null && preg_match_all('/([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+):([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+)/u', $expression, $m, \PREG_SET_ORDER)) {
$lowerErrstr = strtolower($errstr); foreach ($m as $prefix) {
$prefix = $prefix[1];
if (str_contains(needle: 'invalid expression', haystack: $lowerErrstr)) { if ($namespace = $contextNode->lookupNamespaceURI($prefix)) {
throw new XPathException(XPathException::INVALID_EXPRESSION); $doc->xpath->registerNamespace($prefix, $namespace);
}
} }
}
if (str_contains(needle: 'undefined namespace prefix', haystack: $lowerErrstr)) { // PHP's DOM XPath incorrectly issues a warnings rather than exceptions when
throw new XPathException(XPathException::UNDEFINED_NAMESPACE_PREFIX); // expressions are incorrect, so we must use a custom error handler here to
} // "catch" it and throw an exception in its place.
}); set_error_handler([ $this, 'xpathErrorHandler' ]);
$result = $doc->xpath->evaluate($expression, $innerContextNode); $result = $doc->xpath->evaluate($expression, $innerContextNode);
restore_error_handler(); restore_error_handler();
@ -129,8 +142,4 @@ trait XPathEvaluate {
protected function xpathRegisterPhpFunctions(Document $document, string|array|null $restrict = null): void { protected function xpathRegisterPhpFunctions(Document $document, string|array|null $restrict = null): void {
Reflection::getProtectedProperty($document, 'innerNode')->xpath->registerPhpFunctions($restrict); Reflection::getProtectedProperty($document, 'innerNode')->xpath->registerPhpFunctions($restrict);
} }
protected function xpathRegisterNamespace(Document $document, string $prefix, string $namespace): bool {
return Reflection::getProtectedProperty($document, 'innerNode')->xpath->registerNamespace($prefix, $namespace);
}
} }

4
lib/XPathEvaluator.php

@ -16,8 +16,4 @@ class XPathEvaluator {
public function registerXPathFunctions(Document $document, string|array|null $restrict = null): void { public function registerXPathFunctions(Document $document, string|array|null $restrict = null): void {
$this->xpathRegisterPhpFunctions($document, $restrict); $this->xpathRegisterPhpFunctions($document, $restrict);
} }
public function registerXPathNamespace(Document $document, string $prefix, string $namespace): bool {
return $this->xpathRegisterNamespace($document, $prefix, $namespace);
}
} }

9
lib/XPathEvaluatorBase.php

@ -16,10 +16,15 @@ trait XPathEvaluatorBase {
public function createExpression(string $expression, ?XPathNSResolver $resolver = null): XPathExpression { public function createExpression(string $expression, ?XPathNSResolver $resolver = null): XPathExpression {
// XPathExpression cannot be created from their constructors normally. // XPathExpression cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathExpression', $expression); return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathExpression', $expression, $resolver);
}
public function createNSResolver(Node $nodeResolver): XPathNSResolver {
// XPathNSResolver cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathNSResolver', $nodeResolver);
} }
public function evaluate(string $expression, Node $contextNode, ?XPathNSResolver $resolver = null, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult { public function evaluate(string $expression, Node $contextNode, ?XPathNSResolver $resolver = null, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
return $this->xpathEvaluate($expression, $contextNode, $type, $result); return $this->xpathEvaluate($expression, $contextNode, $resolver, $type, $result);
} }
} }

4
lib/XPathException.php

@ -13,13 +13,13 @@ use MensBeam\Framework\Exception;
class XPathException extends Exception { class XPathException extends Exception {
public const INVALID_EXPRESSION = 51; public const INVALID_EXPRESSION = 51;
public const TYPE_ERROR = 52; public const TYPE_ERROR = 52;
public const UNDEFINED_NAMESPACE_PREFIX = 53; public const UNRESOLVABLE_NAMESPACE_PREFIX = 53;
public function __construct(int $code, ...$args) { public function __construct(int $code, ...$args) {
self::$messages = array_replace(parent::$messages, [ self::$messages = array_replace(parent::$messages, [
51 => 'Invalid expression error', 51 => 'Invalid expression error',
52 => 'Expression cannot be converted to the specified type', 52 => 'Expression cannot be converted to the specified type',
53 => 'Undefined namespace prefix' 53 => 'Unresolvable namespace prefix'
]); ]);
parent::__construct($code, ...$args); parent::__construct($code, ...$args);

56
lib/XPathExpression.php

@ -7,40 +7,52 @@
declare(strict_types=1); declare(strict_types=1);
namespace MensBeam\HTML\DOM; namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\Inner\Reflection;
class XPathExpression { class XPathExpression {
use XPathEvaluate; use XPathEvaluate;
protected string $expression; protected string $expression;
protected ?XPathNSResolver $resolver;
protected function __construct(string $expression) {
// Test the expression by attempting to run it on an empty document. PHP's DOM protected function __construct(string $expression, ?XPathNSResolver $resolver) {
// XPath incorrectly issues a warning on an invalid expression rather than an if ($resolver !== null && preg_match_all('/([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+):([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+)/u', $expression, $m, \PREG_SET_ORDER)) {
// exception, so we must use a custom error handler here to "catch" it and throw // This part is especially nasty because of egregious use of reflection to get
// an exception in its place. // protected properties, but neither should be exposed publicly; this is a crazy
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline) { // polyfill hack that wouldn't normally be necessary otherwise.
$lowerErrstr = strtolower($errstr); $nodeResolver = Reflection::getProtectedProperty($resolver, 'nodeResolver');
$innerNodeResolver = Reflection::getProtectedProperty($nodeResolver, 'innerNode');
if (str_contains(needle: 'invalid expression', haystack: $lowerErrstr)) { $doc = ($innerNodeResolver instanceof \DOMDocument) ? $innerNodeResolver : $innerNodeResolver->ownerDocument;
throw new XPathException(XPathException::INVALID_EXPRESSION);
foreach ($m as $prefix) {
$prefix = $prefix[1];
if ($namespace = $resolver->lookupNamespaceURI($prefix)) {
$doc->xpath->registerNamespace($prefix, $namespace);
}
} }
// Ignore undefined namespace prefix warnings here because there's no way to set_error_handler([ $this, 'xpathErrorHandler' ]);
// register namespace prefixes before the expression is created. $doc->xpath->evaluate($expression);
}); restore_error_handler();
} else {
$xpath = new \DOMXPath(new \DOMDocument()); // Test the expression by attempting to run it on an empty document. PHP's DOM
$xpath->evaluate($expression); // XPath incorrectly issues a warnings rather than exceptions when expressions
// are incorrect, so we must use a custom error handler here to "catch" it and
restore_error_handler(); // throw an exception in its place.
set_error_handler([ $this, 'xpathErrorHandler' ]);
$xpath = new \DOMXPath(new \DOMDocument());
$xpath->evaluate($expression);
restore_error_handler();
}
$this->expression = $expression; $this->expression = $expression;
$this->resolver = $resolver;
} }
protected function evaluate(Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult { public function evaluate(Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
return $this->xpathEvaluate($this->expression, $contextNode, $type, $result); return $this->xpathEvaluate($this->expression, $contextNode, $this->resolver, $type, $result);
} }
} }

14
lib/XPathNSResolver.php

@ -9,4 +9,16 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM; namespace MensBeam\HTML\DOM;
interface XPathNSResolver {} class XPathNSResolver {
protected Node $nodeResolver;
protected function __construct(Node $nodeResolver) {
$this->nodeResolver = $nodeResolver;
}
public function lookupNamespaceURI(?string $prefix): ?string {
return $this->nodeResolver->lookupNamespaceURI($prefix);
}
}

8
lib/XPathResult.php

@ -63,12 +63,12 @@ class XPathResult implements \ArrayAccess, \Countable, \Iterator {
return $node->ownerDocument->getWrapperNode($node); return $node->ownerDocument->getWrapperNode($node);
} }
protected function __get_snapshotLength(): bool { protected function __get_snapshotLength(): int {
if (!in_array($this->_resultType, [ self::ORDERED_NODE_SNAPSHOT_TYPE, self::UNORDERED_NODE_SNAPSHOT_TYPE ])) { if (!in_array($this->_resultType, [ self::ORDERED_NODE_SNAPSHOT_TYPE, self::UNORDERED_NODE_SNAPSHOT_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR); throw new XPathException(XPathException::TYPE_ERROR);
} }
return $this->count; return $this->count();
} }
protected function __get_stringValue(): string { protected function __get_stringValue(): string {
@ -102,6 +102,10 @@ class XPathResult implements \ArrayAccess, \Countable, \Iterator {
throw new XPathException(XPathException::TYPE_ERROR); throw new XPathException(XPathException::TYPE_ERROR);
} }
if ($this->position + 1 > $this->count()) {
return null;
}
$node = $this->storage[$this->position++]; $node = $this->storage[$this->position++];
return $node->ownerDocument->getWrapperNode($node); return $node->ownerDocument->getWrapperNode($node);
} }

57
tests/cases/TestXPathEvaluate.php

@ -22,18 +22,40 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
/** /**
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate * @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
* *
* @covers \MensBeam\HTML\DOM\Attr::__get_localName
* @covers \MensBeam\HTML\DOM\Attr::__get_namespaceURI
* @covers \MensBeam\HTML\DOM\Attr::__get_ownerElement
* @covers \MensBeam\HTML\DOM\Attr::__set_value
* @covers \MensBeam\HTML\DOM\Document::__construct * @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::__get_body * @covers \MensBeam\HTML\DOM\Document::__get_body
* @covers \MensBeam\HTML\DOM\Document::__get_documentElement
* @covers \MensBeam\HTML\DOM\Document::createAttributeNS
* @covers \MensBeam\HTML\DOM\Document::createElementNS
* @covers \MensBeam\HTML\DOM\Document::load * @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\Document::registerXPathFunctions * @covers \MensBeam\HTML\DOM\Document::registerXPathFunctions
* @covers \MensBeam\HTML\DOM\DocumentOrElement::validateAndExtract
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Element::__construct * @covers \MensBeam\HTML\DOM\Element::__construct
* @covers \MensBeam\HTML\DOM\Element::getAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNode
* @covers \MensBeam\HTML\DOM\Element::setAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNS
* @covers \MensBeam\HTML\DOM\Node::__construct * @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::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::getInnerNode
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes * @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::locateNamespace
* @covers \MensBeam\HTML\DOM\Node::lookupNamespaceURI
* @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix * @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathRegisterPhpFunctions * @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathRegisterPhpFunctions
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createNSResolver
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate * @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate
* @covers \MensBeam\HTML\DOM\XPathNSResolver::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__construct * @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__get_booleanValue * @covers \MensBeam\HTML\DOM\XPathResult::__get_booleanValue
* @covers \MensBeam\HTML\DOM\XPathResult::__get_numberValue * @covers \MensBeam\HTML\DOM\XPathResult::__get_numberValue
@ -80,6 +102,10 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
$this->assertEquals(3, count($result)); $this->assertEquals(3, count($result));
$result = $d->evaluate('.//span', $d->body, null, XPathResult::FIRST_ORDERED_NODE_TYPE); $result = $d->evaluate('.//span', $d->body, null, XPathResult::FIRST_ORDERED_NODE_TYPE);
$this->assertSame($d->body->firstChild, $result->singleNodeValue); $this->assertSame($d->body->firstChild, $result->singleNodeValue);
$d->documentElement->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:poop', 'https://poop.poop');
$poop = $d->body->appendChild($d->createElementNS('https://poop.poop', 'poop:poop'));
$this->assertSame($poop, $d->evaluate('//poop:poop', $d->body, $d->createNSResolver($d->body), XPathResult::FIRST_ORDERED_NODE_TYPE)->singleNodeValue);
} }
@ -132,7 +158,7 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
$d->evaluate('//svg:svg', $d, null); $d->evaluate('//svg:svg', $d, null);
}, },
XPathException::class, XPathException::class,
XPathException::UNDEFINED_NAMESPACE_PREFIX ], XPathException::UNRESOLVABLE_NAMESPACE_PREFIX ],
[ function() { [ function() {
$d = new Document(); $d = new Document();
@ -165,33 +191,4 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
$this->expectExceptionCode($errorCode); $this->expectExceptionCode($errorCode);
$closure(); $closure();
} }
/**
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathRegisterNamespace
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\Document::registerXPathNamespace
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\Node::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate
* @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath
* @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
*/
function testMethod_xpathRegisterNamespace(): void {
$d = new Document('<!DOCTYPE html><html><body><svg></svg></body></html>');
$d->registerXPathNamespace('svg', Node::SVG_NAMESPACE);
$this->assertEquals(1, count($d->evaluate('//svg:svg', $d)));
}
} }

9
tests/cases/TestXPathEvaluator.php

@ -10,7 +10,6 @@ namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{ use MensBeam\HTML\DOM\{
Document, Document,
Node,
XPathEvaluator XPathEvaluator
}; };
@ -22,12 +21,4 @@ class TestXPathEvaluator extends \PHPUnit\Framework\TestCase {
$e = new XPathEvaluator(); $e = new XPathEvaluator();
$this->assertNull($e->registerXPathFunctions($d)); $this->assertNull($e->registerXPathFunctions($d));
} }
function testMethod_xpathRegisterNamespace(): void {
$d = new Document('<!DOCTYPE html><html><body><svg></svg></body></html>');
$e = new XPathEvaluator();
$e->registerXPathNamespace($d, 'svg', Node::SVG_NAMESPACE);
$this->assertEquals(1, count($e->evaluate('//svg:svg', $d)));
}
} }

159
tests/cases/TestXPathExpression.php

@ -0,0 +1,159 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
Node,
XPathException,
XPathExpression,
XPathResult
};
/** @covers \MensBeam\HTML\DOM\XPathExpression */
class TestXPathExpression extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\XPathExpression::__construct
*
* @covers \MensBeam\HTML\DOM\Attr::__get_localName
* @covers \MensBeam\HTML\DOM\Attr::__get_namespaceURI
* @covers \MensBeam\HTML\DOM\Attr::__get_ownerElement
* @covers \MensBeam\HTML\DOM\Attr::__set_value
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::__get_body
* @covers \MensBeam\HTML\DOM\Document::__get_documentElement
* @covers \MensBeam\HTML\DOM\Document::createAttributeNS
* @covers \MensBeam\HTML\DOM\Document::createElementNS
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\DocumentOrElement::validateAndExtract
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Element::__construct
* @covers \MensBeam\HTML\DOM\Element::getAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNode
* @covers \MensBeam\HTML\DOM\Element::setAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNS
* @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::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::locateNamespace
* @covers \MensBeam\HTML\DOM\Node::lookupNamespaceURI
* @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createExpression
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createNSResolver
* @covers \MensBeam\HTML\DOM\XPathNSResolver::__construct
* @covers \MensBeam\HTML\DOM\XPathNSResolver::lookupNamespaceURI
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath
* @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
*/
function testMethod_constructor(): void {
$d = new Document('<!DOCTYPE><html></html>');
$d->documentElement->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:poop', 'https://poop.poop');
$poop = $d->body->appendChild($d->createElementNS('https://poop.poop', 'poop:poop'));
$this->assertSame(XPathExpression::class, $d->createExpression('//poop:poop', $d->createNSResolver($d))::class);
$this->assertSame(XPathExpression::class, $d->createExpression('//span')::class);
}
function provideMethod_constructor__errors(): iterable {
return [
[ function() {
$d = new Document();
$d->createExpression('//fail:fail');
},
XPathException::UNRESOLVABLE_NAMESPACE_PREFIX ],
[ function() {
$d = new Document();
$d->createExpression('//fail:fail', $d->createNSResolver($d));
},
XPathException::UNRESOLVABLE_NAMESPACE_PREFIX ],
[ function() {
$d = new Document();
$d->createExpression('fail?');
},
XPathException::INVALID_EXPRESSION ],
[ function() {
$d = new Document();
$d->createExpression('fail?', $d->createNSResolver($d));
},
XPathException::INVALID_EXPRESSION ],
];
}
/**
* @dataProvider provideMethod_constructor__errors
* @covers \MensBeam\HTML\DOM\XPathExpression::__construct
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathErrorHandler
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createExpression
* @covers \MensBeam\HTML\DOM\XPathException::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @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
*/
function testMethod_constructor__errors(\Closure $closure, int $errorCode): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode($errorCode);
$closure();
}
/**
* @covers \MensBeam\HTML\DOM\XPathExpression::evaluate
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\Node::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createExpression
* @covers \MensBeam\HTML\DOM\XPathExpression::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__get_booleanValue
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath
* @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
*/
function testMethod_evaluate(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$e = $d->createExpression('//span');
$this->assertTrue($e->evaluate($d, XPathResult::BOOLEAN_TYPE)->booleanValue);
}
}

149
tests/cases/TestXPathResult.php

@ -0,0 +1,149 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
HTMLElement,
Node,
XPathException,
XPathResult
};
/** @covers \MensBeam\HTML\DOM\XPathResult */
class TestXPathResult extends \PHPUnit\Framework\TestCase {
function testIteration(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$result = $d->evaluate('.//span', $d);
foreach ($result as $k => $r) {
$this->assertSame(HTMLElement::class, $r::class);
$this->assertSame(HTMLElement::class, $result[$k]::class);
}
}
function testMethod_iterateNext(): void {
$d = new Document('<!DOCTYPE html><html><body><span></span></body></html>');
$result = $d->evaluate('.//span', $d);
$this->assertSame($d->getElementsByTagName('span')[0], $result->iterateNext());
$this->assertNull($result->iterateNext());
}
function testMethod_iterateNext__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$d->evaluate('.//fail', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->iterateNext();
}
function testMethod_snapshotItem(): void {
$d = new Document('<!DOCTYPE html><html><body><span></span></body></html>');
$this->assertSame($d->getElementsByTagName('span')[0], $d->evaluate('.//span', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->snapshotItem(0));
$this->assertNull($d->evaluate('.//span', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->snapshotItem(42));
}
function testMethod_snapshotItem__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$d->evaluate('.//fail', $d)->snapshotItem(0);
}
function testMethod_validateStorage__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$result = $d->evaluate('count(.//fail)', $d, null, XPathResult::NUMBER_TYPE);
$result[0];
}
function provideProperty__errors(): iterable {
return [
[ function() {
$d = new Document();
$result = $d->evaluate('name(//fail)', $d, null, XPathResult::NUMBER_TYPE);
$result->booleanValue;
} ],
[ function() {
$d = new Document();
$result = $d->evaluate('//fail', $d, null, XPathResult::BOOLEAN_TYPE);
$result->numberValue;
} ],
[ function() {
$d = new Document();
$result = $d->evaluate('//fail', $d, null, XPathResult::BOOLEAN_TYPE);
$result->singleNodeValue;
} ],
[ function() {
$d = new Document();
$result = $d->evaluate('//fail', $d, null, XPathResult::BOOLEAN_TYPE);
$result->stringValue;
} ]
];
}
/** @dataProvider provideProperty__errors */
function testProperty__errors(\Closure $closure): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$closure();
}
function testProperty_invalidIteratorState(): void {
$d = new Document();
$this->assertFalse($d->evaluate('//span', $d)->invalidIteratorState);
}
function testProperty_offsetSet_offsetUnset(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$result = $d->evaluate('.//span', $d);
$result[1] = 'ook';
$this->assertNotSame('ook', $result[1]);
unset($result[1]);
$this->assertSame(HTMLElement::class, $result[1]::class);
}
function testProperty_resultType(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$this->assertEquals(XPathResult::ORDERED_NODE_ITERATOR_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::ORDERED_NODE_ITERATOR_TYPE)->resultType);
$this->assertEquals(XPathResult::ORDERED_NODE_ITERATOR_TYPE, $d->evaluate('.//span', $d->body)->resultType);
$this->assertEquals(XPathResult::NUMBER_TYPE, $d->evaluate('count(.//span)', $d->body, null, XPathResult::NUMBER_TYPE)->resultType);
$this->assertEquals(XPathResult::NUMBER_TYPE, $d->evaluate('count(.//span)', $d->body)->resultType);
$this->assertEquals(XPathResult::STRING_TYPE, $d->evaluate('name(.//span)', $d->body, null, XPathResult::STRING_TYPE)->resultType);
$this->assertEquals(XPathResult::STRING_TYPE, $d->evaluate('name(.//span)', $d->body)->resultType);
$this->assertEquals(XPathResult::BOOLEAN_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::BOOLEAN_TYPE)->resultType);
$this->assertEquals(XPathResult::BOOLEAN_TYPE, $d->evaluate('not(.//span)', $d->body, null, XPathResult::BOOLEAN_TYPE)->resultType);
$this->assertEquals(XPathResult::BOOLEAN_TYPE, $d->evaluate('not(.//span)', $d->body)->resultType);
$this->assertEquals(XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE)->resultType);
$this->assertEquals(XPathResult::FIRST_ORDERED_NODE_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::FIRST_ORDERED_NODE_TYPE)->resultType);
}
function testProperty_snapshotLength(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$this->assertEquals(3, $d->evaluate('.//span', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->snapshotLength);
}
function testProperty_snapshotLength__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$d->evaluate('.//fail', $d)->snapshotLength;
}
}

2
tests/phpunit.dist.xml

@ -36,6 +36,8 @@
<file>cases/TestXMLDocument.php</file> <file>cases/TestXMLDocument.php</file>
<file>cases/TestXPathEvaluate.php</file> <file>cases/TestXPathEvaluate.php</file>
<file>cases/TestXPathEvaluator.php</file> <file>cases/TestXPathEvaluator.php</file>
<file>cases/TestXPathExpression.php</file>
<file>cases/TestXPathResult.php</file>
</testsuite> </testsuite>
<testsuite name="Serializer"> <testsuite name="Serializer">
<file>cases/TestSerializer.php</file> <file>cases/TestSerializer.php</file>

Loading…
Cancel
Save