rebuilt things using generic grouped containers, needs tests
This commit is contained in:
parent
82debbdb11
commit
bc5991fdaa
13 changed files with 396 additions and 50 deletions
|
@ -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,
|
||||
|
|
144
src/Containers/ContainerGroup.php
Normal file
144
src/Containers/ContainerGroup.php
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
27
src/Containers/GroupedContainer.php
Normal file
27
src/Containers/GroupedContainer.php
Normal 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();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
105
src/Traits/GroupedContainerTrait.php
Normal file
105
src/Traits/GroupedContainerTrait.php
Normal 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;
|
||||
}
|
||||
}
|
22
tests/Containers/ContainerGroupTest.php
Normal file
22
tests/Containers/ContainerGroupTest.php
Normal 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'));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue