diff --git a/lib/Element.php b/lib/Element.php index 4b45fcb..d710ba0 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -193,6 +193,77 @@ class Element extends Node { $parent->replaceChild($fragment, $this); } + protected function __get_outerText(): ?string { + # The innerText and outerText getter steps are: + # 1. If this is not being rendered or if the user agent is a non-CSS user agent, + # then return this's descendant text content. + // This is a non-CSS user agent. Nothing else to do here. + return $this->__get_textContent(); + } + + protected function __set_outerText(string $value): void { + # The outerText setter steps are: + # 1. If this's parent is null, then throw a "NoModificationAllowedError" + # DOMException. + $innerNode = $this->innerNode; + if ($this->parentNode === null) { + throw new DOMException(DOMException::NO_MODIFICATION_ALLOWED); + } + + # 2. Let next be this's next sibling. + $next = $innerNode->nextSibling; + + # 3. Let previous be this's previous sibling. + $previous = $innerNode->previousSibling; + + # 4. Let fragment be the rendered text fragment for the given value given this's node + # document. + $fragment = $this->getRenderedTextFragment($value); + + # 5. Replace this with fragment within this's parent. + // Check for child nodes before appending to prevent a stupid warning. + if ($fragment->hasChildNodes()) { + $innerNode->parentNode->replaceChild($fragment, $innerNode); + } else { + $innerNode->parentNode->removeChild($innerNode); + } + + # 6. If next is non-null and next's previous sibling is a Text node, then merge + # with the next text node given next's previous sibling. + if ($next !== null && $next->previousSibling instanceof \DOMText) { + # To merge with the next text node given a Text node node: + # 1. Let next be node's next sibling. + # 2. If next is not a Text node, then return. + // Already checked for + + # 3. Replace data with node, node's data's length, 0, and next's data. + $next->previousSibling->data .= $next->data; + + # 4. If next's parent is non-null, then remove next. + // DEVIATION: There are no mutation events in this implementation, so there's no + // reason to check for a parent here. + $next->parentNode->removeChild($next); + } + + # 7. If previous is a Text node, then merge with the next text node given previous. + if ($previous instanceof \DOMText) { + # To merge with the next text node given a Text node node: + # 1. Let next be node's next sibling. + $next = $previous->nextSibling; + + # 2. If next is not a Text node, then return. + if ($next instanceof \DOMText) { + # 3. Replace data with node, node's data's length, 0, and next's data. + $previous->data .= $next->data; + + # 4. If next's parent is non-null, then remove next. + // DEVIATION: There are no mutation events in this implementation, so there's no + // reason to check for a parent here. + $next->parentNode->removeChild($next); + } + } + } + protected function __get_prefix(): ?string { $prefix = $this->innerNode->prefix; return ($prefix !== '') ? $prefix : null; diff --git a/lib/Inner/Document.php b/lib/Inner/Document.php index 4912c2f..3ac4fde 100644 --- a/lib/Inner/Document.php +++ b/lib/Inner/Document.php @@ -53,7 +53,8 @@ class Document extends \DOMDocument { $this->_wrapperNode = \WeakReference::create($wrapperNode); if (self::$parentNamespace === null) { - self::$parentNamespace = substr(__NAMESPACE__, 0, strrpos(__NAMESPACE__, '\\')); + // This line is covered, but pcov declares it not covered for some reason... + self::$parentNamespace = substr(__NAMESPACE__, 0, strrpos(__NAMESPACE__, '\\')); // @codeCoverageIgnore } } diff --git a/tests/cases/TestElement.php b/tests/cases/TestElement.php index ce98ebb..75c074d 100644 --- a/tests/cases/TestElement.php +++ b/tests/cases/TestElement.php @@ -1375,7 +1375,12 @@ class TestElement extends \PHPUnit\Framework\TestCase { /** * @covers \MensBeam\HTML\DOM\Element::__get_innerText * @covers \MensBeam\HTML\DOM\Element::__set_innerText + * @covers \MensBeam\HTML\DOM\Element::__get_outerText + * @covers \MensBeam\HTML\DOM\Element::__set_outerText * + * @covers \MensBeam\HTML\DOM\Collection::__construct + * @covers \MensBeam\HTML\DOM\Collection::__get_length + * @covers \MensBeam\HTML\DOM\Collection::count * @covers \MensBeam\HTML\DOM\Document::__construct * @covers \MensBeam\HTML\DOM\Document::__get_body * @covers \MensBeam\HTML\DOM\Document::__get_documentElement @@ -1386,6 +1391,8 @@ class TestElement extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Element::__get_innerHTML * @covers \MensBeam\HTML\DOM\Element::getRenderedTextFragment * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_childNodes + * @covers \MensBeam\HTML\DOM\Node::__get_parentNode * @covers \MensBeam\HTML\DOM\Node::__get_textContent * @covers \MensBeam\HTML\DOM\Node::appendChild * @covers \MensBeam\HTML\DOM\Node::getInnerDocument @@ -1405,21 +1412,58 @@ class TestElement extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty */ - public function testProperty_innerText() { + public function testProperty_innerText_outerText() { $d = new Document(); $d->appendChild($d->createElement('html')); $d->documentElement->appendChild($d->createElement('body')); - $s = $d->body->appendChild($d->createElement('span')); + $body = $d->body; + $body->appendChild($d->createTextNode('ook ')); + $s = $body->appendChild($d->createElement('span')); $s->appendChild($d->createTextNode('ook')); - $this->assertSame('ook', $d->body->innerHTML); - - $d->body->innerText = <<appendChild($d->createTextNode(' eek')); + $this->assertSame('ook ook eek', $body->innerHTML); + $s->innerText = <<assertSame('ookook eek ook', $d->body->innerText); - $this->assertSame('ook

ook eek ook', $d->body->innerHTML); + $this->assertSame('ook ookook eek ook eek', $body->innerText); + $this->assertSame('ook

ook eek ook', $s->innerHTML); + + $s->outerText = 'ack'; + $this->assertSame('ook ack eek', $body->outerText); + $this->assertEquals(1, $body->childNodes->length); + + $s = $body->appendChild($d->createElement('span')); + $s->outerText = ''; + $this->assertSame('ook ack eek', $body->outerText); + } + + + /** + * @covers \MensBeam\HTML\DOM\Element::__set_outerText + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMException::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_parentNode + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @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 + */ + public function testProperty_outerText__errors() { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::NO_MODIFICATION_ALLOWED); + $d = new Document(); + $h = $d->createElement('html'); + $h->outerText = 'fail'; }