Browse Source

Implemented DOM XPath, started testing

master
Dustin Wilson 2 years ago
parent
commit
7919f4077a
  1. 5
      README.md
  2. 4
      composer.lock
  3. 10
      lib/Document.php
  4. 1
      lib/Inner/Document.php
  5. 136
      lib/XPathEvaluate.php
  6. 23
      lib/XPathEvaluator.php
  7. 25
      lib/XPathEvaluatorBase.php
  8. 27
      lib/XPathException.php
  9. 46
      lib/XPathExpression.php
  10. 12
      lib/XPathNSResolver.php
  11. 168
      lib/XPathResult.php
  12. 197
      tests/cases/TestXPathEvaluate.php
  13. 33
      tests/cases/TestXPathEvaluator.php
  14. 2
      tests/phpunit.dist.xml

5
README.md

@ -56,4 +56,7 @@ The primary aim of this library is accuracy. However, due either to limitations
10. This implementation will not implement the `NodeIterator` and `TreeWalker` APIs. They are horribly conceived and impractical APIs that few people actually use because it's literally easier and faster to write recursive loops to walk through the DOM than it is to use those APIs. Walking downward through the tree has been replaced with the `ParentNode::walk` generator, and walking through adjacent children and moonwalking up the DOM tree can be accomplished through simple while or do/while loops.
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.
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.
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.

4
composer.lock

@ -63,7 +63,7 @@
"source": {
"type": "git",
"url": "mensbeam-gitea:MensBeam/HTML-Parser.git",
"reference": "b90806860312fad8f6da763656784a8a6cf880b0"
"reference": "37f0fa8647ead67e5e9f356efe91df8825094ee8"
},
"require": {
"ext-dom": "*",
@ -130,7 +130,7 @@
"parsing",
"whatwg"
],
"time": "2021-12-16T15:29:51+00:00"
"time": "2021-12-16T20:25:19+00:00"
},
{
"name": "mensbeam/intl",

10
lib/Document.php

@ -21,7 +21,7 @@ use MensBeam\HTML\Parser\{
class Document extends Node implements \ArrayAccess {
use DocumentOrElement, NonElementParentNode, ParentNode;
use DocumentOrElement, NonElementParentNode, ParentNode, XPathEvaluatorBase;
protected static ?NodeCache $cache = null;
@ -734,6 +734,14 @@ class Document extends Node implements \ArrayAccess {
// that's what we're going to do.
}
public function registerXPathFunctions(string|array|null $restrict = null): void {
$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 {
$node = $node ?? $this;
if ($node !== $this) {

1
lib/Inner/Document.php

@ -36,6 +36,7 @@ class Document extends \DOMDocument {
protected function __get_xpath(): \DOMXPath {
if ($this->_xpath === null) {
$this->_xpath = new \DOMXPath($this);
$this->_xpath->registerNamespace('xmlns', WrapperNode::XMLNS_NAMESPACE);
}
return $this->_xpath;

136
lib/XPathEvaluate.php

@ -0,0 +1,136 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\Inner\Reflection;
trait XPathEvaluate {
protected function xpathEvaluate(string $expression, Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
$innerContextNode = Reflection::getProtectedProperty($contextNode, 'innerNode');
$doc = ($innerContextNode instanceof \DOMDocument) ? $innerContextNode : $innerContextNode->ownerDocument;
set_error_handler(function(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::UNDEFINED_NAMESPACE_PREFIX);
}
});
$result = $doc->xpath->evaluate($expression, $innerContextNode);
restore_error_handler();
if ($type === XPathResult::ANY_TYPE) {
$typeOfResult = gettype($result);
if ($typeOfResult === 'object') {
$typeOfResult = $result::class;
}
switch ($typeOfResult) {
case 'integer':
case 'double':
$resultType = XPathResult::NUMBER_TYPE;
break;
case 'string':
$resultType = XPathResult::STRING_TYPE;
break;
case 'boolean':
$resultType = XPathResult::BOOLEAN_TYPE;
break;
case 'DOMNodeList':
$resultType = XPathResult::ORDERED_NODE_ITERATOR_TYPE;
break;
default:
throw new DOMException(DOMException::NOT_SUPPORTED);
}
} else {
switch ($type) {
case XPathResult::NUMBER_TYPE:
if ($result instanceof \DOMNodeList) {
throw new XPathException(XPathException::TYPE_ERROR);
}
$result = (float)$result;
break;
case XPathResult::STRING_TYPE:
if ($result instanceof \DOMNodeList) {
throw new XPathException(XPathException::TYPE_ERROR);
}
$result = (string)$result;
break;
case XPathResult::BOOLEAN_TYPE:
if ($result instanceof \DOMNodeList) {
$result = ($result->length > 0);
}
$result = (bool)$result;
break;
// In this implementation there's no difference between these because PHP's
// XPath DOM (ALMOST!) always returns in document order, and that cannot be
// changed.
case XPathResult::UNORDERED_NODE_ITERATOR_TYPE:
case XPathResult::ORDERED_NODE_ITERATOR_TYPE:
if (!$result instanceof \DOMNodeList) {
throw new XPathException(XPathException::TYPE_ERROR);
}
break;
// In this implementation there's no difference between these because PHP's
// XPath DOM (ALMOST!) always returns in document order, and that cannot be
// changed.
case XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE:
case XPathResult::ORDERED_NODE_SNAPSHOT_TYPE:
if (!$result instanceof \DOMNodeList) {
throw new XPathException(XPathException::TYPE_ERROR);
}
$temp = [];
foreach ($result as $node) {
$temp[] = $node;
}
$result = $temp;
break;
// In this implementation there's no difference between these because PHP's
// XPath DOM (ALMOST!) always returns in document order, and that cannot be
// changed.
case XPathResult::ANY_UNORDERED_NODE_TYPE:
case XPathResult::FIRST_ORDERED_NODE_TYPE:
if (!$result instanceof \DOMNodeList) {
throw new XPathException(XPathException::TYPE_ERROR);
}
$result = $result->item(0);
break;
default: throw new DOMException(DOMException::NOT_SUPPORTED);
}
$resultType = $type;
}
// XPathResult cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathResult', $resultType, ($result instanceof \DOMNodeList || is_array($result)) ? $result : [ $result ]);
}
protected function xpathRegisterPhpFunctions(Document $document, string|array|null $restrict = null): void {
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);
}
}

23
lib/XPathEvaluator.php

@ -0,0 +1,23 @@
<?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;
class XPathEvaluator {
use XPathEvaluatorBase;
public function registerXPathFunctions(Document $document, string|array|null $restrict = null): void {
$this->xpathRegisterPhpFunctions($document, $restrict);
}
public function registerXPathNamespace(Document $document, string $prefix, string $namespace): bool {
return $this->xpathRegisterNamespace($document, $prefix, $namespace);
}
}

25
lib/XPathEvaluatorBase.php

@ -0,0 +1,25 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\Inner\Reflection;
trait XPathEvaluatorBase {
use XPathEvaluate;
public function createExpression(string $expression, ?XPathNSResolver $resolver = null): XPathExpression {
// XPathExpression cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathExpression', $expression);
}
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);
}
}

27
lib/XPathException.php

@ -0,0 +1,27 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\Exception;
class XPathException extends Exception {
public const INVALID_EXPRESSION = 51;
public const TYPE_ERROR = 52;
public const UNDEFINED_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 => 'Undefined namespace prefix'
]);
parent::__construct($code, ...$args);
}
}

46
lib/XPathExpression.php

@ -0,0 +1,46 @@
<?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;
class XPathExpression {
use XPathEvaluate;
protected string $expression;
protected function __construct(string $expression) {
// Test the expression by attempting to run it on an empty document. PHP's DOM
// XPath incorrectly issues a warning on an invalid expression rather than an
// exception, so we must use a custom error handler here to "catch" it and throw
// an exception in its place.
set_error_handler(function(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);
}
// Ignore undefined namespace prefix warnings here because there's no way to
// register namespace prefixes before the expression is created.
});
$xpath = new \DOMXPath(new \DOMDocument());
$xpath->evaluate($expression);
restore_error_handler();
$this->expression = $expression;
}
protected function evaluate(Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
return $this->xpathEvaluate($this->expression, $contextNode, $type, $result);
}
}

12
lib/XPathNSResolver.php

@ -0,0 +1,12 @@
<?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;
interface XPathNSResolver {}

168
lib/XPathResult.php

@ -0,0 +1,168 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties,
MensBeam\HTML\DOM\Inner\Reflection;
class XPathResult implements \ArrayAccess, \Countable, \Iterator {
use MagicProperties;
public const ANY_TYPE = 0;
public const NUMBER_TYPE = 1;
public const STRING_TYPE = 2;
public const BOOLEAN_TYPE = 3;
public const UNORDERED_NODE_ITERATOR_TYPE = 4;
public const ORDERED_NODE_ITERATOR_TYPE = 5;
public const UNORDERED_NODE_SNAPSHOT_TYPE = 6;
public const ORDERED_NODE_SNAPSHOT_TYPE = 7;
public const ANY_UNORDERED_NODE_TYPE = 8;
public const FIRST_ORDERED_NODE_TYPE = 9;
protected bool $_invalidIteratorState = false;
protected int $position = 0;
protected int $_resultType;
protected \DOMNodeList|array $storage;
protected function __get_booleanValue(): bool {
if ($this->_resultType !== self::BOOLEAN_TYPE) {
throw new XPathException(XPathException::TYPE_ERROR);
}
return $this->storage[0];
}
protected function __get_invalidIteratorState(): bool {
return $this->_invalidIteratorState;
}
protected function __get_numberValue(): float {
if ($this->_resultType !== self::NUMBER_TYPE) {
throw new XPathException(XPathException::TYPE_ERROR);
}
return $this->storage[0];
}
protected function __get_resultType(): int {
return $this->_resultType;
}
protected function __get_singleNodeValue(): Node {
if (!in_array($this->_resultType, [ self::ANY_UNORDERED_NODE_TYPE, self::FIRST_ORDERED_NODE_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR);
}
$node = $this->storage[0];
return $node->ownerDocument->getWrapperNode($node);
}
protected function __get_snapshotLength(): bool {
if (!in_array($this->_resultType, [ self::ORDERED_NODE_SNAPSHOT_TYPE, self::UNORDERED_NODE_SNAPSHOT_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR);
}
return $this->count;
}
protected function __get_stringValue(): string {
if ($this->_resultType !== self::STRING_TYPE) {
throw new XPathException(XPathException::TYPE_ERROR);
}
return $this->storage[0];
}
protected function __construct(int $type, \DOMNodeList|array $object) {
$this->storage = $object;
$this->_resultType = $type;
}
public function count(): int {
$this->validateStorage();
return (is_array($this->storage)) ? count($this->storage) : $this->storage->length;
}
public function current(): ?Node {
$this->validateStorage();
$node = $this->storage[$this->position];
return $node->ownerDocument->getWrapperNode($node);
}
public function iterateNext(): ?Node {
if (!in_array($this->_resultType, [ self::ORDERED_NODE_ITERATOR_TYPE, self::UNORDERED_NODE_ITERATOR_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR);
}
$node = $this->storage[$this->position++];
return $node->ownerDocument->getWrapperNode($node);
}
public function key(): int {
$this->validateStorage();
return $this->position;
}
public function next(): void {
$this->validateStorage();
$this->position++;
}
public function rewind(): void {
$this->validateStorage();
$this->position = 0;
}
public function offsetExists($offset): bool {
$this->validateStorage();
return isset($this->storage[$offset]);
}
public function offsetGet($offset): ?Node {
$this->validateStorage();
$node = $this->storage[$this->position];
return $node->ownerDocument->getWrapperNode($node);
}
public function offsetSet($offset, $value): void {
$this->validateStorage();
}
public function offsetUnset($offset): void {
$this->validateStorage();
}
public function snapshotItem(int $index): ?Node {
if (!in_array($this->_resultType, [ self::ORDERED_NODE_SNAPSHOT_TYPE, self::UNORDERED_NODE_SNAPSHOT_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR);
}
if (!isset($this->storage[$index])) {
return null;
}
$node = $this->storage[$index];
return $node->ownerDocument->getWrapperNode($node);
}
public function valid(): bool {
$this->validateStorage();
return $this->offsetExists($this->position);
}
protected function validateStorage(): void {
if (!in_array($this->_resultType, [ self::ORDERED_NODE_ITERATOR_TYPE, self::UNORDERED_NODE_ITERATOR_TYPE, self::ORDERED_NODE_SNAPSHOT_TYPE, self::UNORDERED_NODE_SNAPSHOT_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR);
}
}
}

197
tests/cases/TestXPathEvaluate.php

@ -0,0 +1,197 @@
<?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,
DOMException,
Node,
XPathException,
XPathResult
};
/** @covers \MensBeam\HTML\DOM\XPathEvaluate */
class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::__get_body
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\Document::registerXPathFunctions
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Element::__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::xpathRegisterPhpFunctions
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate
* @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__get_booleanValue
* @covers \MensBeam\HTML\DOM\XPathResult::__get_numberValue
* @covers \MensBeam\HTML\DOM\XPathResult::__get_singleNodeValue
* @covers \MensBeam\HTML\DOM\XPathResult::__get_stringValue
* @covers \MensBeam\HTML\DOM\XPathResult::count
* @covers \MensBeam\HTML\DOM\XPathResult::validateStorage
* @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_xpathEvaluate(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$d->registerXPathFunctions();
$result = $d->evaluate('.//span', $d->body);
$this->assertEquals(3, count($result));
$result = $d->evaluate('count(.//span)', $d->body, null, XPathResult::NUMBER_TYPE);
$this->assertEquals(3, $result->numberValue);
$result = $d->evaluate('count(.//span)', $d->body, null);
$this->assertEquals(3, $result->numberValue);
$result = $d->evaluate('name(.//span)', $d->body, null, XPathResult::STRING_TYPE);
$this->assertEquals('span', $result->stringValue);
$result = $d->evaluate('name(.//span)', $d->body, null);
$this->assertEquals('span', $result->stringValue);
$result = $d->evaluate('.//span', $d->body, null, XPathResult::BOOLEAN_TYPE);
$this->assertTrue($result->booleanValue);
$result = $d->evaluate('not(.//span)', $d->body, null, XPathResult::BOOLEAN_TYPE);
$this->assertFalse($result->booleanValue);
$result = $d->evaluate('not(.//span)', $d->body, null);
$this->assertFalse($result->booleanValue);
$result = $d->evaluate('.//span', $d->body, null, XPathResult::ORDERED_NODE_ITERATOR_TYPE);
$this->assertEquals(3, count($result));
$result = $d->evaluate('.//span', $d->body, null);
$this->assertEquals(3, count($result));
$result = $d->evaluate('.//span', $d->body, null, XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE);
$this->assertEquals(3, count($result));
$result = $d->evaluate('.//span', $d->body, null, XPathResult::FIRST_ORDERED_NODE_TYPE);
$this->assertSame($d->body->firstChild, $result->singleNodeValue);
}
function provideMethod_xpathEvaluate__errors(): iterable {
return [
[ function() {
$d = new Document();
$d->evaluate('fail?', $d);
},
XPathException::class,
XPathException::INVALID_EXPRESSION ],
[ function() {
$d = new Document();
$d->evaluate('//fail', $d, null, XPathResult::NUMBER_TYPE);
},
XPathException::class,
XPathException::TYPE_ERROR ],
[ function() {
$d = new Document();
$d->evaluate('//fail', $d, null, XPathResult::STRING_TYPE);
},
XPathException::class,
XPathException::TYPE_ERROR ],
[ function() {
$d = new Document();
$d->evaluate('count(//fail)', $d, null, XPathResult::UNORDERED_NODE_ITERATOR_TYPE);
},
XPathException::class,
XPathException::TYPE_ERROR ],
[ function() {
$d = new Document();
$d->evaluate('count(//fail)', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE);
},
XPathException::class,
XPathException::TYPE_ERROR ],
[ function() {
$d = new Document();
$d->evaluate('count(//fail)', $d, null, XPathResult::ANY_UNORDERED_NODE_TYPE);
},
XPathException::class,
XPathException::TYPE_ERROR ],
[ function() {
$d = new Document();
$d->evaluate('//svg:svg', $d, null);
},
XPathException::class,
XPathException::UNDEFINED_NAMESPACE_PREFIX ],
[ function() {
$d = new Document();
$d->evaluate('count(//fail)', $d, null, 2112);
},
DOMException::class,
DOMException::NOT_SUPPORTED ]
];
}
/**
* @dataProvider provideMethod_xpathEvaluate__errors
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
*
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate
* @covers \MensBeam\HTML\DOM\XPathException::__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::getProtectedProperty
*/
function testMethod_xpathEvaluate__errors(\Closure $closure, string $errorClass, int $errorCode): void {
$this->expectException($errorClass);
$this->expectExceptionCode($errorCode);
$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)));
}
}

33
tests/cases/TestXPathEvaluator.php

@ -0,0 +1,33 @@
<?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,
XPathEvaluator
};
/** @covers \MensBeam\HTML\DOM\XPathEvaluator */
class TestXPathEvaluator extends \PHPUnit\Framework\TestCase {
function testMethod_registerXPathFunctions(): void {
$d = new Document();
$e = new XPathEvaluator();
$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)));
}
}

2
tests/phpunit.dist.xml

@ -34,6 +34,8 @@
<file>cases/TestProcessingInstruction.php</file>
<file>cases/TestText.php</file>
<file>cases/TestXMLDocument.php</file>
<file>cases/TestXPathEvaluate.php</file>
<file>cases/TestXPathEvaluator.php</file>
</testsuite>
<testsuite name="Serializer">
<file>cases/TestSerializer.php</file>

Loading…
Cancel
Save