Browse Source

Fixing template element references

• ElementSet is now ElementMap again because it's now internally a map 
of a Document and an array of Elements, but it's public API is still 
that of a set.
• Template elements are now only added as a reference in ElementMap when 
they're appended to a document. If they're removed, they're removed from 
ElementMap. If a template element's owner document is destructed then it 
and all of the other template elements in the document are removed from 
ElementMap.
wrapper-classes
Dustin Wilson 3 years ago
parent
commit
debaa3f388
  1. 7
      lib/Document.php
  2. 102
      lib/ElementMap.php
  3. 74
      lib/ElementSet.php
  4. 6
      lib/HTMLTemplateElement.php
  5. 2
      lib/traits/Moonwalk.php
  6. 26
      lib/traits/ParentNode.php
  7. 48
      tests/cases/TestDocument.php
  8. 18
      tests/cases/TestSerializer.php

7
lib/Document.php

@ -276,7 +276,6 @@ class Document extends AbstractDocument {
# qualifiedName to validate and extract.
[ 'namespace' => $namespaceURI, 'prefix' => $prefix, 'localName' => $localName ] = $this->validateAndExtract($qualifiedName, $namespaceURI);
# 2. Let is be null.
# 3. If options is a dictionary and options["is"] exists, then set is to it.
# 4. Return the result of creating an element given document, localName, namespace,
@ -287,7 +286,7 @@ class Document extends AbstractDocument {
if (strtolower($qualifiedName) !== 'template' || ($namespaceURI !== null && $namespaceURI !== Parser::HTML_NAMESPACE)) {
$e = parent::createElementNS($namespaceURI, $qualifiedName);
} else {
$e = new HTMLTemplateElement($this, $qualifiedName);
$e = new HTMLTemplateElement($this, $qualifiedName, $namespaceURI);
}
return $e;
@ -1014,6 +1013,10 @@ class Document extends AbstractDocument {
}
public function __destruct() {
ElementMap::destroy($this);
}
public function __toString() {
return $this->saveHTML();
}

102
lib/ElementMap.php

@ -0,0 +1,102 @@
<?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;
// This is a set of elements which need to be kept in memory; it exists because
// of the peculiar way PHP works. Derived DOM classes (such as
// HTMLTemplateElement) won't remain as such in the DOM (meaning they will
// revert to being what is registered for elements in Document) unless at least
// one reference is kept for the element somewhere in userspace. This is that
// somewhere.
class ElementMap {
protected static $documents = [];
protected static $elements = [];
public static function add(Element $element): bool {
$document = $element->ownerDocument;
$index = self::index($document);
if ($index === -1) {
self::$documents[] = $document;
self::$elements[count(self::$documents) - 1][] = $element;
return true;
} else {
foreach (self::$elements[$index] as $v) {
if ($v->isSameNode($element)) {
return false;
}
}
self::$elements[$index][] = $element;
return true;
}
return false;
}
public static function delete(Element $element): bool {
$document = $element->ownerDocument;
$index = self::index($document);
if ($index !== -1) {
foreach (self::$elements[$index] as $k => $v) {
if ($v->isSameNode($element)) {
unset(self::$elements[$index][$k]);
self::$elements[$index] = array_values(self::$elements[$index]);
return true;
}
}
}
return false;
}
public static function destroy(Document $document): bool {
$index = self::index($document);
if ($index !== -1) {
unset(self::$documents[$index]);
unset(self::$elements[$index]);
self::$documents = array_values(self::$documents);
self::$elements = array_values(self::$elements);
return true;
}
return false;
}
public static function getIterator(Document $document): \Traversable {
$index = self::index($document);
foreach (self::$elements[$index] as $v) {
yield $v;
}
}
public static function has(Element $element): bool {
$document = $element->ownerDocument;
$index = self::index($document);
if ($index !== -1) {
foreach (self::$elements[$index] as $v) {
if ($v->isSameNode($element)) {
return true;
}
}
}
return false;
}
protected static function index(Document $document): int {
foreach (self::$documents as $k => $d) {
if ($d->isSameNode($document)) {
return $k;
}
}
return -1;
}
}

74
lib/ElementSet.php

@ -1,74 +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;
// This is a set of elements which need to be kept in memory; it exists because
// of the peculiar way PHP works. Derived DOM classes (such as
// HTMLTemplateElement) won't remain as such in the DOM (meaning they will
// revert to being what is registered for elements in Document) unless at least
// one reference is kept for the element somewhere in userspace. This is that
// somewhere.
class ElementSet {
protected static $_storage = [];
public static function add(Element $element) {
if (!self::has($element)) {
self::$_storage[] = $element;
return true;
}
return false;
}
public static function delete(Element $element) {
foreach (self::$_storage as $k => $v) {
if ($v->isSameNode($element)) {
unset(self::$_storage[$k]);
self::$_storage = array_values(self::$_storage);
return true;
}
}
return false;
}
public static function destroy(Document $document) {
$changed = false;
foreach (self::$_storage as $k => $v) {
if ($v->ownerDocument->isSameNode($document)) {
unset(self::$_storage[$k]);
$changed = true;
}
}
if ($changed) {
self::$_storage = array_values(self::$_storage);
return true;
}
return false;
}
public static function getIterator(): \Traversable {
foreach (self::$_storage as $v) {
yield $v;
}
}
public static function has(Element $element) {
foreach (self::$_storage as $v) {
if ($v->isSameNode($element)) {
return true;
}
}
return false;
}
}

6
lib/HTMLTemplateElement.php

@ -12,8 +12,8 @@ namespace MensBeam\HTML\DOM;
class HTMLTemplateElement extends Element {
public $content = null;
public function __construct(Document $ownerDocument, string $qualifiedName, ?string $namespace = '') {
parent::__construct($qualifiedName, null, $namespace);
public function __construct(Document $ownerDocument, string $qualifiedName, ?string $namespace = null) {
parent::__construct($qualifiedName, null, $namespace ?? '');
// Elements that are created by their constructor in PHP aren't owned by any
// document and are readonly until owned by one. Temporarily append to a
@ -25,8 +25,6 @@ class HTMLTemplateElement extends Element {
unset($frag);
$this->content = $this->ownerDocument->createDocumentFragment();
// Template elements need to have a reference kept in userland
ElementSet::add($this);
}

2
lib/traits/Moonwalk.php

@ -46,7 +46,7 @@ trait Moonwalk {
// templates; if it is change node to the template element and reprocess. Magic!
// Can walk backwards THROUGH templates!
if ($node instanceof DocumentFragment) {
foreach (ElementSet::getIterator() as $element) {
foreach (ElementMap::getIterator($node->ownerDocument) as $element) {
if ($element->ownerDocument->isSameNode($node->ownerDocument) && $element instanceof TemplateElement && $element->content->isSameNode($node)) {
$node = $element;
continue;

26
lib/traits/ParentNode.php

@ -31,10 +31,8 @@ trait ParentNode {
$this->preInsertionValidity($node);
$result = parent::appendChild($node);
if ($result !== false && $result instanceof TemplateElement) {
if ($result instanceof TemplateElement) {
ElementSet::add($result);
}
if ($result !== false && $result instanceof HTMLTemplateElement) {
ElementMap::add($result);
}
return $result;
}
@ -44,11 +42,11 @@ trait ParentNode {
$result = parent::insertBefore($node, $child);
if ($result !== false) {
if ($result instanceof TemplateElement) {
ElementSet::add($result);
if ($result instanceof HTMLTemplateElement) {
ElementMap::add($result);
}
if ($child instanceof TemplateElement) {
ElementSet::delete($child);
if ($child instanceof HTMLTemplateElement) {
ElementMap::delete($child);
}
}
return $result;
@ -56,8 +54,8 @@ trait ParentNode {
public function removeChild($child) {
$result = parent::removeChild($child);
if ($result !== false && $result instanceof TemplateElement) {
ElementSet::delete($child);
if ($result !== false && $result instanceof HTMLTemplateElement) {
ElementMap::delete($child);
}
return $result;
}
@ -65,11 +63,11 @@ trait ParentNode {
public function replaceChild($node, $child) {
$result = parent::replaceChild($node, $child);
if ($result !== false) {
if ($result instanceof TemplateElement) {
ElementSet::add($child);
if ($result instanceof HTMLTemplateElement) {
ElementMap::add($child);
}
if ($child instanceof TemplateElement) {
ElementSet::delete($child);
if ($child instanceof HTMLTemplateElement) {
ElementMap::delete($child);
}
}
return $result;

48
tests/cases/TestDocument.php

@ -10,12 +10,14 @@ use MensBeam\HTML\DOM\{
Document,
DOMException,
Element,
ElementMap,
Exception,
HTMLTemplateElement
};
use MensBeam\HTML\Parser;
/** @covers \MensBeam\HTML\DOM\Document */
class TestDocument extends \PHPUnit\Framework\TestCase {
public function provideAttributeNodes(): iterable {
return [
@ -49,6 +51,7 @@ class TestDocument extends \PHPUnit\Framework\TestCase {
/**
* @dataProvider provideAttributeNodesNS
* @covers \MensBeam\HTML\DOM\Document::createAttributeNS
* @covers \MensBeam\HTML\DOM\Document::validateAndExtract
*/
public function testAttributeNodeNSCreation(?string $nsIn, string $nameIn, string $local, string $prefix): void {
$d = new Document();
@ -60,6 +63,38 @@ class TestDocument extends \PHPUnit\Framework\TestCase {
}
/**
* @covers \MensBeam\HTML\DOM\Document::__destruct
* @covers \MensBeam\HTML\DOM\ElementMap::add
* @covers \MensBeam\HTML\DOM\ElementMap::delete
* @covers \MensBeam\HTML\DOM\ElementMap::destroy
* @covers \MensBeam\HTML\DOM\ElementMap::has
* @covers \MensBeam\HTML\DOM\ElementMap::index
*/
public function testDestruction(): void {
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
$t = $d->createElement('template');
$this->assertFalse(ElementMap::has($t));
$d->body->appendChild($t);
$this->assertTrue(ElementMap::has($t));
$d->__destruct();
unset($d);
$this->assertFalse(ElementMap::has($t));
$d = new Document();
$d->appendChild($d->createElement('html'));
$d->documentElement->appendChild($d->createElement('body'));
$t = $d->importNode($t);
$this->assertFalse(ElementMap::has($t));
$d->body->appendChild($t);
$this->assertTrue(ElementMap::has($t));
$d->body->removeChild($t);
$this->assertFalse(ElementMap::has($t));
}
/**
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::loadDOM
@ -113,23 +148,24 @@ class TestDocument extends \PHPUnit\Framework\TestCase {
public function provideElementsNS(): iterable {
return [
// HTML element with a null namespace
[ null, null, 'div', 'div', Element::class ],
[ null, null, 'div', 'div', Element::class ],
// Template element with a null namespace
[ null, null, 'template', 'template', HTMLTemplateElement::class ],
[ null, null, 'template', 'template', HTMLTemplateElement::class ],
// Template element with a null namespace and uppercase name
[ null, null, 'TEMPLATE', 'TEMPLATE', HTMLTemplateElement::class ],
[ null, null, 'TEMPLATE', 'TEMPLATE', HTMLTemplateElement::class ],
// Template element
[ Parser::HTML_NAMESPACE, Parser::HTML_NAMESPACE, 'template', 'template', HTMLTemplateElement::class ],
[ Parser::HTML_NAMESPACE, Parser::HTML_NAMESPACE, 'template', 'template', HTMLTemplateElement::class ],
// SVG element with SVG namespace
[ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'svg', 'svg', Element::class ],
[ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'svg', 'svg', Element::class ],
// SVG element with SVG namespace and uppercase local name
[ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'SVG', 'SVG', Element::class ]
[ Parser::SVG_NAMESPACE, Parser::SVG_NAMESPACE, 'SVG', 'SVG', Element::class ]
];
}
/**
* @dataProvider provideElementsNS
* @covers \MensBeam\HTML\DOM\Document::createElementNS
* @covers \MensBeam\HTML\DOM\Document::validateAndExtract
*/
public function testElementCreationNS(?string $nsIn, ?string $nsOut, string $localIn, string $localOut, string $class): void {
$d = new Document;

18
tests/cases/TestSerializer.php

@ -10,16 +10,26 @@ use MensBeam\HTML\DOM\Document;
use MensBeam\HTML\Parser;
/**
* @covers \MensBeam\HTML\DOM\Comment
* @covers \MensBeam\HTML\DOM\Document
* @covers \MensBeam\HTML\DOM\DocumentFragment
* @covers \MensBeam\HTML\DOM\Element
* @covers \MensBeam\HTML\DOM\TemplateElement
* @covers \MensBeam\HTML\DOM\Comment
* @covers \MensBeam\HTML\DOM\Text
* @covers \MensBeam\HTML\DOM\HTMLTemplateElement
* @covers \MensBeam\HTML\DOM\ProcessingInstruction
* @covers \MensBeam\HTML\DOM\Text
*/
class TestSerializer extends \PHPUnit\Framework\TestCase {
/** @dataProvider provideStandardSerializerTests */
/**
* @dataProvider provideStandardSerializerTests
* @covers \MensBeam\HTML\DOM\Comment::__toString
* @covers \MensBeam\HTML\DOM\Document::saveHTML
* @covers \MensBeam\HTML\DOM\Document::__toString
* @covers \MensBeam\HTML\DOM\DocumentFragment::__toString
* @covers \MensBeam\HTML\DOM\Element::__toString
* @covers \MensBeam\HTML\DOM\HTMLTemplateElement::__toString
* @covers \MensBeam\HTML\DOM\ProcessingInstruction::__toString
* @covers \MensBeam\HTML\DOM\Text::__toString
*/
public function testStandardTreeTests(array $data, bool $fragment, string $exp): void {
$node = $this->buildTree($data, $fragment);
$this->assertSame($exp, (string) $node);

Loading…
Cancel
Save