documents/fragments, 100% test coverage

This commit is contained in:
Joby 2022-11-30 18:48:42 -07:00
parent 7298617363
commit 56658e5a47
20 changed files with 451 additions and 45 deletions

View file

@ -1,11 +0,0 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\ContainerInterface;
use ByJoby\HTML\Traits\ContainerTrait;
abstract class AbstractDocument implements DocumentInterface
{
use ContainerTrait;
}

View file

@ -3,15 +3,7 @@
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\ContainerInterface;
use ByJoby\HTML\Containers\DocumentTags\BodyTagInterface;
use ByJoby\HTML\Containers\DocumentTags\DoctypeInterface;
use ByJoby\HTML\Containers\DocumentTags\HeadTagInterface;
use ByJoby\HTML\Containers\DocumentTags\HtmlTagInterface;
interface DocumentInterface extends ContainerInterface
{
public function doctype(): DoctypeInterface;
public function html(): HtmlTagInterface;
public function head(): HeadTagInterface;
public function body(): BodyTagInterface;
}

View file

@ -0,0 +1,13 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Tags\AbstractContainerTag;
class BodyTag extends AbstractContainerTag implements BodyTagInterface
{
public function tag(): string
{
return 'body';
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Traits\NodeTrait;
class Doctype implements DoctypeInterface
{
use NodeTrait;
public function __toString(): string
{
return '<!DOCTYPE html>';
}
}

View file

@ -2,8 +2,8 @@
namespace ByJoby\HTML\Containers\DocumentTags;
use PhpParser\Node;
use ByJoby\HTML\NodeInterface;
interface DoctypeInterface extends Node
interface DoctypeInterface extends NodeInterface
{
}

View file

@ -0,0 +1,28 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Tags\AbstractContainerTag;
class HeadTag extends AbstractContainerTag implements HeadTagInterface
{
/** @var TitleTagInterface */
protected $title;
public function __construct()
{
parent::__construct();
$this->title = (new TitleTag);
$this->addChild($this->title);
}
public function title(): TitleTagInterface
{
return $this->title;
}
public function tag(): string
{
return 'head';
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Tags\AbstractContainerTag;
class HtmlTag extends AbstractContainerTag implements HtmlTagInterface
{
/** @var HeadTagInterface */
protected $head;
/** @var BodyTagInterface */
protected $body;
public function __construct()
{
parent::__construct();
$this->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;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Tags\AbstractContainerTag;
use ByJoby\HTML\Traits\NodeTrait;
use Exception;
class TitleTag implements TitleTagInterface
{
use NodeTrait;
/** @var string */
protected $title = 'Untitled';
public function setTitle(string $title): static
{
$this->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 '<title>' . $this->title() . '</title>';
}
}

View file

@ -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;

View file

@ -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<mixed,string|Stringable|NodeInterface>|Traversable<mixed,string|Stringable|NodeInterface>|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());

View file

@ -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
{
}

View file

@ -0,0 +1,61 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\Containers\DocumentTags\BodyTag;
use ByJoby\HTML\Containers\DocumentTags\BodyTagInterface;
use ByJoby\HTML\Containers\DocumentTags\Doctype;
use ByJoby\HTML\Containers\DocumentTags\DoctypeInterface;
use ByJoby\HTML\Containers\DocumentTags\HeadTag;
use ByJoby\HTML\Containers\DocumentTags\HeadTagInterface;
use ByJoby\HTML\Containers\DocumentTags\HtmlTag;
use ByJoby\HTML\Containers\DocumentTags\HtmlTagInterface;
class GenericHtmlDocument implements HtmlDocumentInterface
{
/** @var DoctypeInterface */
protected $doctype;
/** @var HtmlTagInterface */
protected $html;
public function __construct()
{
$this->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();
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\Containers\DocumentTags\BodyTagInterface;
use ByJoby\HTML\Containers\DocumentTags\DoctypeInterface;
use ByJoby\HTML\Containers\DocumentTags\HeadTagInterface;
use ByJoby\HTML\Containers\DocumentTags\HtmlTagInterface;
interface HtmlDocumentInterface extends DocumentInterface
{
public function doctype(): DoctypeInterface;
public function html(): HtmlTagInterface;
public function head(): HeadTagInterface;
public function body(): BodyTagInterface;
}

View file

@ -3,6 +3,7 @@
namespace ByJoby\HTML\Traits;
use ByJoby\HTML\ContainerMutableInterface;
use ByJoby\HTML\Containers\DocumentInterface;
use ByJoby\HTML\NodeInterface;
use ByJoby\HTML\Nodes\Text;
use ByJoby\HTML\Nodes\UnsanitizedText;
@ -18,12 +19,7 @@ trait ContainerMutableTrait
bool $prepend = false,
bool $skip_sanitize = false
): static {
$child = $this->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

View file

@ -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

View file

@ -0,0 +1,22 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use PHPUnit\Framework\TestCase;
class HeadTagTest extends TestCase
{
public function testTitle()
{
$head = new HeadTag;
$this->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();
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace ByJoby\HTML\Containers\DocumentTags;
use PHPUnit\Framework\TestCase;
class TitleTagTest extends TestCase
{
public function testGetAndSet()
{
$title = new TitleTag;
$this->assertEquals('Untitled', $title->title());
$title->setTitle('<strong>Titled</strong>');
$this->assertEquals('Titled', $title->title());
$this->assertEquals('<title>Titled</title>', $title->__toString());
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\Tags\AbstractContainerTag;
use ByJoby\HTML\Tags\AbstractTag;
use PHPUnit\Framework\TestCase;
class FragmentTest extends TestCase
{
public function testConstruction()
{
$empty = new Fragment();
$this->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());
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\Containers\DocumentTags\BodyTagInterface;
use ByJoby\HTML\Containers\DocumentTags\DoctypeInterface;
use ByJoby\HTML\Containers\DocumentTags\HeadTagInterface;
use ByJoby\HTML\Containers\DocumentTags\HtmlTagInterface;
use PHPUnit\Framework\TestCase;
class GenericHtmlDocumentTest extends TestCase
{
public function testConstruction(): void
{
$document = new GenericHtmlDocument;
// all the right classes
$this->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,
[
'<!DOCTYPE html>',
'<html>',
'<head>',
'<title>Untitled</title>',
'</head>',
'<body></body>',
'</html>'
]
),
$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();
}
}

View file

@ -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