Browse Source

Node fully implemented, not tested

wrapper-classes
Dustin Wilson 3 years ago
parent
commit
5918e694ea
  1. 4
      lib/CDATASection.php
  2. 53
      lib/CharacterData.php
  3. 4
      lib/ChildNode.php
  4. 4
      lib/Comment.php
  5. 11
      lib/DOMException.php
  6. 10
      lib/DOMImplementation.php
  7. 15
      lib/Document.php
  8. 3
      lib/DocumentFragment.php
  9. 5
      lib/Element.php
  10. 4
      lib/HTMLElement.php
  11. 4
      lib/HTMLTemplateElement.php
  12. 12
      lib/InnerNode/CDATASection.php
  13. 66
      lib/InnerNode/Document.php
  14. 12
      lib/InnerNode/DocumentFragment.php
  15. 12
      lib/InnerNode/ProcessingInstruction.php
  16. 6
      lib/InnerNode/Reflection.php
  17. 4
      lib/MathMLElement.php
  18. 447
      lib/Node.php
  19. 12
      lib/ProcessingInstruction.php
  20. 12
      lib/SVGElement.php
  21. 16
      lib/Text.php

4
lib/InnerNode/Attr.php → lib/CDATASection.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
namespace MensBeam\HTML\DOM;
class Attr extends \DOMAttr {}
class CDATASection extends Text {}

53
lib/CharacterData.php

@ -0,0 +1,53 @@
<?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;
abstract class CharacterData extends Node {
protected function __get_data(): string {
// PHP's DOM does this correctly already.
return $this->innerNode->data;
}
protected function __set_data(string $value): void {
// PHP's DOM does this correctly already.
$this->innerNode->data = $value;
}
protected function __get_length(): int {
// PHP's DOM does this correctly already.
return $this->innerNode->length;
}
public function appendData(string $data) {
// PHP's DOM does this correctly already.
return $this->innerNode->appendData($data);
}
public function deleteData(int $offset, int $count) {
// PHP's DOM does this correctly already.
return $this->innerNode->deleteData($data);
}
public function insertData(int $offset, string $data) {
// PHP's DOM does this correctly already.
return $this->innerNode->insertData($offset, $data);
}
public function replaceData(int $offset, int $count, string $data) {
// PHP's DOM does this correctly already.
return $this->innerNode->replaceData($offset, $count, $data);
}
public function substringData(int $offset, int $count): string {
// PHP's DOM does this correctly already.
return $this->innerNode->substringData($offset, $count);
}
}

4
lib/ChildNode.php

@ -8,7 +8,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties,
MensBeam\HTML\DOM\InnerNode\Factory;
MensBeam\HTML\DOM\InnerNode\Reflection;
trait ChildNode {
@ -41,7 +41,7 @@ trait ChildNode {
}
if ($node instanceof DocumentFragment) {
$host = Factory::getProtectedProperty($node, 'host');
$host = Reflection::getProtectedProperty($node, 'host');
if ($host !== null) {
$next = $host->get();
}

4
lib/InnerNode/Text.php → lib/Comment.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
namespace MensBeam\HTML\DOM;
class Text extends \DOMText {}
class Comment extends CharacterData {}

11
lib/DOMException.php

@ -11,7 +11,7 @@ use MensBeam\Framework\Exception;
class DOMException extends Exception {
// From PHP's DOMException; keeping error codes consistent
public const INDEX_SIZE_ERROR = 1;
public const HIERARCHY_REQUEST_ERROR = 3;
public const WRONG_DOCUMENT = 4;
public const INVALID_CHARACTER = 5;
@ -22,13 +22,11 @@ class DOMException extends Exception {
public const INVALID_MODIFICATION_ERROR = 13;
public const NAMESPACE_ERROR = 14;
public const INVALID_ACCESS_ERROR = 15;
public const VALIDATION_ERROR = 16;
public const CLIENT_ONLY_NOT_IMPLEMENTED = 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',
@ -38,10 +36,7 @@ class DOMException extends Exception {
12 => 'Syntax error',
13 => 'Invalid modification error',
14 => 'Namespace error',
15 => 'Invalid access error',
16 => 'Validation error',
301 => '%s is client side only; not implemented'
15 => 'Invalid access error'
]);
parent::__construct($code, ...$args);

10
lib/DOMImplementation.php

@ -7,7 +7,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\InnerNode\Factory,
use MensBeam\HTML\DOM\InnerNode\Reflection,
MensBeam\HTML\DOM\Parser;
@ -24,7 +24,7 @@ class DOMImplementation {
# The createDocument(namespace, qualifiedName, doctype) method steps are:
#
# 1. Let document be a new XMLDocument.
$document = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\XMLDocument');
$document = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XMLDocument');
# 2. Let element be null.
$element = null;
@ -67,7 +67,7 @@ class DOMImplementation {
$contentType = 'application/xml';
}
Factory::setProtectedProperty($document, '_contentType', $contentType);
Reflection::setProtectedProperty($document, '_contentType', $contentType);
# 8. Return document.
return $document;
@ -86,14 +86,14 @@ class DOMImplementation {
# 2. Return a new doctype, with qualifiedName as its name, publicId as its
# public ID, and systemId as its system ID, and with its node document set to
# the associated document of this.
$innerDocument = Factory::getProtectedProperty($this->document->get(), 'innerNode');
$innerDocument = Reflection::getProtectedProperty($this->document->get(), 'innerNode');
// PHP's DOM won't accept an empty string as the qualifiedName, so use a space
// instead; this will be worked around in DocumentType.
return $innerDocument->getWrapperNode($innerDocument->implementation->createDocumentType(($qualifiedName !== '') ? $qualifiedName : ' ', $publicId, $systemId));
}
public function createHTMLDocument(string $title = ''): Document {
$document = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\Document');
$document = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\Document');
if ($title !== '') {
$document->title = $title;
}

15
lib/Document.php

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\InnerNode\{
Document as InnerDocument,
Factory
Reflection
};
use MensBeam\HTML\Parser;
@ -31,7 +31,7 @@ class Document extends Node {
public function __construct() {
parent::__construct(new InnerDocument($this));
$this->_implementation = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\DOMImplementation', $this);
$this->_implementation = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\DOMImplementation', $this);
}
@ -43,6 +43,17 @@ class Document extends Node {
return $this->innerNode->getWrapperNode($this->innerNode->createElement($localName));
}
public function createTextNode(string $data): Text {
// Text has a public constructor that creates an inner text node without an
// associated document, so some jiggerypokery must be done instead.
$reflector = new \ReflectionClass(__NAMESPACE__ . '\\Text');
$text = $reflector->newInstanceWithoutConstructor();
$property = new \ReflectionProperty($text, 'innerNode');
$property->setAccessible(true);
$property->setValue($text, $this->innerNode->createTextNode($data));
return $text;
}
public function importNode(\DOMNode|Node $node, bool $deep = false): Node {
$isDOMNode = ($node instanceof \DOMNode);
$node = $this->innerNode->getWrapperNode($this->innerNode->importNode((!$isDOMNode) ? $this->getInnerNode($node) : $node, false));

3
lib/DocumentFragment.php

@ -7,7 +7,6 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\InnerNode\DocumentFragment as InnerDocumentFragment;
class DocumentFragment extends Node {
@ -16,7 +15,7 @@ class DocumentFragment extends Node {
protected ?\WeakReference $host = null;
protected function __construct(InnerDocumentFragment $fragment) {
protected function __construct(\DOMDocumentFragment $fragment) {
parent::__construct($fragment);
}
}

5
lib/Element.php

@ -7,8 +7,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\InnerNode\Element as InnerElement,
MensBeam\HTML\Parser;
use MensBeam\HTML\Parser;
class Element extends Node {
@ -24,7 +23,7 @@ class Element extends Node {
}
protected function __construct(InnerElement $element) {
protected function __construct(\DOMElement $element) {
parent::__construct($element);
}

4
lib/InnerNode/Comment.php → lib/HTMLElement.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
namespace MensBeam\HTML\DOM;
class Comment extends \DOMComment {}
class HTMLElement extends Element {}

4
lib/HTMLTemplateElement.php

@ -8,7 +8,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\InnerNode\Element as InnerElement,
MensBeam\HTML\DOM\InnerNode\Factory;
MensBeam\HTML\DOM\InnerNode\Reflection;
class HTMLTemplateElement extends Element {
@ -25,6 +25,6 @@ class HTMLTemplateElement extends Element {
parent::__construct($element);
$this->_content = $this->ownerDocument->createDocumentFragment();
Factory::setProtectedProperty($this->_content, 'host', \WeakReference::create($this));
Reflection::setProtectedProperty($this->_content, 'host', \WeakReference::create($this));
}
}

12
lib/InnerNode/CDATASection.php

@ -1,12 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
class CDATASection extends \DOMCDATASection {}

66
lib/InnerNode/Document.php

@ -30,15 +30,7 @@ class Document extends \DOMDocument {
public function __construct(WrapperDocument $wrapperNode) {
parent::__construct();
parent::registerNodeClass('DOMAttr', Attr::class);
parent::registerNodeClass('DOMComment', Comment::class);
parent::registerNodeClass('DOMCDATASection', CDATASection::class);
parent::registerNodeClass('DOMDocument', self::class);
parent::registerNodeClass('DOMDocumentFragment', DocumentFragment::class);
parent::registerNodeClass('DOMElement', Element::class);
parent::registerNodeClass('DOMProcessingInstruction', ProcessingInstruction::class);
parent::registerNodeClass('DOMText', Text::class);
$this->nodeMap = new NodeMap();
// Use a weak reference here to prevent a circular reference
@ -64,36 +56,42 @@ class Document extends \DOMDocument {
// If the node didn't exist we must construct the wrapper node's class name
// based upon the node's class name
$className = $node::class;
switch ($className) {
case __NAMESPACE__ . '\\Attr': $className = self::$parentNamespace . "\\Attr";
break;
case __NAMESPACE__ . '\\CDATASection': $className = self::$parentNamespace . "\\CDATASection";
break;
case __NAMESPACE__ . '\\Comment': $className = self::$parentNamespace . "\\Comment";
break;
case __NAMESPACE__ . '\\Document': $className = self::$parentNamespace . "\\Document";
break;
case __NAMESPACE__ . '\\DocumentFragment': $className = self::$parentNamespace . "\\DocumentFragment";
break;
case 'DOMDocumentType': $className = self::$parentNamespace . "\\DocumentType";
break;
case __NAMESPACE__ . '\\Element':
if (($node->namespaceURI === null || $node->namespaceURI === Parser::HTML_NAMESPACE) && $node->nodeName === 'template') {
$className = self::$parentNamespace . "\\HTMLTemplateElement";
if ($node instanceof \DOMAttr) {
$className = 'Attr';
} elseif ($node instanceof \DOMCDATASection) {
$className = 'CDATASection';
} elseif ($node instanceof \DOMComment) {
$className = 'Comment';
} elseif ($node instanceof \DOMDocument) {
$className = 'Document';
} elseif ($node instanceof \DOMDocumentFragment) {
$className = 'DocumentFragment';
} elseif ($node instanceof \DOMDocumentType) {
$className = 'DOMDocumentType';
} elseif ($node instanceof \DOMElement) {
$namespace = $node->namespaceURI;
if ($namespace === null) {
if ($node->nodeName === 'template') {
$className = 'HTMLTemplateElement';
} else {
$className = self::$parentNamespace . "\\Element";
$className = 'HTMLElement';
}
break;
case __NAMESPACE__ . '\\ProcessingInstruction': $className = self::$parentNamespace . "\\ProcessingInstruction";
break;
case __NAMESPACE__ . '\\Text': $className = self::$parentNamespace . "\\Text";
break;
case __NAMESPACE__ . '\\XMLDocument': $className = self::$parentNamespace . "\\XMLDocument";
break;
} elseif ($namespace === Parser::SVG_NAMESPACE) {
$className = 'SVGElement';
} elseif ($namespace === Parser::MATHML_NAMESPACE) {
$className = 'MathMLElement';
} else {
$className = 'Element';
}
} elseif ($node instanceof \DOMProcessingInstruction) {
$className = 'ProcessingInstruction';
} elseif ($node instanceof \DOMText) {
$className = 'Text';
} elseif ($node instanceof XMLDocument) {
$className = 'XMLDocument';
}
// Nodes cannot be created from their constructors normally
return Factory::createFromProtectedConstructor($className, $node);
return Reflection::createFromProtectedConstructor(self::$parentNamespace . "\\$className", $node);
}
}

12
lib/InnerNode/DocumentFragment.php

@ -1,12 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
class DocumentFragment extends \DOMDocumentFragment {}

12
lib/InnerNode/ProcessingInstruction.php

@ -1,12 +0,0 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
class ProcessingInstruction extends \DOMProcessingInstruction {}

6
lib/InnerNode/Factory.php → lib/InnerNode/Reflection.php

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
class Factory {
class Reflection {
public static function createFromProtectedConstructor(string $className, ...$arguments): mixed {
$reflector = new \ReflectionClass($className);
$result = $reflector->newInstanceWithoutConstructor();
@ -26,10 +26,10 @@ class Factory {
return $property->getValue($instance);
}
public static function setProtectedProperty(mixed $instance, string $propertyName, mixed $value): mixed {
public static function setProtectedProperty(mixed $instance, string $propertyName, mixed $value): void {
$reflector = new \ReflectionClass($instance::class);
$property = new \ReflectionProperty($instance, $propertyName);
$property->setAccessible(true);
return $property->setValue($instance, $value);
$property->setValue($instance, $value);
}
}

4
lib/InnerNode/Element.php → lib/MathMLElement.php

@ -6,7 +6,7 @@
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\InnerNode;
namespace MensBeam\HTML\DOM;
class Element extends \DOMElement {}
class MathMLElement extends Element {}

447
lib/Node.php

@ -8,7 +8,7 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\Framework\MagicProperties,
MensBeam\HTML\DOM\InnerNode\Factory;
MensBeam\HTML\DOM\InnerNode\Reflection;
abstract class Node {
@ -37,6 +37,40 @@ abstract class Node {
protected ?NodeList $_childNodes = null;
protected \DOMNode $innerNode;
private static ?int $rand = null;
protected function __get_baseURI(): string {
# The baseURI getter steps are to return this’s node document’s document base
# URL, serialized.
#
# The document base URL of a Document object is the absolute URL obtained by running these steps:
$document = ($this instanceof Document) ? $this : $this->ownerDocument;
$base = $doc->getElementsByNodeName('base');
foreach ($base as $b) {
$href = $base->getAttribute('href');
# 2. Otherwise, return the frozen base URL of the first base element in the
# Document that has an href attribute, in tree order.
// URL of base element is always frozen
if ($href !== null) {
return $href;
}
}
# 1. If there is no base element that has an href attribute in the Document,
# then return the Document's fallback base URL.
// This is going to be done last because I have to iterate over the base elements first.
# The fallback base URL of a Document object document is the URL record obtained by running these steps:
#
# 1. If document is an iframe srcdoc document, then return the document base URL
# of document's browsing context's container document.
// DEVIATION: There can't be an iframe srcdoc document in this implementation.
# 2. If document's URL is about:blank, and document's browsing context's creator
# base URL is non-null, then return that creator base URL.
// DEVIATION: Document's URL cannot be about:blank in this implementation.
# 3. Return document's URL.
return $document->URL;
}
protected function __get_childNodes(): NodeList {
// NodeLists cannot be created from their constructors normally.
// OPTIMIZATION: Going to optimize here and only create a truly live NodeList if
@ -44,7 +78,7 @@ abstract class Node {
// NodeList. There is no sense in generating a live list that will never update.
if ($this instanceof Document || $this instanceof DocumentFragment || $this instanceof Element) {
$doc = ($this instanceof Document) ? $this->innerNode : $this->innerNode->ownerDocument;
return Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\NodeList', function() use($doc) {
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\NodeList', function() use($doc) {
$result = [];
$innerChildNodes = $this->innerNode->childNodes;
foreach ($innerChildNodes as $i) {
@ -59,7 +93,7 @@ abstract class Node {
return $this->_childNodes;
}
$this->_childNodes = Factory::createFromProtectedConstructor(__NAMESPACE__ . '\\Nodelist', []);
$this->_childNodes = Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\Nodelist', []);
return $this->_childNodes;
}
@ -68,6 +102,13 @@ abstract class Node {
return $this->innerNode->firstChild;
}
protected function __get_isConnected(): bool {
# The isConnected getter steps are to return true, if this is connected;
# otherwise false.
# An element is connected if its shadow-including root is a document.
return ($this->getRootNode() instanceof Document);
}
protected function __get_lastChild(): ?Node {
// PHP's DOM does this correctly already.
return $this->innerNode->lastChild;
@ -196,6 +237,93 @@ abstract class Node {
return $newInner->ownerDocument->getWrapperNode($newInner);
}
public function compareDocumentPosition(Node $other): int {
# The compareDocumentPosition(other) method steps are:
#
# 1. If this is other, then return zero.
if ($this === $other) {
return 0;
}
# 2. Let node1 be other and node2 be this.
$node1 = $other;
$node2 = $this;
# 3. Let attr1 and attr2 be null.
$attr1 = $attr2 = null;
# 4. If node1 is an attribute, then set attr1 to node1 and node1 to attr1’s
# element.
if ($node1 instanceof Attr) {
$attr1 = $node1;
$node1 = $attr1->ownerElement;
}
# 5. If node2 is an attribute, then:
if ($node2 instanceof Attr) {
# 1. Set attr2 to node2 and node2 to attr2’s element.
$attr2 = $node2;
$node2 = $attr2->ownerElement;
# 2. If attr1 and node1 are non-null, and node2 is node1, then:
if ($attr1 !== null && $node1 !== null && $node2 === $node1) {
# 1. For each attr in node2’s attribute list:
foreach ($node2->attributes as $attr) {
# 1. If attr equals attr1, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_PRECEDING.
if ($attr === $attr1) {
return Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + Node::DOCUMENT_POSITION_PRECEDING;
}
# 2. If attr equals attr2, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_FOLLOWING.
if ($attr === $attr2) {
return Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + Node::DOCUMENT_POSITION_FOLLOWING;
}
}
}
}
# 6. If node1 or node2 is null, or node1’s root is not node2’s root, then return the
# result of adding DOCUMENT_POSITION_DISCONNECTED,
# DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either
# DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, with the constraint
# that this is to be consistent, together.
#
# NOTE: Whether to return DOCUMENT_POSITION_PRECEDING or
# DOCUMENT_POSITION_FOLLOWING is typically implemented via pointer comparison.
# In JavaScript implementations a cached Math.random() value can be used.
if (self::$rand === null) {
self::$rand = rand(0, 1);
}
if ($node1 === null || $node2 === null || $node1->getRootNode() !== $node2->getRootNode()) {
return Node::DOCUMENT_POSITION_DISCONNECTED + Node::DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + ((self::$rand === 0) ? Node::DOCUMENT_POSITION_PRECEDING : Node::DOCUMENT_POSITION_FOLLOWING);
}
# 7. If node1 is an ancestor of node2 and attr1 is null, or node1 is node2 and attr2
# is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINS to
# DOCUMENT_POSITION_PRECEDING.
if (($node1 === $node2 && $attr2 !== null) || ($attr1 === null && $node2->contains($node1))) {
return Node::DOCUMENT_POSITION_CONTAINS + Node::DOCUMENT_POSITION_PRECEDING;
}
# 8. If node1 is a descendant of node2 and attr2 is null, or node1 is node2 and attr1
# is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINED_BY to
# DOCUMENT_POSITION_FOLLOWING.
if (($node1 === $node2 && $attr1 !== null) || ($attr2 === null && $node2->contains($node1))) {
return Node::DOCUMENT_POSITION_CONTAINED_BY + Node::DOCUMENT_POSITION_FOLLOWING;
}
# 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING.
if ($node2->walkPreceding(function($n) use($node1) {
return ($n === $node1);
})->current() !== null) {
return Node::DOCUMENT_POSITION_PRECEDING;
}
# 10. Return DOCUMENT_POSITION_FOLLOWING.
return Node::DOCUMENT_POSITION_FOLLOWING;
}
public function contains(?Node $other): bool {
# The contains(other) method steps are to return true if other is an inclusive
# descendant of this; otherwise false (including when other is null).
@ -204,6 +332,24 @@ abstract class Node {
})->current() !== null);
}
public function getRootNode(array $options): Node {
# The getRootNode(options) method steps are to return this’s shadow-including
# root if options["composed"] is true; otherwise this’s root.
// DEVIATION: This implementation does not have scripting, so there's no Shadow
// DOM. Therefore, there isn't a need for the options parameter.
# The root of an object is itself, if its parent is null, or else it is the root
# of its parent. The root of a tree is any object participating in that tree
# whose parent is null.
if ($this->parentNode === null) {
return $this;
}
return $this->moonwalk(function($n) {
return ($n->parentNode === null);
})->current();
}
public function hasChildNodes(): bool {
// PHP's DOM does this correctly already.
return $this->innerNode->hasChildNodes();
@ -218,12 +364,156 @@ abstract class Node {
return $node;
}
public function isDefaultNamespace(?string $namespace = null): bool {
# The isDefaultNamespace(namespace) method steps are:
// PHP DOM's implementation of this is broken for HTML, so let's do this
// manually.
# 1. If namespace is the empty string, then set it to null.
if ($namespace === '') {
$namespace = null;
}
# 2. Let defaultNamespace be the result of running locate a namespace for this
# using null.
# 3. Return true if defaultNamespace is the same as namespace; otherwise false.
return ($this->locateNamespace($this, null) === $namespace);
}
public function isEqualNode(?Node $otherNode) {
# The isEqualNode(otherNode) method steps are to return true if otherNode is
# non-null and this equals otherNode; otherwise false.
# A node A equals a node B if all of the following conditions are true:
#
# • A and B implement the same interfaces.
if ($this::class !== $otherNode::class) {
return false;
}
# • The following are equal, switching on the interface A implements:
#
# ↪ DocumentType
# Its name, public ID, and system ID.
if ($this instanceof DocumentType) {
if ($this->name !== $otherNode->name || $this->publicId !== $otherNode->publicId || $this->systemId !== $this->publicId) {
return false;
}
}
# ↪ Element
# Its namespace, namespace prefix, local name, and its attribute list’s size.
elseif ($this instanceof Element) {
if ($this->namespaceURI !== $otherNode->namespaceURI || $this->prefix !== $otherNode->prefix || $this->localName !== $otherNode->localName || $this->attributes->length !== $otherNode->attributes->length) {
return false;
}
# • If A is an element, each attribute in its attribute list has an attribute that
# equals an attribute in B’s attribute list.
foreach ($this->attributes as $key => $attr) {
if (!$attr->isEqualNode($otherNode->attributes[$key])) {
return false;
}
}
}
# ↪ Attr
# Its namespace, local name, and value.
elseif ($this instanceof Attr) {
if ($this->namespaceURI !== $otherNode->namespaceURI || $this->localName !== $otherNode->localName || $this->value !== $otherNode->value) {
return false;
}
}
# ↪ Text
# ↪ Comment
# Its data.
elseif ($this instanceof Text || $this instanceof Comment) {
if ($this->data !== $otherNode->data) {
return false;
}
}
if ($this instanceof Document || $this instanceof DocumentFragment || $this instanceof Element) {
# • A and B have the same number of children.
if ($this->childNodes->length !== $otherNode->childNodes->length) {
return false;
}
# • Each child of A equals the child of B at the identical index.
foreach ($this->childNodes as $key => $child) {
$other = $otherNode->childNodes[$key];
if ($child->name !== $other->name || $child->publicId !== $other->publicId || $child->systemId !== $other->systemId) {
return false;
}
}
}
return true;
}
public function isSameNode(?Node $otherNode) {
# The isSameNode(otherNode) method steps are to return true if otherNode is
# this; otherwise false.
return ($otherNode === $this);
}
public function lookupPrefix(?string $namespace = null): ?string {
# The lookupPrefix(namespace) method steps are:
// PHP DOM's implementation of this is broken for HTML, so let's do this
// manually.
# 1. If namespace is null or the empty string, then return null.
if ($namespace === null || $namespace === '') {
return null;
}
# 2. Switch on the interface this implements:
#
# ↪ Element
if ($this instanceof Element) {
# Return the result of locating a namespace prefix for it using namespace.
return $this->locateNamespacePrefix($this, $namespace);
}
# ↪ Document
elseif ($this instanceof Document) {
# Return the result of locating a namespace prefix for its document element, if
# its document element is non-null; otherwise null.
return ($this->documentElement !== null) ? $this->locateNamespacePrefix($this->documentElement, $namespace) : null;
}
# ↪ DocumentType
# ↪ DocumentFragment
elseif ($this instanceof DocumentType || $this instanceof DocumentFragment) {
return null;
}
# ↪ Attr
elseif ($this instanceof Attr) {
# Return the result of locating a namespace prefix for its element, if its
# element is non-null; otherwise null.
return $this->locateNamespacePrefix($this->ownerElement, $namespace);
}
# ↪ Otherwise
# Return the result of locating a namespace prefix for its parent element,
# if its parent element is non-null; otherwise null.
$parentElement = $this->parentElement;
return ($parentElement !== null) ? $this->locateNamespacePrefix($this->parentElement, $namespace) : null;
}
public function lookupNamespaceURI(?string $prefix = null): ?string {
# The lookupNamespaceURI(prefix) method steps are:
// PHP DOM's implementation of this is broken for HTML, so let's do this
// manually.
# 1. If prefix is the empty string, then set it to null.
if ($prefix === '') {
$prefix = null;
}
# 2. Return the result of running locate a namespace for this using prefix.
return $this->locateNamespace($this, $prefix);
}
public function normalize(): void {
// PHP's DOM does this correctly already.
$this->innerNode->normalize();
@ -289,19 +579,22 @@ abstract class Node {
}, true)->current() !== null) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
} elseif ($nodeChildElementCount === 1) {
$n = $this->firstChild;
$beforeChild = true;
do {
if ($node->firstChild->walkFollowing(function($n) use(&$beforeChild, $child) {
if (!$beforeChild && $n instanceof DocumentType) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
return true;
}
if ($n instanceof Element && $n !== $child) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
return true;
} elseif ($n === $child) {
$beforeChild = false;
}
} while ($n = $n->nextSibling);
return false;
}, true)->current() !== null) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
}
}
}
@ -309,38 +602,44 @@ abstract class Node {
# parent has an element child that is not child or a doctype is following
# child.
elseif ($node instanceof Element) {
$n = $this->firstChild;
$beforeChild = true;
do {
if ($node->firstChild->walkFollowing(function($n) use(&$beforeChild, $child) {
if (!$beforeChild && $n instanceof DocumentType) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
return true;
}
if ($n instanceof Element && $n !== $child) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
return true;
} elseif ($n === $child) {
$beforeChild = false;
}
} while ($n = $n->nextSibling);
return false;
}, true)->current() !== null) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
}
}
# ↪ DocumentType
# parent has a doctype child that is not child, or an element is preceding
# child.
elseif ($node instanceof DocumentType) {
$n = $this->firstChild;
$beforeChild = true;
do {
if ($node->firstChild->walkFollowing(function($n) use(&$beforeChild, $child) {
if ($beforeChild && $n instanceof Element) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
return true;
}
if ($n instanceof DocumentType && $n !== $child) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
return true;
} elseif ($n === $child) {
$beforeChild = false;
}
} while ($n = $n->nextSibling);
return false;
}, true)->current() !== null) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
}
}
}
@ -355,7 +654,115 @@ abstract class Node {
return $this->innerNode;
}
return Factory::getProtectedProperty($node, 'innerNode');
return Reflection::getProtectedProperty($node, 'innerNode');
}
protected function locateNamespace(Node $node, ?string $prefix = null): ?string {
# To locate a namespace for a node using prefix, switch on the interface node
# implements:
#
# ↪ Element
if ($node instanceof Element) {
# 1. If its namespace is non-null and its namespace prefix is prefix, then return
# namespace.
if ($node->namespaceURI !== null && $node->prefix === $prefix) {
return $node->namespaceURI;
}
# 2. If it has an attribute whose namespace is the XMLNS namespace, namespace prefix
# is "xmlns", and local name is prefix, or if prefix is null and it has an
# attribute whose namespace is the XMLNS namespace, namespace prefix is null, and
# local name is "xmlns", then return its value if it is not the empty string, and
# null otherwise.
$attributes = $node->attributes;
foreach ($attributes as $attr) {
if (($attr->namespaceURI === Parser::XMLNS_NAMESPACE && $attr->prefix === 'xmlns' && $attr->localName === $prefix) || ($prefix === null && $attr->namespaceURI === Parser::XMLNS_NAMESPACE && $attr->prefix === null && $attr->localName === 'xmlns')) {
return ($attr->value !== '') ? $attr->value : null;
}
}
$parentElement = $node->parentElement;
# 3. If its parent element is null, then return null.
if ($parentElement === null) {
return null;
}
# 4. Return the result of running locate a namespace on its parent element using
# prefix.
return $this->locateNamespace($parentElement, $prefix);
}
# ↪ Document
elseif ($node instanceof Document) {
# 1. If its document element is null, then return null.
if ($node->documentElement === null) {
return null;
}
# 2. Return the result of running locate a namespace on its document element
# using prefix.
return $this->locateNamespace($node->documentElement, $prefix);
}
# ↪ DocumentType
# ↪ DocumentFragment
elseif ($node instanceof DocumentType || $node instanceof DocumentFragment) {
# Return null.
return null;
}
# ↪ Attr
elseif ($node instanceof Attr) {
# 1. If its element is null, then return null.
if ($node->ownerElement === null) {
return null;
}
# 2. Return the result of running locate a namespace on its element using
# prefix.
return $this->locateNamespace($node->ownerElement, $prefix);
}
# ↪ Otherwise
# 1. If its parent element is null, then return null.
$parentElement = $node->parentElement;
if ($parentElement === null) {
return null;
}
# 2. Return the result of running locate a namespace on its parent element using
# prefix.
return $this->locateNamespace($parentElement, $prefix);
}
protected function locateNamespacePrefix(Element $element, ?string $namespace = null) {
# To locate a namespace prefix for an element using namespace, run these steps:
#
# 1. If element’s namespace is namespace and its namespace prefix is non-null,
# then return its namespace prefix.
if ($element->namespaceURI === $namespace && $element->$prefix !== null) {
return $element->prefix;
}
# 2. If element has an attribute whose namespace prefix is "xmlns" and value is
# namespace, then return element’s first such attribute’s local name.
$attributes = $element->attributes;
foreach ($attributes as $attr) {
if ($attr->prefix === 'xmlns' && $attr->value === $namespace) {
return $attr->localName;
}
}
# 3. If element’s parent element is not null, then return the result of running
# locate a namespace prefix on that element using namespace.
$parentElement = $element->parentElement;
if ($parentElement !== null) {
return $this->locateNamespacePrefix($parentElement, $namespace);
}
# Return null.
return null;
}
protected function preInsertionValidity(Node $node, ?Node $child = null) {
@ -379,7 +786,7 @@ abstract class Node {
} else {
$parentRoot = $this->getRootNode();
if ($parentRoot instanceof DocumentFragment) {
$parentRootHost = Factory::getProtectedProperty($parentRoot, 'host')->get();
$parentRootHost = Reflection::getProtectedProperty($parentRoot, 'host')->get();
if ($parentRootHost !== null && ($parentRootHost === $node || $node->contains($parentRootHost))) {
throw new DOMException(DOMException::HIERARCHY_REQUEST_ERROR);
}

12
lib/ProcessingInstruction.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;
class ProcessingInstruction extends CharacterData {}

12
lib/SVGElement.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;
class SVGElement extends Element {}

16
lib/Text.php

@ -0,0 +1,16 @@
<?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 Text extends CharacterData {
public function __construct(string $data = '') {
$this->innerNode = new \DOMText($data);
}
}
Loading…
Cancel
Save