rebuilt things using generic grouped containers, needs tests

This commit is contained in:
Joby 2022-12-12 18:34:42 -07:00
parent 82debbdb11
commit bc5991fdaa
13 changed files with 396 additions and 50 deletions

View file

@ -9,6 +9,10 @@ interface ContainerInterface extends Stringable
/** @return array<int,NodeInterface> */
public function children(): array;
public function contains(
NodeInterface|Stringable|string $child
): bool;
public function addChild(
NodeInterface|Stringable|string $child,
bool $prepend = false,

View file

@ -0,0 +1,144 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\ContainerInterface;
use ByJoby\HTML\NodeInterface;
use ByJoby\HTML\Tags\TagInterface;
use ByJoby\HTML\Traits\ContainerTrait;
use ByJoby\HTML\Traits\NodeTrait;
use Stringable;
/**
* @template T of NodeInterface
* @method array<int,T> children()
*/
class ContainerGroup implements ContainerInterface, NodeInterface
{
use NodeTrait;
use ContainerTrait {
ContainerTrait::contains as doContains;
ContainerTrait::addChild as doAddChild;
ContainerTrait::addChildBefore as doAddChildBefore;
ContainerTrait::addChildAfter as doAddChildAfter;
}
/** @var callable */
protected $validator;
/** @var int */
protected $limit = 0;
/**
* @return ContainerGroup<NodeInterface>
*/
public static function catchAll(): ContainerGroup
{
return new ContainerGroup(
function () {
return true;
},
0
);
}
/**
* @template C of T
* @param class-string<C> $class
* @param int $limit
* @return ContainerGroup<C>
*/
public static function ofClass(string $class, int $limit = 0): ContainerGroup
{
return new ContainerGroup( // @phpstan-ignore-line
function (NodeInterface $child) use ($class): bool {
return $child instanceof $class;
},
$limit
);
}
/**
* @param string $tag
* @param int $limit
* @return ContainerGroup<TagInterface>
*/
public static function ofTag(string $tag, int $limit = 0): ContainerGroup
{
return new ContainerGroup( // @phpstan-ignore-line
function (NodeInterface $child) use ($tag): bool {
if ($child instanceof TagInterface) {
return $child->tag() == $tag;
} else {
return false;
}
},
$limit
);
}
public function addChild(NodeInterface|Stringable|string $child, bool $prepend = false, bool $skip_sanitize = false): static
{
if ($this->willAccept($child)) {
$this->makeRoomForChild($prepend);
$this->doAddChild($child, $prepend, $skip_sanitize);
}
return $this;
}
public function addChildAfter(NodeInterface|Stringable|string $new_child, NodeInterface|Stringable|string $after_child, bool $skip_sanitize = false): static
{
if ($this->willAccept($new_child)) {
$this->makeRoomForChild(false);
$this->doAddChildAfter($new_child, $after_child, $skip_sanitize);
}
return $this;
}
public function addChildBefore(NodeInterface|Stringable|string $new_child, NodeInterface|Stringable|string $before_child, bool $skip_sanitize = false): static
{
if ($this->willAccept($new_child)) {
$this->makeRoomForChild(true);
$this->doAddChildBefore($new_child, $before_child, $skip_sanitize);
}
return $this;
}
protected function makeRoomForChild(bool $remove_from_end): void
{
if ($this->limit > 0) {
while (count($this->children) > $this->limit) {
if ($remove_from_end) {
$child = array_pop($this->children);
} else {
$child = array_shift($this->children);
}
$child->setParent(null);
}
}
}
public function willAccept(NodeInterface|Stringable|string $child, bool $check_limit = true): bool
{
if ($check_limit && $this->limit > 0) {
if (count($this->children()) >= $this->limit) {
return false;
}
}
if ($child instanceof NodeInterface) {
$child = $child->detachCopy();
}
$child = $this->prepareChildToAdd($child, false);
return !!call_user_func($this->validator, $child);
}
public function __construct(callable $validator, int $limit = 0)
{
$this->validator = $validator;
$this->limit = $limit;
}
public function __toString(): string
{
return implode(PHP_EOL, $this->children());
}
}

View file

@ -2,24 +2,36 @@
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Tags\AbstractContainerTag;
use ByJoby\HTML\Containers\ContainerGroup;
use ByJoby\HTML\Tags\AbstractGroupedTag;
use ByJoby\HTML\Traits\GroupedContainerTrait;
class HeadTag extends AbstractContainerTag implements HeadTagInterface
class HeadTag extends AbstractGroupedTag implements HeadTagInterface
{
const TAG = 'head';
/** @var TitleTagInterface */
use GroupedContainerTrait;
/** @var ContainerGroup<TitleTagInterface> */
protected $title;
public function __construct()
{
parent::__construct();
$this->title = (new TitleTag);
$this->addChild($this->title);
$this->title = ContainerGroup::ofClass(TitleTagInterface::class, 1);
$this->addGroup($this->title);
$this->addChild(new TitleTag);
$this->addGroup(ContainerGroup::ofTag('meta'));
$this->addGroup(ContainerGroup::ofTag('base', 1));
$this->addGroup(ContainerGroup::ofTag('style'));
$this->addGroup(ContainerGroup::ofTag('script'));
$this->addGroup(ContainerGroup::ofTag('noscript'));
$this->addGroup(ContainerGroup::ofTag('template', 1));
$this->addGroup(ContainerGroup::catchAll());
}
public function title(): TitleTagInterface
{
return $this->title;
return $this->title->children()[0];
}
}

View file

@ -2,39 +2,39 @@
namespace ByJoby\HTML\Containers\DocumentTags;
use ByJoby\HTML\Tags\AbstractContainerTag;
use ByJoby\HTML\Containers\ContainerGroup;
use ByJoby\HTML\Tags\AbstractGroupedTag;
use ByJoby\HTML\Traits\GroupedContainerTrait;
class HtmlTag extends AbstractContainerTag implements HtmlTagInterface
class HtmlTag extends AbstractGroupedTag implements HtmlTagInterface
{
const TAG = 'html';
/** @var HeadTagInterface */
use GroupedContainerTrait;
/** @var ContainerGroup<HeadTagInterface> */
protected $head;
/** @var BodyTagInterface */
/** @var ContainerGroup<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()
];
$this->head = ContainerGroup::ofClass(HeadTagInterface::class);
$this->body = ContainerGroup::ofClass(BodyTagInterface::class, 1);
$this->addGroup($this->head);
$this->addGroup($this->body);
$this->addChild(new HeadTag);
$this->addChild(new BodyTag);
}
public function head(): HeadTagInterface
{
return $this->head;
return $this->head->children()[0];
}
public function body(): BodyTagInterface
{
return $this->body;
return $this->body->children()[0];
}
}

View file

@ -8,33 +8,35 @@ use ByJoby\HTML\Containers\DocumentTags\DoctypeInterface;
use ByJoby\HTML\Containers\DocumentTags\HeadTagInterface;
use ByJoby\HTML\Containers\DocumentTags\HtmlTag;
use ByJoby\HTML\Containers\DocumentTags\HtmlTagInterface;
use ByJoby\HTML\Traits\ContainerTrait;
use ByJoby\HTML\Traits\GroupedContainerTrait;
class GenericHtmlDocument implements HtmlDocumentInterface
{
use ContainerTrait;
use GroupedContainerTrait;
/** @var DoctypeInterface */
/** @var ContainerGroup<DoctypeInterface> */
protected $doctype;
/** @var HtmlTagInterface */
/** @var ContainerGroup<HtmlTagInterface> */
protected $html;
public function __construct()
{
$this->doctype = (new Doctype);
$this->html = (new HtmlTag);
$this->addChild($this->doctype);
$this->addChild($this->html);
$this->doctype = ContainerGroup::ofClass(DoctypeInterface::class, 1);
$this->html = ContainerGroup::ofClass(HtmlTagInterface::class, 1);
$this->addGroup($this->doctype);
$this->addGroup($this->html);
$this->addChild(new Doctype);
$this->addChild(new HtmlTag);
}
public function doctype(): DoctypeInterface
{
return $this->doctype;
return $this->doctype->children()[0];
}
public function html(): HtmlTagInterface
{
return $this->html;
return $this->html->children()[0];
}
public function head(): HeadTagInterface
@ -47,18 +49,16 @@ class GenericHtmlDocument implements HtmlDocumentInterface
return $this->html()->body();
}
public function children(): array
{
return [
$this->doctype(),
$this->html()
];
}
public function __toString(): string
{
return $this->doctype()
. PHP_EOL
. $this->html();
return implode(
PHP_EOL,
array_filter(
$this->groups(),
function (ContainerGroup $group) {
return !!$group->children();
}
)
);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\ContainerInterface;
use ByJoby\HTML\NodeInterface;
use ByJoby\HTML\Traits\GroupedContainerTrait;
use ByJoby\HTML\Traits\NodeTrait;
class GroupedContainer implements ContainerInterface, NodeInterface
{
use NodeTrait;
use GroupedContainerTrait;
public function __toString(): string
{
return implode(
PHP_EOL,
array_filter(
$this->groups(),
function (ContainerGroup $group) {
return !!$group->children();
}
)
);
}
}

View file

@ -2,6 +2,31 @@
namespace ByJoby\HTML\Tags;
abstract class AbstractGroupedTag extends AbstractContainerTag
use ByJoby\HTML\Containers\ContainerGroup;
use ByJoby\HTML\Traits\GroupedContainerTrait;
abstract class AbstractGroupedTag extends AbstractTag implements ContainerTagInterface
{
use GroupedContainerTrait;
public function __toString(): string
{
$openingTag = sprintf('<%s>', implode(' ', $this->openingTagStrings()));
$closingTag = sprintf('</%s>', $this->tag());
$groups = array_filter(
$this->groups(),
function (ContainerGroup $group) {
return !!$group->children();
}
);
if (!$groups) return $openingTag . $closingTag;
else return implode(
PHP_EOL,
[
$openingTag,
implode(PHP_EOL, $groups),
$closingTag
]
);
}
}

View file

@ -19,6 +19,16 @@ trait ContainerTrait
return $this->children;
}
public function contains(
NodeInterface|Stringable|string $child
): bool {
if ($child instanceof NodeInterface) {
return $child->parent() === $this;
} else {
return $this->indexOfChild($child) !== null;
}
}
public function addChild(
NodeInterface|Stringable|string $child,
bool $prepend = false,

View file

@ -0,0 +1,105 @@
<?php
namespace ByJoby\HTML\Traits;
use ByJoby\HTML\Containers\ContainerGroup;
use ByJoby\HTML\NodeInterface;
use Stringable;
/**
* @method bool containsGroup(ContainerGroup $group)
* @method static addGroup(ContainerGroup $group, bool $prepend=false, bool $skip_sanitize=false)
*/
trait GroupedContainerTrait
{
use ContainerTrait {
ContainerTrait::contains as containsGroup;
ContainerTrait::addChild as addGroup;
ContainerTrait::removeChild as removeGroup;
ContainerTrait::addChildBefore as addGroupBefore;
ContainerTrait::addChildAfter as addGroupAfter;
}
/**
* @return array<int,ContainerGroup<NodeInterface>>
*/
public function groups(): array
{
return array_filter(
$this->children(),
function (NodeInterface $node) {
return $node instanceof ContainerGroup;
}
);
}
public function willAccept(NodeInterface|Stringable|string $child): bool
{
foreach ($this->groups() as $group) {
if ($group->willAccept($child)) return true;
}
return false;
}
public function contains(
NodeInterface|Stringable|string $child
): bool {
foreach ($this->groups() as $group) {
if ($group->contains($child)) {
return true;
}
}
return false;
}
public function addChild(
NodeInterface|Stringable|string $child,
bool $prepend = false,
bool $skip_sanitize = false
): static {
foreach ($this->groups() as $group) {
if ($group->willAccept($child)) {
$group->addChild($child, $prepend, $skip_sanitize);
break;
}
}
return $this;
}
public function removeChild(
NodeInterface|Stringable|string $child
): static {
foreach ($this->groups() as $group) {
$group->removeChild($child);
}
return $this;
}
public function addChildBefore(
NodeInterface|Stringable|string $new_child,
NodeInterface|Stringable|string $before_child,
bool $skip_sanitize = false
): static {
foreach ($this->groups() as $group) {
if ($group->willAccept($new_child) && $group->contains($before_child)) {
$group->addChildBefore($new_child, $before_child, $skip_sanitize);
break;
}
}
return $this;
}
public function addChildAfter(
NodeInterface|Stringable|string $new_child,
NodeInterface|Stringable|string $after_child,
bool $skip_sanitize = false
): static {
foreach ($this->groups() as $group) {
if ($group->willAccept($new_child) && $group->contains($after_child)) {
$group->addChildAfter($new_child, $after_child, $skip_sanitize);
break;
}
}
return $this;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace ByJoby\HTML\Containers;
use PHPUnit\Framework\TestCase;
class ContainerGroupTest extends TestCase
{
public function testCatchAll()
{
$group = ContainerGroup::catchAll();
$this->assertTrue($group->willAccept('Foo'));
}
public function testCatchNone()
{
$group = new ContainerGroup(function () {
return false;
});
$this->assertFalse($group->willAccept('Foo'));
}
}

View file

@ -10,6 +10,6 @@ class HeadTagTest extends TestCase
{
$head = new HeadTag;
$this->assertInstanceOf(TitleTagInterface::class, $head->title());
$this->assertEquals($head, $head->title()->parent());
$this->assertEquals($head, $head->title()->parentTag());
}
}

View file

@ -3,7 +3,6 @@
namespace ByJoby\HTML\Containers;
use ByJoby\HTML\Tags\AbstractContainerTag;
use ByJoby\HTML\Tags\AbstractTag;
use PHPUnit\Framework\TestCase;
class FragmentTest extends TestCase

View file

@ -19,15 +19,13 @@ class GenericHtmlDocumentTest extends TestCase
$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());
$this->assertEquals($document->body(), $document->html()->body());
$this->assertEquals($document->head(), $document->html()->head());
// everything has the correct document
$this->assertEquals($document, $document->doctype()->parentDocument());
$this->assertEquals($document, $document->html()->parentDocument());
$this->assertEquals($document, $document->body()->parentDocument());
$this->assertEquals($document, $document->head()->parentDocument());
// children are doctype and html
$this->assertEquals([$document->doctype(), $document->html()], $document->children());
// string version of an empty document
$this->assertEquals(
implode(