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