From 56658e5a471968d69a6d889e5ee4b6cbce990478 Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Wed, 30 Nov 2022 18:48:42 -0700 Subject: [PATCH] documents/fragments, 100% test coverage --- src/Containers/AbstractDocument.php | 11 --- src/Containers/DocumentInterface.php | 8 -- src/Containers/DocumentTags/BodyTag.php | 13 +++ src/Containers/DocumentTags/Doctype.php | 15 ++++ .../DocumentTags/DoctypeInterface.php | 4 +- src/Containers/DocumentTags/HeadTag.php | 28 ++++++ src/Containers/DocumentTags/HtmlTag.php | 43 +++++++++ src/Containers/DocumentTags/TitleTag.php | 36 ++++++++ .../DocumentTags/TitleTagInterface.php | 4 +- src/Containers/Fragment.php | 14 +++ src/Containers/FragmentInterface.php | 2 +- src/Containers/GenericHtmlDocument.php | 61 +++++++++++++ src/Containers/HtmlDocumentInterface.php | 16 ++++ src/Traits/ContainerMutableTrait.php | 36 +++++--- src/Traits/NodeTrait.php | 14 +-- tests/Containers/DocumentTags/HeadTagTest.php | 22 +++++ .../Containers/DocumentTags/TitleTagTest.php | 17 ++++ tests/Containers/FragmentTest.php | 87 +++++++++++++++++++ tests/Containers/GenericHtmlDocumentTest.php | 62 +++++++++++++ tests/Tags/AbstractContainerTagTest.php | 3 +- 20 files changed, 451 insertions(+), 45 deletions(-) delete mode 100644 src/Containers/AbstractDocument.php create mode 100644 src/Containers/DocumentTags/BodyTag.php create mode 100644 src/Containers/DocumentTags/Doctype.php create mode 100644 src/Containers/DocumentTags/HeadTag.php create mode 100644 src/Containers/DocumentTags/HtmlTag.php create mode 100644 src/Containers/DocumentTags/TitleTag.php create mode 100644 src/Containers/GenericHtmlDocument.php create mode 100644 src/Containers/HtmlDocumentInterface.php create mode 100644 tests/Containers/DocumentTags/HeadTagTest.php create mode 100644 tests/Containers/DocumentTags/TitleTagTest.php create mode 100644 tests/Containers/FragmentTest.php create mode 100644 tests/Containers/GenericHtmlDocumentTest.php diff --git a/src/Containers/AbstractDocument.php b/src/Containers/AbstractDocument.php deleted file mode 100644 index 09cab2c..0000000 --- a/src/Containers/AbstractDocument.php +++ /dev/null @@ -1,11 +0,0 @@ -'; + } +} diff --git a/src/Containers/DocumentTags/DoctypeInterface.php b/src/Containers/DocumentTags/DoctypeInterface.php index f143b4b..b1fd6eb 100644 --- a/src/Containers/DocumentTags/DoctypeInterface.php +++ b/src/Containers/DocumentTags/DoctypeInterface.php @@ -2,8 +2,8 @@ namespace ByJoby\HTML\Containers\DocumentTags; -use PhpParser\Node; +use ByJoby\HTML\NodeInterface; -interface DoctypeInterface extends Node +interface DoctypeInterface extends NodeInterface { } diff --git a/src/Containers/DocumentTags/HeadTag.php b/src/Containers/DocumentTags/HeadTag.php new file mode 100644 index 0000000..d626e94 --- /dev/null +++ b/src/Containers/DocumentTags/HeadTag.php @@ -0,0 +1,28 @@ +title = (new TitleTag); + $this->addChild($this->title); + } + + public function title(): TitleTagInterface + { + return $this->title; + } + + public function tag(): string + { + return 'head'; + } +} diff --git a/src/Containers/DocumentTags/HtmlTag.php b/src/Containers/DocumentTags/HtmlTag.php new file mode 100644 index 0000000..36e58d0 --- /dev/null +++ b/src/Containers/DocumentTags/HtmlTag.php @@ -0,0 +1,43 @@ +head = (new HeadTag)->setParent($this); + $this->body = (new BodyTag)->setParent($this); + } + + public function children(): array + { + return [ + $this->head(), + $this->body() + ]; + } + + public function tag(): string + { + return 'html'; + } + + public function head(): HeadTagInterface + { + return $this->head; + } + + public function body(): BodyTagInterface + { + return $this->body; + } +} diff --git a/src/Containers/DocumentTags/TitleTag.php b/src/Containers/DocumentTags/TitleTag.php new file mode 100644 index 0000000..5e408a2 --- /dev/null +++ b/src/Containers/DocumentTags/TitleTag.php @@ -0,0 +1,36 @@ +title = trim(strip_tags($title)); + return $this; + } + + public function title(): string + { + return $this->title; + } + + public function detach(): static + { + throw new Exception('Not allowed to detach TitleTag'); + } + + public function __toString(): string + { + return '' . $this->title() . ''; + } +} diff --git a/src/Containers/DocumentTags/TitleTagInterface.php b/src/Containers/DocumentTags/TitleTagInterface.php index 026cb5d..fa0e945 100644 --- a/src/Containers/DocumentTags/TitleTagInterface.php +++ b/src/Containers/DocumentTags/TitleTagInterface.php @@ -2,9 +2,9 @@ namespace ByJoby\HTML\Containers\DocumentTags; -use ByJoby\HTML\Tags\TagInterface; +use ByJoby\HTML\NodeInterface; -interface TitleTagInterface extends TagInterface +interface TitleTagInterface extends NodeInterface { public function title(): string; public function setTitle(string $title): static; diff --git a/src/Containers/Fragment.php b/src/Containers/Fragment.php index 31e1b41..4f5214b 100644 --- a/src/Containers/Fragment.php +++ b/src/Containers/Fragment.php @@ -2,14 +2,28 @@ namespace ByJoby\HTML\Containers; +use ByJoby\HTML\NodeInterface; use ByJoby\HTML\Traits\ContainerMutableTrait; use ByJoby\HTML\Traits\ContainerTrait; +use Stringable; +use Traversable; class Fragment implements FragmentInterface { use ContainerTrait; use ContainerMutableTrait; + /** + * @param null|array|Traversable|null $children + */ + public function __construct(null|array|Traversable $children = null) + { + if (!$children) return; + foreach ($children as $child) { + $this->addChild($child); + } + } + public function __toString(): string { return implode(PHP_EOL, $this->children()); diff --git a/src/Containers/FragmentInterface.php b/src/Containers/FragmentInterface.php index f7b76bf..7215815 100644 --- a/src/Containers/FragmentInterface.php +++ b/src/Containers/FragmentInterface.php @@ -5,6 +5,6 @@ namespace ByJoby\HTML\Containers; use ByJoby\HTML\ContainerInterface; use ByJoby\HTML\ContainerMutableInterface; -interface FragmentInterface extends ContainerMutableInterface +interface FragmentInterface extends DocumentInterface, ContainerMutableInterface { } diff --git a/src/Containers/GenericHtmlDocument.php b/src/Containers/GenericHtmlDocument.php new file mode 100644 index 0000000..ccfe18b --- /dev/null +++ b/src/Containers/GenericHtmlDocument.php @@ -0,0 +1,61 @@ +doctype = (new Doctype)->setDocument($this); + $this->html = (new HtmlTag)->setDocument($this); + } + + public function doctype(): DoctypeInterface + { + return $this->doctype; + } + + public function html(): HtmlTagInterface + { + return $this->html; + } + + public function head(): HeadTagInterface + { + return $this->html()->head(); + } + + public function body(): BodyTagInterface + { + return $this->html()->body(); + } + + public function children(): array + { + return [ + $this->doctype(), + $this->html() + ]; + } + + public function __toString(): string + { + return $this->doctype() + . PHP_EOL + . $this->html(); + } +} diff --git a/src/Containers/HtmlDocumentInterface.php b/src/Containers/HtmlDocumentInterface.php new file mode 100644 index 0000000..69cc294 --- /dev/null +++ b/src/Containers/HtmlDocumentInterface.php @@ -0,0 +1,16 @@ +normalizeChild($child, $skip_sanitize); - if ($this instanceof NodeInterface) { - $child->detach(); - $child->setParent($this); - $child->setDocument($this->document()); - } + $child = $this->prepareChildToAdd($child, $skip_sanitize); if ($prepend) array_unshift($this->children, $child); else $this->children[] = $child; return $this; @@ -51,7 +47,7 @@ trait ContainerMutableTrait if ($i === null) { throw new Exception('Reference child not found in this container'); } - $new_child = $this->normalizeChild($new_child, $skip_sanitize); + $new_child = $this->prepareChildToAdd($new_child, $skip_sanitize); array_splice($this->children, $i, 0, [$new_child]); return $this; } @@ -65,19 +61,31 @@ trait ContainerMutableTrait if ($i === null) { throw new Exception('Reference child not found in this container'); } - $new_child = $this->normalizeChild($new_child, $skip_sanitize); + $new_child = $this->prepareChildToAdd($new_child, $skip_sanitize); array_splice($this->children, $i + 1, 0, [$new_child]); return $this; } - protected function normalizeChild(NodeInterface|Stringable|string $child, bool $skip_sanitize): NodeInterface + protected function prepareChildToAdd(NodeInterface|Stringable|string $child, bool $skip_sanitize): NodeInterface { - if ($child instanceof NodeInterface) { - return $child; - } else { - if ($skip_sanitize) return new UnsanitizedText($child); - else return new Text($child); + if (!($child instanceof NodeInterface)) { + if ($skip_sanitize) $child = new UnsanitizedText($child); + else $child = new Text($child); } + if ($this instanceof NodeInterface) { + if ($child->parent() || $child->document()) { + $child->detach(); + } + $child->setParent($this); + $child->setDocument($this->document()); + } + if ($this instanceof DocumentInterface) { + if ($child->parent() || $child->document()) { + $child->detach(); + } + $child->setDocument($this); + } + return $child; } protected function indexOfChild(NodeInterface|Stringable|string $child): null|int diff --git a/src/Traits/NodeTrait.php b/src/Traits/NodeTrait.php index d4169cb..f3d0ead 100644 --- a/src/Traits/NodeTrait.php +++ b/src/Traits/NodeTrait.php @@ -47,15 +47,17 @@ trait NodeTrait public function detach(): static { - if (!$this->parent()) return $this; - if ($this->parent() instanceof ContainerMutableInterface) { - $this->parent()->removeChild($this); + $parent = $this->parent() ?? $this->document(); + if ($parent === null) { + return $this; + } elseif ($parent instanceof ContainerMutableInterface) { + $parent->removeChild($this); + $this->setParent(null); + $this->setDocument(null); + return $this; } else { throw new Exception('Cannot detach() a Node from a parent that is not a ContainerMutableInterface, use detachCopy() instead'); } - $this->setParent(null); - $this->setDocument(null); - return $this; } public function detachCopy(): static diff --git a/tests/Containers/DocumentTags/HeadTagTest.php b/tests/Containers/DocumentTags/HeadTagTest.php new file mode 100644 index 0000000..4645166 --- /dev/null +++ b/tests/Containers/DocumentTags/HeadTagTest.php @@ -0,0 +1,22 @@ +assertInstanceOf(TitleTagInterface::class, $head->title()); + $this->assertEquals($head, $head->title()->parent()); + } + + public function testNoTitleDetach() + { + $head = new HeadTag; + $this->expectExceptionMessage('Not allowed to detach TitleTag'); + $head->title()->detach(); + } +} diff --git a/tests/Containers/DocumentTags/TitleTagTest.php b/tests/Containers/DocumentTags/TitleTagTest.php new file mode 100644 index 0000000..5e86f3a --- /dev/null +++ b/tests/Containers/DocumentTags/TitleTagTest.php @@ -0,0 +1,17 @@ +assertEquals('Untitled', $title->title()); + $title->setTitle('Titled'); + $this->assertEquals('Titled', $title->title()); + $this->assertEquals('Titled', $title->__toString()); + } +} diff --git a/tests/Containers/FragmentTest.php b/tests/Containers/FragmentTest.php new file mode 100644 index 0000000..e9def6e --- /dev/null +++ b/tests/Containers/FragmentTest.php @@ -0,0 +1,87 @@ +assertEquals('', $empty->__toString()); + $full = new Fragment(['a', 'b']); + $this->assertEquals('a' . PHP_EOL . 'b', $full->__toString()); + } + + public function testNestingDocument(): Fragment + { + $fragment = new Fragment(); + $div1 = $this->getMockForAbstractClass(AbstractContainerTag::class); + $div1->method('tag')->will($this->returnValue('div')); + $div2 = $this->getMockForAbstractClass(AbstractContainerTag::class); + $div2->method('tag')->will($this->returnValue('div')); + // adding div1 to fragment sets its fragment + $fragment->addChild($div1); + $this->assertEquals($fragment, $div1->document()); + // adding div2 to div1 sets its document + $div1->addChild($div2); + $this->assertEquals($fragment, $div2->document()); + return $fragment; + } + + /** @depends clone testNestingDocument */ + public function testDetaching(Fragment $fragment): void + { + /** @var AbstractContainerTag */ + $div1 = $fragment->children()[0]; + /** @var AbstractContainerTag */ + $div2 = $div1->children()[0]; + // add a span and verify it has the right parent + $span = $this->getMockForAbstractClass(AbstractTag::class); + $span->method('tag')->will($this->returnValue('span')); + $div2->addChild($span); + $this->assertEquals($fragment, $span->document()); + // detach and check document/parent of all nodes + $div1->detach(); + $this->assertNull($div1->document()); + $this->assertNull($div1->parent()); + $this->assertNull($div2->document()); + $this->assertEquals($div1, $div2->parent()); + $this->assertNull($span->document()); + $this->assertEquals($div2, $span->parent()); + // try detaching again, to verify detaching a detached node does nothing + $div1->detach(); + $this->assertNull($div1->document()); + $this->assertNull($div1->parent()); + $this->assertNull($div2->document()); + $this->assertEquals($div1, $div2->parent()); + $this->assertNull($span->document()); + $this->assertEquals($div2, $span->parent()); + } + + public function testMovingChild(): void + { + $fragment = new Fragment(['a', 'b']); + $fragment->addChild($fragment->children()[0]); + $this->assertEquals('b' . PHP_EOL . 'a', $fragment->__toString()); + } + + /** @depends clone testNestingDocument */ + public function testAddBeforeAndAfterOnChildren(Fragment $fragment): void + { + /** @var AbstractContainerTag */ + $div1 = $fragment->children()[0]; + /** @var AbstractContainerTag */ + $div2 = $div1->children()[0]; + $div2->addChild('a'); + // add child before a + $div2->addChildBefore('b', 'a'); + $this->assertEquals($fragment, $div2->children()[0]->document()); + // add child after a + $div2->addChildAfter('c', 'a'); + $this->assertEquals($fragment, $div2->children()[2]->document()); + } +} diff --git a/tests/Containers/GenericHtmlDocumentTest.php b/tests/Containers/GenericHtmlDocumentTest.php new file mode 100644 index 0000000..75feb34 --- /dev/null +++ b/tests/Containers/GenericHtmlDocumentTest.php @@ -0,0 +1,62 @@ +assertInstanceOf(DoctypeInterface::class, $document->doctype()); + $this->assertInstanceOf(HtmlTagInterface::class, $document->html()); + $this->assertInstanceOf(BodyTagInterface::class, $document->body()); + $this->assertInstanceOf(HeadTagInterface::class, $document->head()); + // body and head are being passed properly + $this->assertTrue($document->body() === $document->html()->body()); + $this->assertTrue($document->head() === $document->html()->head()); + // everything has the correct document + $this->assertEquals($document, $document->doctype()->document()); + $this->assertEquals($document, $document->html()->document()); + $this->assertEquals($document, $document->body()->document()); + $this->assertEquals($document, $document->head()->document()); + // children are doctype and html + $this->assertEquals([$document->doctype(), $document->html()], $document->children()); + // string version of an empty document + $this->assertEquals( + implode( + PHP_EOL, + [ + '', + '', + '', + 'Untitled', + '', + '', + '' + ] + ), + $document->__toString() + ); + } + + public function testNoDoctypeDetach(): void + { + $document = new GenericHtmlDocument; + $this->expectExceptionMessage('Cannot detach() a Node from a parent that is not a ContainerMutableInterface, use detachCopy() instead'); + $document->doctype()->detach(); + } + + public function testNoHtmlDetach(): void + { + $document = new GenericHtmlDocument; + $this->expectExceptionMessage('Cannot detach() a Node from a parent that is not a ContainerMutableInterface, use detachCopy() instead'); + $document->html()->detach(); + } +} diff --git a/tests/Tags/AbstractContainerTagTest.php b/tests/Tags/AbstractContainerTagTest.php index 7f692a5..d07bc84 100644 --- a/tests/Tags/AbstractContainerTagTest.php +++ b/tests/Tags/AbstractContainerTagTest.php @@ -118,7 +118,8 @@ class AbstractContainerTagTest extends TestCase $div->addChildBefore('z', 'x'); } - public function testAddChildAfter(): void { + public function testAddChildAfter(): void + { $div = $this->getMockForAbstractClass(AbstractContainerTag::class); $div->method('tag')->will($this->returnValue('div')); // add a string child