reduced complexity and improved code coverage
This commit is contained in:
parent
b9f3e837f1
commit
7298617363
8 changed files with 157 additions and 51 deletions
|
@ -11,10 +11,8 @@ use Traversable;
|
|||
|
||||
/**
|
||||
* Holds and sorts a list of CSS classes, including validation and add/remove/contains methods.
|
||||
*
|
||||
* @implements IteratorAggregate<int,string|Stringable>
|
||||
*/
|
||||
class Classes implements IteratorAggregate, Countable
|
||||
class Classes implements Countable
|
||||
{
|
||||
/** @var array<int,string|Stringable> */
|
||||
protected $classes = [];
|
||||
|
@ -37,11 +35,6 @@ class Classes implements IteratorAggregate, Countable
|
|||
return count($this->classes);
|
||||
}
|
||||
|
||||
function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->getArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string|Stringable>
|
||||
*/
|
||||
|
|
|
@ -29,10 +29,9 @@ class Styles implements Countable, ArrayAccess, Stringable
|
|||
*/
|
||||
public function __construct(null|array|Traversable $classes = null)
|
||||
{
|
||||
if ($classes) {
|
||||
foreach ($classes as $name => $value) {
|
||||
$this[$name] = $value;
|
||||
}
|
||||
if (!$classes) return;
|
||||
foreach ($classes as $name => $value) {
|
||||
$this[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +42,6 @@ class Styles implements Countable, ArrayAccess, Stringable
|
|||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
$offset = static::normalizePropertyName($offset);
|
||||
if (!$offset) return false;
|
||||
return isset($this->styles[$offset]);
|
||||
}
|
||||
|
@ -55,7 +53,6 @@ class Styles implements Countable, ArrayAccess, Stringable
|
|||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$offset = static::normalizePropertyName($offset);
|
||||
if (!$offset) return;
|
||||
if ($value) $value = trim($value);
|
||||
if (!$value) unset($this->styles[$offset]);
|
||||
|
@ -68,8 +65,6 @@ class Styles implements Countable, ArrayAccess, Stringable
|
|||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
$offset = static::normalizePropertyName($offset);
|
||||
if (!$offset) return;
|
||||
unset($this->styles[$offset]);
|
||||
}
|
||||
|
||||
|
@ -94,24 +89,15 @@ class Styles implements Countable, ArrayAccess, Stringable
|
|||
return implode(';', $styles);
|
||||
}
|
||||
|
||||
public static function normalizePropertyName(null|string $name): null|string
|
||||
protected static function validate(null|string $property, null|string $value): bool
|
||||
{
|
||||
if (!$name) return null;
|
||||
$name = trim(strtolower($name));
|
||||
$name = preg_replace('/[^a-z\-]/', '', $name);
|
||||
return $name;
|
||||
}
|
||||
|
||||
public static function validate(null|string $property, null|string $value): bool
|
||||
{
|
||||
$property = static::normalizePropertyName($property);
|
||||
if (!$property) return false;
|
||||
if (!preg_match('/[a-z]/', $property)) return false;
|
||||
elseif (!preg_match('/[a-z]/', $property)) return false;
|
||||
|
||||
if ($value) $value = trim($value);
|
||||
if (!$value) return false;
|
||||
if (str_contains($value, ';')) return false;
|
||||
if (str_contains($value, ':')) return false;
|
||||
elseif (str_contains($value, ';')) return false;
|
||||
elseif (str_contains($value, ':')) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use ByJoby\HTML\ContainerMutableInterface;
|
|||
use ByJoby\HTML\NodeInterface;
|
||||
use ByJoby\HTML\Nodes\Text;
|
||||
use ByJoby\HTML\Nodes\UnsanitizedText;
|
||||
use Exception;
|
||||
use Stringable;
|
||||
|
||||
trait ContainerMutableTrait
|
||||
|
@ -17,10 +18,7 @@ trait ContainerMutableTrait
|
|||
bool $prepend = false,
|
||||
bool $skip_sanitize = false
|
||||
): static {
|
||||
if (!($child instanceof NodeInterface)) {
|
||||
if ($skip_sanitize) $child = new UnsanitizedText($child);
|
||||
else $child = new Text($child);
|
||||
}
|
||||
$child = $this->normalizeChild($child, $skip_sanitize);
|
||||
if ($this instanceof NodeInterface) {
|
||||
$child->detach();
|
||||
$child->setParent($this);
|
||||
|
@ -37,7 +35,7 @@ trait ContainerMutableTrait
|
|||
$this->children = array_filter(
|
||||
$this->children,
|
||||
function (NodeInterface $e) use ($child) {
|
||||
if ($child instanceof NodeInterface) return $e !== $child;
|
||||
if (is_object($child)) return $e !== $child;
|
||||
else return $e != $child;
|
||||
}
|
||||
);
|
||||
|
@ -49,6 +47,12 @@ trait ContainerMutableTrait
|
|||
NodeInterface|Stringable|string $before_child,
|
||||
bool $skip_sanitize = false
|
||||
): static {
|
||||
$i = $this->indexOfChild($before_child);
|
||||
if ($i === null) {
|
||||
throw new Exception('Reference child not found in this container');
|
||||
}
|
||||
$new_child = $this->normalizeChild($new_child, $skip_sanitize);
|
||||
array_splice($this->children, $i, 0, [$new_child]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -57,6 +61,36 @@ trait ContainerMutableTrait
|
|||
NodeInterface|Stringable|string $after_child,
|
||||
bool $skip_sanitize = false
|
||||
): static {
|
||||
$i = $this->indexOfChild($after_child);
|
||||
if ($i === null) {
|
||||
throw new Exception('Reference child not found in this container');
|
||||
}
|
||||
$new_child = $this->normalizeChild($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
|
||||
{
|
||||
if ($child instanceof NodeInterface) {
|
||||
return $child;
|
||||
} else {
|
||||
if ($skip_sanitize) return new UnsanitizedText($child);
|
||||
else return new Text($child);
|
||||
}
|
||||
}
|
||||
|
||||
protected function indexOfChild(NodeInterface|Stringable|string $child): null|int
|
||||
{
|
||||
if ($child instanceof NodeInterface) {
|
||||
foreach ($this->children() as $i => $v) {
|
||||
if ($v === $child) return $i;
|
||||
}
|
||||
} else {
|
||||
foreach ($this->children() as $i => $v) {
|
||||
if ($v == $child) return $i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,16 @@ class AttributesTest extends TestCase
|
|||
$this->assertEquals(['a' => 'b', 'foo' => 'bar'], $attributes->getArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends clone testConstruction
|
||||
*/
|
||||
public function testOffsetExists(Attributes $attributes): void
|
||||
{
|
||||
$this->assertFalse(isset($attributes['a']));
|
||||
$attributes['a'] = 'b';
|
||||
$this->assertTrue(isset($attributes['a']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends clone testConstruction
|
||||
*/
|
||||
|
|
|
@ -37,4 +37,14 @@ class ClassesTest extends TestCase
|
|||
$this->expectExceptionMessage('Invalid class name');
|
||||
$classes->add('0a');
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends clone testConstruction
|
||||
*/
|
||||
public function testContains(Classes $classes): void
|
||||
{
|
||||
$this->assertFalse($classes->contains('d'));
|
||||
$classes->add('d');
|
||||
$this->assertTrue($classes->contains('d'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,23 +6,6 @@ use PHPUnit\Framework\TestCase;
|
|||
|
||||
class StylesTest extends TestCase
|
||||
{
|
||||
public function testValidate(): void
|
||||
{
|
||||
$this->assertTrue(Styles::validate('foo', 'bar'));
|
||||
$this->assertFalse(Styles::validate('foo', ''));
|
||||
$this->assertFalse(Styles::validate('foo', ' '));
|
||||
$this->assertFalse(Styles::validate('foo', null));
|
||||
$this->assertTrue(Styles::validate(' -foo', 'bar'));
|
||||
$this->assertTrue(Styles::validate(' foo ', 'bar'));
|
||||
$this->assertFalse(Styles::validate('', 'bar'));
|
||||
$this->assertFalse(Styles::validate('-', 'bar'));
|
||||
$this->assertFalse(Styles::validate(' ', 'bar'));
|
||||
$this->assertFalse(Styles::validate(null, 'bar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testValidate
|
||||
*/
|
||||
public function testConstruction(): Styles
|
||||
{
|
||||
$styles = new Styles();
|
||||
|
@ -41,5 +24,13 @@ class StylesTest extends TestCase
|
|||
$this->assertEquals('b', $styles['a']);
|
||||
unset($styles['foo']);
|
||||
$this->assertNull($styles['foo']);
|
||||
$this->assertTrue(isset($styles['a']));
|
||||
$this->assertFalse(isset($styles['foo']));
|
||||
}
|
||||
|
||||
public function testToString(): void
|
||||
{
|
||||
$styles = new Styles(['a' => 'b', 'b' => 'c']);
|
||||
$this->assertEquals('a:b;b:c', $styles->__toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ namespace ByJoby\HTML\Tags;
|
|||
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use ByJoby\HTML\Nodes\Text;
|
||||
use ByJoby\HTML\Nodes\UnsanitizedText;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AbstractContainerTagTest extends TestCase
|
||||
|
@ -49,6 +51,32 @@ class AbstractContainerTagTest extends TestCase
|
|||
return $div;
|
||||
}
|
||||
|
||||
/** @depends clone testDIV */
|
||||
public function testRemoveChild(AbstractContainerTag $div): void
|
||||
{
|
||||
$span2 = $this->getMockForAbstractClass(AbstractContainerTag::class);
|
||||
$span2->method('tag')->will($this->returnValue('span'));
|
||||
// add a second span and remove it using its object
|
||||
$div->addChild($span2);
|
||||
$this->assertCount(2, $div->children());
|
||||
$div->removeChild($span2);
|
||||
$this->assertCount(1, $div->children());
|
||||
// re-add second span and remove it using string
|
||||
$div->addChild($span2);
|
||||
$this->assertCount(2, $div->children());
|
||||
$div->removeChild('<span></span>');
|
||||
$this->assertCount(0, $div->children());
|
||||
}
|
||||
|
||||
/** @depends clone testDIV */
|
||||
public function testTextChildren(AbstractContainerTag $div): void
|
||||
{
|
||||
$div->addChild('<strong>text</strong>');
|
||||
$div->addChild('<strong>unsanitized text</strong>', false, true);
|
||||
$this->assertInstanceOf(Text::class, $div->children()[1]);
|
||||
$this->assertInstanceOf(UnsanitizedText::class, $div->children()[2]);
|
||||
}
|
||||
|
||||
/** @depends clone testMoreNesting */
|
||||
public function testDetach(AbstractContainerTag $div): void
|
||||
{
|
||||
|
@ -58,4 +86,55 @@ class AbstractContainerTagTest extends TestCase
|
|||
$this->assertEquals('<div a="b"></div>', $div->__toString());
|
||||
$this->assertNull($span1->parent());
|
||||
}
|
||||
|
||||
/** @depends clone testMoreNesting */
|
||||
public function testDetachCopy(AbstractContainerTag $div): void
|
||||
{
|
||||
$span1 = $div->children()[0];
|
||||
$span2 = $span1->children()[0];
|
||||
$copy = $span1->detachCopy();
|
||||
$this->assertNull($copy->parent());
|
||||
$this->assertEquals($div, $span1->parent());
|
||||
}
|
||||
|
||||
public function testAddChildBefore(): void
|
||||
{
|
||||
$div = $this->getMockForAbstractClass(AbstractContainerTag::class);
|
||||
$div->method('tag')->will($this->returnValue('div'));
|
||||
// add a string child
|
||||
$div->addChild('a');
|
||||
$div->addChildBefore('b', 'a');
|
||||
$this->assertEquals('b', $div->children()[0]->__toString());
|
||||
// add an actual node object
|
||||
$span1 = $this->getMockForAbstractClass(AbstractContainerTag::class);
|
||||
$span1->method('tag')->will($this->returnValue('span'));
|
||||
$div->addChildBefore($span1, 'a');
|
||||
$this->assertEquals($span1, $div->children()[1]->__toString());
|
||||
// add another object referencing the node object
|
||||
$div->addChildBefore('c', $span1);
|
||||
$this->assertEquals('c', $div->children()[1]->__toString());
|
||||
// should throw an exception if reference child is not found
|
||||
$this->expectExceptionMessage('Reference child not found in this container');
|
||||
$div->addChildBefore('z', 'x');
|
||||
}
|
||||
|
||||
public function testAddChildAfter(): void {
|
||||
$div = $this->getMockForAbstractClass(AbstractContainerTag::class);
|
||||
$div->method('tag')->will($this->returnValue('div'));
|
||||
// add a string child
|
||||
$div->addChild('a');
|
||||
$div->addChildAfter('b', 'a');
|
||||
$this->assertEquals('b', $div->children()[1]->__toString());
|
||||
// add an actual node object
|
||||
$span1 = $this->getMockForAbstractClass(AbstractContainerTag::class);
|
||||
$span1->method('tag')->will($this->returnValue('span'));
|
||||
$div->addChildAfter($span1, 'a');
|
||||
$this->assertEquals($span1, $div->children()[1]->__toString());
|
||||
// add another object referencing the node object
|
||||
$div->addChildAfter('c', $span1);
|
||||
$this->assertEquals('c', $div->children()[2]->__toString());
|
||||
// should throw an exception if reference child is not found
|
||||
$this->expectExceptionMessage('Reference child not found in this container');
|
||||
$div->addChildAfter('z', 'x');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,9 @@ class AbstractTagTest extends TestCase
|
|||
$this->assertEquals('<br a="b" b="c"/>', $tag->__toString());
|
||||
unset($tag->attributes()['a']);
|
||||
$this->assertEquals('<br b="c"/>', $tag->__toString());
|
||||
$tag->classes()->add('some-class');
|
||||
$tag->styles()['style'] = 'value';
|
||||
$this->assertEquals('<br class="some-class" style="style:value" b="c"/>', $tag->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue