From 0af465fe4711fd09147125d9316f80354bac8c12 Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Wed, 30 Nov 2022 07:55:35 -0700 Subject: [PATCH] initial commit of new version --- .github/workflows/tests.yaml | 17 +++ .gitignore | 6 +- composer.json | 18 +-- phpstan.neon | 4 + phpunit.xml | 31 ++++- src/A.php | 9 -- src/ContainerInterface.php | 12 ++ src/ContainerMutableInterface.php | 30 +++++ src/Containers/AbstractDocument.php | 11 ++ src/Containers/DocumentInterface.php | 17 +++ .../DocumentTags/BodyTagInterface.php | 9 ++ .../DocumentTags/DoctypeInterface.php | 9 ++ .../DocumentTags/HeadTagInterface.php | 10 ++ .../DocumentTags/HtmlTagInterface.php | 12 ++ .../DocumentTags/TitleTagInterface.php | 11 ++ src/Containers/Fragment.php | 17 +++ src/Containers/FragmentInterface.php | 10 ++ src/GenericTag.php | 22 --- src/Helpers/Attributes.php | 92 +++++++++++++ src/Helpers/Classes.php | 96 +++++++++++++ src/Helpers/Styles.php | 118 ++++++++++++++++ src/Input.php | 30 ----- src/NodeInterface.php | 23 ++++ src/Nodes/Comment.php | 27 ++++ src/Nodes/CommentInterface.php | 11 ++ src/Nodes/Text.php | 20 +++ src/Nodes/TextInterface.php | 11 ++ src/Nodes/UnsanitizedText.php | 20 +++ src/TagInterface.php | 19 --- src/TagTrait.php | 127 ------------------ src/Tags/AbstractContainerTag.php | 20 +++ src/Tags/AbstractTag.php | 13 ++ src/Tags/ContainerTagInterface.php | 10 ++ src/Tags/TagInterface.php | 19 +++ src/Traits/ContainerMutableTrait.php | 62 +++++++++ src/Traits/ContainerTagTrait.php | 23 ++++ src/Traits/ContainerTrait.php | 17 +++ src/Traits/NodeTrait.php | 69 ++++++++++ src/Traits/TagTrait.php | 101 ++++++++++++++ tests/GenericTagTest.php | 79 ----------- tests/Helpers/AttributesTest.php | 59 ++++++++ tests/Helpers/ClassesTest.php | 40 ++++++ tests/Helpers/StylesTest.php | 45 +++++++ tests/Nodes/CommentTest.php | 16 +++ tests/Nodes/TextTest.php | 15 +++ tests/Nodes/UnsanitizedTextTest.php | 15 +++ tests/Tags/AbstractContainerTagTest.php | 61 +++++++++ tests/Tags/AbstractTagTest.php | 73 ++++++++++ 48 files changed, 1285 insertions(+), 301 deletions(-) create mode 100644 .github/workflows/tests.yaml create mode 100644 phpstan.neon delete mode 100644 src/A.php create mode 100644 src/ContainerInterface.php create mode 100644 src/ContainerMutableInterface.php create mode 100644 src/Containers/AbstractDocument.php create mode 100644 src/Containers/DocumentInterface.php create mode 100644 src/Containers/DocumentTags/BodyTagInterface.php create mode 100644 src/Containers/DocumentTags/DoctypeInterface.php create mode 100644 src/Containers/DocumentTags/HeadTagInterface.php create mode 100644 src/Containers/DocumentTags/HtmlTagInterface.php create mode 100644 src/Containers/DocumentTags/TitleTagInterface.php create mode 100644 src/Containers/Fragment.php create mode 100644 src/Containers/FragmentInterface.php delete mode 100644 src/GenericTag.php create mode 100644 src/Helpers/Attributes.php create mode 100644 src/Helpers/Classes.php create mode 100644 src/Helpers/Styles.php delete mode 100644 src/Input.php create mode 100644 src/NodeInterface.php create mode 100644 src/Nodes/Comment.php create mode 100644 src/Nodes/CommentInterface.php create mode 100644 src/Nodes/Text.php create mode 100644 src/Nodes/TextInterface.php create mode 100644 src/Nodes/UnsanitizedText.php delete mode 100644 src/TagInterface.php delete mode 100644 src/TagTrait.php create mode 100644 src/Tags/AbstractContainerTag.php create mode 100644 src/Tags/AbstractTag.php create mode 100644 src/Tags/ContainerTagInterface.php create mode 100644 src/Tags/TagInterface.php create mode 100644 src/Traits/ContainerMutableTrait.php create mode 100644 src/Traits/ContainerTagTrait.php create mode 100644 src/Traits/ContainerTrait.php create mode 100644 src/Traits/NodeTrait.php create mode 100644 src/Traits/TagTrait.php delete mode 100644 tests/GenericTagTest.php create mode 100644 tests/Helpers/AttributesTest.php create mode 100644 tests/Helpers/ClassesTest.php create mode 100644 tests/Helpers/StylesTest.php create mode 100644 tests/Nodes/CommentTest.php create mode 100644 tests/Nodes/TextTest.php create mode 100644 tests/Nodes/UnsanitizedTextTest.php create mode 100644 tests/Tags/AbstractContainerTagTest.php create mode 100644 tests/Tags/AbstractTagTest.php diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..4289a62 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,17 @@ +name: Tests and analysis +on: + push: + branches-ignore: [main] +jobs: + test-and-analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: php-actions/composer@v6 + with: + args: --ignore-platform-reqs + - uses: php-actions/phpunit@v3 + - uses: php-actions/phpstan@v3 + with: + memory_limit: 1G + args: --memory-limit 1G \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3a9875b..82dac7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -/vendor/ -composer.lock +/vendor +/composer.lock +/.phpunit.result.cache +/coverage \ No newline at end of file diff --git a/composer.json b/composer.json index 03db9fc..f3e7561 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,10 @@ { "name": "byjoby/html-object-strings", - "description": "Abstraction layer for constructing arbitrary HTML tags in PHP", + "description": "Abstraction layer for constructing arbitrary HTML tags and documents in PHP", "type": "library", "require": { - "php": ">=7.1" + "php": ">=8.1", + "myclabs/deep-copy": "^1" }, "license": "MIT", "authors": [{ @@ -14,21 +15,20 @@ "prefer-stable": true, "autoload": { "psr-4": { - "HtmlObjectStrings\\": "src/" + "ByJoby\\HTML\\": "src/" } }, "autoload-dev": { "psr-4": { - "HtmlObjectStrings\\": "tests/" + "ByJoby\\HTML\\": "tests/" } }, "scripts": { - "test": [ - "phpunit" - ] + "test": "phpunit", + "stan": "phpstan" }, "require-dev": { - "phpunit/phpunit": "^7", - "stevegrunwell/phpunit-markup-assertions": "^1.2" + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^9.5" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..478362b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: max + paths: + - src \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index a671214..71de912 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,7 +1,30 @@ - + + + + + + + - - tests + + tests/ - + + + src + + + + + + \ No newline at end of file diff --git a/src/A.php b/src/A.php deleted file mode 100644 index 51f7e5b..0000000 --- a/src/A.php +++ /dev/null @@ -1,9 +0,0 @@ - */ + public function children(): array; +} diff --git a/src/ContainerMutableInterface.php b/src/ContainerMutableInterface.php new file mode 100644 index 0000000..bfa8a27 --- /dev/null +++ b/src/ContainerMutableInterface.php @@ -0,0 +1,30 @@ +children()); + } +} diff --git a/src/Containers/FragmentInterface.php b/src/Containers/FragmentInterface.php new file mode 100644 index 0000000..f7b76bf --- /dev/null +++ b/src/Containers/FragmentInterface.php @@ -0,0 +1,10 @@ +htmlInit(); - } - - protected function htmlInit() - { - $this->tag = static::TAG; - $this->selfClosing = static::SELFCLOSING; - } -} diff --git a/src/Helpers/Attributes.php b/src/Helpers/Attributes.php new file mode 100644 index 0000000..3b36d06 --- /dev/null +++ b/src/Helpers/Attributes.php @@ -0,0 +1,92 @@ + + * @implements IteratorAggregate + */ +class Attributes implements IteratorAggregate, ArrayAccess +{ + /** @var array */ + protected $array = []; + /** @var bool */ + protected $sorted = true; + /** @var array */ + protected $disallowed = []; + + /** + * @param null|array $array + * @param array $disallowed + * @return void + */ + public function __construct(null|array $array = null, $disallowed = []) + { + $this->disallowed = $disallowed; + if (!$array) return; + foreach ($array as $key => $value) { + $this[$key] = $value; + } + } + + function offsetExists(mixed $offset): bool + { + $offset = static::sanitizeOffset($offset); + return isset($this->array[$offset]); + } + + function offsetGet(mixed $offset): mixed + { + $offset = static::sanitizeOffset($offset); + return $this->array[$offset]; + } + + function offsetSet(mixed $offset, mixed $value): void + { + if (!$offset || !trim($offset)) throw new Exception('Attribute name must be specified when setting'); + $offset = static::sanitizeOffset($offset); + if (in_array($offset, $this->disallowed)) throw new Exception('Setting attribute is disallowed'); + if (!isset($this->array[$offset])) $this->sorted = false; + $this->array[$offset] = $value; + } + + function offsetUnset(mixed $offset): void + { + $offset = static::sanitizeOffset($offset); + unset($this->array[$offset]); + } + + /** + * @return array + */ + function getArray(): array + { + if (!$this->sorted) { + ksort($this->array); + $this->sorted = true; + } + return $this->array; + } + + function getIterator(): Traversable + { + return new ArrayIterator($this->getArray()); + } + + protected static function sanitizeOffset(string $offset): string + { + $offset = trim($offset); + $offset = strtolower($offset); + if (preg_match('/[\t\n\f \/>"\'=]/', $offset)) throw new Exception('Invalid character in attribute name'); + return $offset; + } +} diff --git a/src/Helpers/Classes.php b/src/Helpers/Classes.php new file mode 100644 index 0000000..d53751d --- /dev/null +++ b/src/Helpers/Classes.php @@ -0,0 +1,96 @@ + + */ +class Classes implements IteratorAggregate, Countable +{ + /** @var array */ + protected $classes = []; + /** @var bool */ + protected $sorted = true; + + /** + * @param null|array|Traversable $array + */ + public function __construct(null|array|Traversable $array = null, bool $no_exception = true) + { + if (!$array) return; + foreach ($array as $class) { + $this->add($class, $no_exception); + } + } + + public function count(): int + { + return count($this->classes); + } + + function getIterator(): Traversable + { + return new ArrayIterator($this->getArray()); + } + + /** + * @return array + */ + function getArray(): array + { + if (!$this->sorted) { + sort($this->classes); + $this->sorted = true; + } + return $this->classes; + } + + public function add(string|Stringable $class, bool $no_exception = false): static + { + try { + $class = static::sanitizeClassName($class, true); + } catch (\Throwable $th) { + if ($no_exception) return $this; + else throw $th; + } + if (!in_array($class, $this->classes)) { + $this->classes[] = $class; + $this->sorted = false; + } + return $this; + } + + public function remove(string|Stringable $class): static + { + $class = static::sanitizeClassName($class); + $this->classes = array_values(array_filter( + $this->classes, + function (string|Stringable $e) use ($class): bool { + return $e != $class; + } + )); + return $this; + } + + public function contains(string|Stringable $class): bool + { + $class = static::sanitizeClassName($class); + return in_array($class, $this->classes); + } + + protected static function sanitizeClassName(string $class, bool $validate = false): string + { + $class = trim($class); + if ($validate && !preg_match('/^[_\-a-z][_\-a-z0-9]*$/i', $class)) throw new Exception('Invalid class name'); + return $class; + } +} diff --git a/src/Helpers/Styles.php b/src/Helpers/Styles.php new file mode 100644 index 0000000..7850596 --- /dev/null +++ b/src/Helpers/Styles.php @@ -0,0 +1,118 @@ + + */ +class Styles implements Countable, ArrayAccess, Stringable +{ + /** @var array */ + protected $styles = []; + /** @var bool */ + protected $sorted = true; + + /** + * @param null|array|Traversable|null $classes + */ + public function __construct(null|array|Traversable $classes = null) + { + if ($classes) { + foreach ($classes as $name => $value) { + $this[$name] = $value; + } + } + } + + public function count(): int + { + return count($this->styles); + } + + public function offsetExists(mixed $offset): bool + { + $offset = static::normalizePropertyName($offset); + if (!$offset) return false; + return isset($this->styles[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + return @$this->styles[$offset]; + } + + 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]); + else { + if (!static::validate($offset, $value)) return; + if (!isset($this->styles[$offset])) $this->sorted = false; + $this->styles[$offset] = $value; + } + } + + public function offsetUnset(mixed $offset): void + { + $offset = static::normalizePropertyName($offset); + if (!$offset) return; + unset($this->styles[$offset]); + } + + /** + * @return array + */ + public function getArray(): array + { + if (!$this->sorted) { + ksort($this->styles); + $this->sorted = true; + } + return $this->styles; + } + + public function __toString(): string + { + $styles = []; + foreach ($this->getArray() as $key => $value) { + $styles[] = $key . ':' . $value; + } + return implode(';', $styles); + } + + public static function normalizePropertyName(null|string $name): null|string + { + 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; + + if ($value) $value = trim($value); + if (!$value) return false; + if (str_contains($value, ';')) return false; + if (str_contains($value, ':')) return false; + + return true; + } +} diff --git a/src/Input.php b/src/Input.php deleted file mode 100644 index 5947538..0000000 --- a/src/Input.php +++ /dev/null @@ -1,30 +0,0 @@ -attr('type', static::TYPE); - } - - protected function htmlContent() - { - return parent::htmlContent(); - } - - protected function htmlAttributes() - { - $attr = parent::htmlAttributes(); - if ($value = $this->htmlContent()) { - $attr['value'] = $value; - } - return $attr; - } -} diff --git a/src/NodeInterface.php b/src/NodeInterface.php new file mode 100644 index 0000000..1e8f608 --- /dev/null +++ b/src/NodeInterface.php @@ -0,0 +1,23 @@ +', + str_replace( + '--', // regular hyphens + '‑‑', // non-breaking hyphens + $this->value + ) + ); + } +} diff --git a/src/Nodes/CommentInterface.php b/src/Nodes/CommentInterface.php new file mode 100644 index 0000000..0405dce --- /dev/null +++ b/src/Nodes/CommentInterface.php @@ -0,0 +1,11 @@ +value)); + } +} diff --git a/src/Nodes/TextInterface.php b/src/Nodes/TextInterface.php new file mode 100644 index 0000000..0abf881 --- /dev/null +++ b/src/Nodes/TextInterface.php @@ -0,0 +1,11 @@ +value; + } +} diff --git a/src/TagInterface.php b/src/TagInterface.php deleted file mode 100644 index 3643634..0000000 --- a/src/TagInterface.php +++ /dev/null @@ -1,19 +0,0 @@ -hidden = $hidden; - } - return $this->hidden; - } - - protected function htmlContent() - { - if (is_array($this->content)) { - return implode(PHP_EOL, $this->content); - } else { - return $this->content; - } - } - - protected function htmlAttributes() - { - $attr = $this->attributes; - if ($this->classes()) { - $attr['class'] = implode(' ', $this->classes()); - } - return $attr; - } - - public function addClass(string $name) - { - if (!$name) { - return; - } - $this->classes[] = $name; - $this->classes = array_unique($this->classes); - sort($this->classes); - } - - public function hasClass(string $name) : bool - { - return in_array($name, $this->classes); - } - - public function removeClass(string $name) - { - $this->classes = array_filter( - $this->classes, - function ($e) use ($name) { - return $e != $name; - } - ); - sort($this->classes); - } - - public function classes() : array - { - return $this->classes; - } - - public function attr(string $name, $value = null) - { - if ($value === false) { - unset($this->attributes[$name]); - return null; - } - if ($value !== null) { - $this->attributes[$name] = $value; - } - return @$this->attributes[$name]; - } - - public function data(string $name, $value = null) - { - return $this->attr("data-$name", $value); - } - - public function string() : string - { - //output empty string if hidden - if ($this->hidden()) { - return ''; - } - //build output - $out = ''; - //build opening tag - $out .= '<'.$this->tag; - //build attributes - if ($attr = $this->htmlAttributes()) { - foreach ($attr as $key => $value) { - if ($value === null) { - $out .= " $key"; - } else { - $value = htmlspecialchars($value); - $out .= " $key=\"$value\""; - } - } - } - //continue t close opening tag and add content and closing tag if needed - if ($this->selfClosing) { - $out .= ' />'; - } else { - $out .= '>'; - //build content - $out .= $this->htmlContent(); - //build closing tag - $out .= 'tag.'>'; - } - return $out; - } - - public function __toString() - { - return $this->string(); - } -} diff --git a/src/Tags/AbstractContainerTag.php b/src/Tags/AbstractContainerTag.php new file mode 100644 index 0000000..613d9ba --- /dev/null +++ b/src/Tags/AbstractContainerTag.php @@ -0,0 +1,20 @@ +detach(); + $child->setParent($this); + $child->setDocument($this->document()); + } + if ($prepend) array_unshift($this->children, $child); + else $this->children[] = $child; + return $this; + } + + public function removeChild( + NodeInterface|Stringable|string $child + ): static { + $this->children = array_filter( + $this->children, + function (NodeInterface $e) use ($child) { + if ($child instanceof NodeInterface) return $e !== $child; + else return $e != $child; + } + ); + return $this; + } + + public function addChildBefore( + NodeInterface|Stringable|string $new_child, + NodeInterface|Stringable|string $before_child, + bool $skip_sanitize = false + ): static { + return $this; + } + + public function addChildAfter( + NodeInterface|Stringable|string $new_child, + NodeInterface|Stringable|string $after_child, + bool $skip_sanitize = false + ): static { + return $this; + } +} diff --git a/src/Traits/ContainerTagTrait.php b/src/Traits/ContainerTagTrait.php new file mode 100644 index 0000000..5826fe7 --- /dev/null +++ b/src/Traits/ContainerTagTrait.php @@ -0,0 +1,23 @@ +', implode(' ', $this->openingTagStrings())); + $closingTag = sprintf('', $this->tag()); + if (!$this->children()) return $openingTag . $closingTag; + else return implode( + PHP_EOL, + [ + $openingTag, + implode(PHP_EOL, $this->children()), + $closingTag + ] + ); + } +} diff --git a/src/Traits/ContainerTrait.php b/src/Traits/ContainerTrait.php new file mode 100644 index 0000000..ec81ba0 --- /dev/null +++ b/src/Traits/ContainerTrait.php @@ -0,0 +1,17 @@ + */ + protected $children = []; + + /** @return array */ + public function children(): array + { + return $this->children; + } +} diff --git a/src/Traits/NodeTrait.php b/src/Traits/NodeTrait.php new file mode 100644 index 0000000..d4169cb --- /dev/null +++ b/src/Traits/NodeTrait.php @@ -0,0 +1,69 @@ +parent; + } + + public function setParent(null|NodeInterface $parent): static + { + $this->parent = $parent; + return $this; + } + + public function document(): null|DocumentInterface + { + return $this->document; + } + + public function setDocument(null|DocumentInterface $document): static + { + $this->document = $document; + if ($this instanceof ContainerInterface) { + foreach ($this->children() as $child) { + $child->setDocument($document); + } + } + return $this; + } + + public function detach(): static + { + if (!$this->parent()) return $this; + if ($this->parent() instanceof ContainerMutableInterface) { + $this->parent()->removeChild($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 + { + static $copier; + $copier = $copier ?? new DeepCopy(); + return ($copier->copy($this)) + ->setParent(null) + ->setDocument(null); + } +} diff --git a/src/Traits/TagTrait.php b/src/Traits/TagTrait.php new file mode 100644 index 0000000..0feca72 --- /dev/null +++ b/src/Traits/TagTrait.php @@ -0,0 +1,101 @@ +attributes = new Attributes(null, ['id', 'class', 'style']); + $this->classes = new Classes(); + $this->styles = new Styles(); + } + + public function id(): null|string + { + return $this->id; + } + + public function setID(null|string|Stringable $id): static + { + if ($id) $this->id = static::sanitizeID($id); + else $this->id = null; + return $this; + } + + protected static function sanitizeID(string|Stringable $id): string + { + $id = trim($id); + if (!preg_match('/^[_\-a-z][_\-a-z0-9]*$/i', $id)) throw new Exception('Invalid ID name'); + return $id; + } + + public function attributes(): Attributes + { + return $this->attributes; + } + + public function classes(): Classes + { + return $this->classes; + } + + public function styles(): Styles + { + return $this->styles; + } + + public function __toString(): string + { + return sprintf('<%s/>', implode(' ', $this->openingTagStrings())); + } + + /** + * @return array + */ + protected function openingTagStrings(): array + { + $strings = [$this->tag()]; + if ($this->id) $strings[] = sprintf('id="%s"', $this->id); + if ($this->classes()->count()) { + $strings[] = sprintf('class="%s"', implode(' ', $this->classes()->getArray())); + } + if ($this->styles()->count()) { + $strings[] = sprintf('style="%s"', $this->styles()); + } + foreach ($this->attributes() as $name => $value) { + if ($value === null) $strings[] = $name; + else $strings[] = sprintf('%s="%s"', $name, static::sanitizeAttribute($value)); + } + return $strings; + } + + protected static function sanitizeAttribute(string $value): string + { + return str_replace( + ['<', '>', '&', '"'], + ['<', '>', '&', '"'], + $value + ); + } +} diff --git a/tests/GenericTagTest.php b/tests/GenericTagTest.php deleted file mode 100644 index 83faf26..0000000 --- a/tests/GenericTagTest.php +++ /dev/null @@ -1,79 +0,0 @@ -assertEquals([], $h->classes()); - $this->assertFalse($h->hasClass('foo')); - //adding a class - $h->addClass('foo'); - //should now exist - $this->assertEquals(['foo'], $h->classes()); - $this->assertTrue($h->hasClass('foo')); - //adding a second time shouldn't change anything - $h->addClass('foo'); - $this->assertEquals(['foo'], $h->classes()); - $this->assertTrue($h->hasClass('foo')); - //adding another class - $h->addClass('bar'); - //should now exist, and classes should be in alphabetical order - $this->assertEquals(['bar','foo'], $h->classes()); - $this->assertTrue($h->hasClass('bar')); - //removing a class - $h->addClass('abc'); - $h->removeClass('bar'); - //bar should now not exist, and classes should be in alphabetical order - $this->assertEquals(['abc','foo'], $h->classes()); - $this->assertFalse($h->hasClass('bar')); - } - - public function testDataAndAttributeManagement() - { - /* - This section tests the basic getting/setting of attributes - */ - $h = new GenericTag(); - //set and get an attribute - $h->attr('foo', 'bar'); - $this->assertEquals('bar', $h->attr('foo')); - //set and get data - $h->data('foo', 'baz'); - $this->assertEquals('baz', $h->data('foo')); - $this->assertEquals('baz', $h->attr('data-foo')); - $this->assertEquals('bar', $h->attr('foo')); - } - - public function testMarkupOutput() - { - /* - Test that output has the correct attributes and classes for what was - configured into the object - */ - $h = new GenericTag(); - $h->tag = 'div'; - $h->selfClosing = false; - $h->content = 'markup content'; - $h->attr('id', 'h'); - $h->data('foo', 'bar'); - $h->addClass('class-foo'); - $h->addClass('class-bar'); - //should be a div tag - $this->assertContainsSelector('div', "$h"); - $this->assertContainsSelector('div#h', "$h"); - $this->assertContainsSelector('div[data-foo="bar"]', "$h"); - $this->assertContainsSelector('div.class-foo.class-bar', "$h"); - } -} diff --git a/tests/Helpers/AttributesTest.php b/tests/Helpers/AttributesTest.php new file mode 100644 index 0000000..732b4a9 --- /dev/null +++ b/tests/Helpers/AttributesTest.php @@ -0,0 +1,59 @@ +assertEquals([], $attributes->getArray()); + $attributes = new Attributes(['foo' => 'bar', 'baz' => null]); + $this->assertEquals(['baz' => null, 'foo' => 'bar'], $attributes->getArray()); + return $attributes; + } + + public function testInvalidConstructionEmptyName(): Attributes + { + $this->expectExceptionMessage('Attribute name must be specified when setting'); + $attributes = new Attributes(['' => 'foo']); + } + + public function testInvalidConstructionInvalidName(): Attributes + { + $this->expectExceptionMessage('Invalid character in attribute name'); + $attributes = new Attributes(['a=b' => 'foo']); + } + + /** + * @depends clone testConstruction + */ + public function testSetAndUnset(Attributes $attributes): void + { + $attributes['a'] = 'b'; + $this->assertEquals('b', $attributes['a']); + $this->assertEquals(['a' => 'b', 'baz' => null, 'foo' => 'bar'], $attributes->getArray()); + unset($attributes['baz']); + $this->assertEquals(['a' => 'b', 'foo' => 'bar'], $attributes->getArray()); + } + + /** + * @depends clone testConstruction + */ + public function testInvalidSetEmptyName(Attributes $attributes): void + { + $this->expectExceptionMessage('Attribute name must be specified when setting'); + $attributes[] = 'b'; + } + + /** + * @depends clone testConstruction + */ + public function testInvalidSetInvalidName(Attributes $attributes): void + { + $this->expectExceptionMessage('Invalid character in attribute name'); + $attributes['>'] = 'b'; + } +} diff --git a/tests/Helpers/ClassesTest.php b/tests/Helpers/ClassesTest.php new file mode 100644 index 0000000..12853ab --- /dev/null +++ b/tests/Helpers/ClassesTest.php @@ -0,0 +1,40 @@ +assertEquals([], $classes->getArray()); + $classes = new Classes(['a', 'c', ' a ', 'b', '!']); + $this->assertEquals(['a', 'b', 'c'], $classes->getArray()); + return $classes; + } + + public function testInvalidConstruction() + { + $this->expectExceptionMessage('Invalid class name'); + $classes = new Classes(['a', 'c', ' a ', 'b', '!'], false); + } + + /** + * @depends clone testConstruction + */ + public function testAddRemove(Classes $classes): void + { + $classes->add('d'); + $this->assertEquals(['a', 'b', 'c', 'd'], $classes->getArray()); + $classes->add('-d'); + $this->assertEquals(['-d', 'a', 'b', 'c', 'd'], $classes->getArray()); + $classes->add('_A'); + $this->assertEquals(['-d', '_A', 'a', 'b', 'c', 'd'], $classes->getArray()); + $classes->remove('b'); + $this->assertEquals(['-d', '_A', 'a', 'c', 'd'], $classes->getArray()); + $this->expectExceptionMessage('Invalid class name'); + $classes->add('0a'); + } +} diff --git a/tests/Helpers/StylesTest.php b/tests/Helpers/StylesTest.php new file mode 100644 index 0000000..b726bc7 --- /dev/null +++ b/tests/Helpers/StylesTest.php @@ -0,0 +1,45 @@ +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(); + $this->assertEquals([], $styles->getArray()); + $styles = new Styles(['foo' => 'bar', 'baz' => null]); + $this->assertEquals(['foo' => 'bar'], $styles->getArray()); + return $styles; + } + + /** + * @depends clone testConstruction + */ + public function testGettingAndSetting(Styles $styles): void + { + $styles['a'] = 'b'; + $this->assertEquals('b', $styles['a']); + unset($styles['foo']); + $this->assertNull($styles['foo']); + } +} diff --git a/tests/Nodes/CommentTest.php b/tests/Nodes/CommentTest.php new file mode 100644 index 0000000..ba34766 --- /dev/null +++ b/tests/Nodes/CommentTest.php @@ -0,0 +1,16 @@ +assertEquals('', new Comment('')); + $this->assertEquals('', new Comment('foo')); + $this->assertEquals('', new Comment('foo-bar')); + $this->assertNotEquals('', new Comment('foo--bar')); + } +} diff --git a/tests/Nodes/TextTest.php b/tests/Nodes/TextTest.php new file mode 100644 index 0000000..af80b09 --- /dev/null +++ b/tests/Nodes/TextTest.php @@ -0,0 +1,15 @@ +assertEquals('', new Text('')); + $this->assertEquals('foo', new Text('foo')); + $this->assertEquals('foo', new Text('foo')); + } +} diff --git a/tests/Nodes/UnsanitizedTextTest.php b/tests/Nodes/UnsanitizedTextTest.php new file mode 100644 index 0000000..dd4555c --- /dev/null +++ b/tests/Nodes/UnsanitizedTextTest.php @@ -0,0 +1,15 @@ +assertEquals('', new UnsanitizedText('')); + $this->assertEquals('foo', new UnsanitizedText('foo')); + $this->assertEquals('foo', new UnsanitizedText('foo')); + } +} diff --git a/tests/Tags/AbstractContainerTagTest.php b/tests/Tags/AbstractContainerTagTest.php new file mode 100644 index 0000000..189d588 --- /dev/null +++ b/tests/Tags/AbstractContainerTagTest.php @@ -0,0 +1,61 @@ +getMockForAbstractClass(AbstractContainerTag::class); + $div->method('tag')->will($this->returnValue('div')); + $this->assertEquals('
', $div->__toString()); + $span = $this->getMockForAbstractClass(AbstractContainerTag::class); + $span->method('tag')->will($this->returnValue('span')); + $div->addChild($span); + $div->attributes()['a'] = 'b'; + $this->assertEquals( + implode(PHP_EOL, [ + '
', + '', + '
', + ]), + $div->__toString() + ); + $this->assertEquals($div, $span->parent()); + return $div; + } + + /** @depends clone testDIV */ + public function testMoreNesting(AbstractContainerTag $div): AbstractContainerTag + { + $span1 = $div->children()[0]; + $span2 = $this->getMockForAbstractClass(AbstractContainerTag::class); + $span2->method('tag')->will($this->returnValue('span')); + $span1->addChild($span2); + $this->assertEquals( + implode(PHP_EOL, [ + '
', + '', + '', + '', + '
', + ]), + $div->__toString() + ); + return $div; + } + + /** @depends clone testMoreNesting */ + public function testDetach(AbstractContainerTag $div): void + { + $span1 = $div->children()[0]; + $span2 = $span1->children()[0]; + $span1->detach(); + $this->assertEquals('
', $div->__toString()); + $this->assertNull($span1->parent()); + } +} diff --git a/tests/Tags/AbstractTagTest.php b/tests/Tags/AbstractTagTest.php new file mode 100644 index 0000000..a33c067 --- /dev/null +++ b/tests/Tags/AbstractTagTest.php @@ -0,0 +1,73 @@ +getMockForAbstractClass(AbstractTag::class); + $br->method('tag')->will($this->returnValue('br')); + $this->assertEquals('
', $br->__toString()); + $this->assertInstanceOf(Classes::class, $br->classes()); + $this->assertInstanceOf(Attributes::class, $br->attributes()); + return $br; + } + + /** + * @depends clone testBR + */ + public function testID(AbstractTag $tag): void + { + $this->assertNull($tag->id()); + $tag->setID('foo'); + $this->assertEquals('foo', $tag->id()); + $this->assertEquals('
', $tag->__toString()); + $tag->setID(null); + $this->assertNull($tag->id()); + $this->assertEquals('
', $tag->__toString()); + } + + /** + * @depends clone testBR + */ + public function testAttributes(AbstractTag $tag): void + { + $tag->attributes()['b'] = 'c'; + $tag->attributes()['a'] = 'b'; + $this->assertEquals('
', $tag->__toString()); + unset($tag->attributes()['a']); + $this->assertEquals('
', $tag->__toString()); + } + + /** + * @depends clone testBR + */ + public function testSettingIDException(AbstractTag $tag): void + { + $this->expectExceptionMessage('Setting attribute is disallowed'); + $tag->attributes()['id'] = 'foo'; + } + + /** + * @depends clone testBR + */ + public function testSettingClassException(AbstractTag $tag): void + { + $this->expectExceptionMessage('Setting attribute is disallowed'); + $tag->attributes()['class'] = 'foo'; + } + + /** + * @depends clone testBR + */ + public function testSettingStyleException(AbstractTag $tag): void + { + $this->expectExceptionMessage('Setting attribute is disallowed'); + $tag->attributes()['style'] = 'foo'; + } +}