diff --git a/src/Helpers/Classes.php b/src/Helpers/Classes.php index d53751d..b16f435 100644 --- a/src/Helpers/Classes.php +++ b/src/Helpers/Classes.php @@ -11,10 +11,8 @@ use Traversable; /** * Holds and sorts a list of CSS classes, including validation and add/remove/contains methods. - * - * @implements IteratorAggregate */ -class Classes implements IteratorAggregate, Countable +class Classes implements Countable { /** @var array */ 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 */ diff --git a/src/Helpers/Styles.php b/src/Helpers/Styles.php index 7850596..763cece 100644 --- a/src/Helpers/Styles.php +++ b/src/Helpers/Styles.php @@ -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; } diff --git a/src/Traits/ContainerMutableTrait.php b/src/Traits/ContainerMutableTrait.php index 9fd310e..e065bf6 100644 --- a/src/Traits/ContainerMutableTrait.php +++ b/src/Traits/ContainerMutableTrait.php @@ -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; + } } diff --git a/tests/Helpers/AttributesTest.php b/tests/Helpers/AttributesTest.php index 732b4a9..1d218ea 100644 --- a/tests/Helpers/AttributesTest.php +++ b/tests/Helpers/AttributesTest.php @@ -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 */ diff --git a/tests/Helpers/ClassesTest.php b/tests/Helpers/ClassesTest.php index 12853ab..e1c74ef 100644 --- a/tests/Helpers/ClassesTest.php +++ b/tests/Helpers/ClassesTest.php @@ -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')); + } } diff --git a/tests/Helpers/StylesTest.php b/tests/Helpers/StylesTest.php index b726bc7..15cd6cc 100644 --- a/tests/Helpers/StylesTest.php +++ b/tests/Helpers/StylesTest.php @@ -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()); } } diff --git a/tests/Tags/AbstractContainerTagTest.php b/tests/Tags/AbstractContainerTagTest.php index 189d588..7f692a5 100644 --- a/tests/Tags/AbstractContainerTagTest.php +++ b/tests/Tags/AbstractContainerTagTest.php @@ -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(''); + $this->assertCount(0, $div->children()); + } + + /** @depends clone testDIV */ + public function testTextChildren(AbstractContainerTag $div): void + { + $div->addChild('text'); + $div->addChild('unsanitized text', 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->__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'); + } } diff --git a/tests/Tags/AbstractTagTest.php b/tests/Tags/AbstractTagTest.php index a33c067..5639161 100644 --- a/tests/Tags/AbstractTagTest.php +++ b/tests/Tags/AbstractTagTest.php @@ -42,6 +42,9 @@ class AbstractTagTest extends TestCase $this->assertEquals('
', $tag->__toString()); unset($tag->attributes()['a']); $this->assertEquals('
', $tag->__toString()); + $tag->classes()->add('some-class'); + $tag->styles()['style'] = 'value'; + $this->assertEquals('
', $tag->__toString()); } /**