initial commit of new version
This commit is contained in:
parent
1dd277dc0b
commit
0af465fe47
48 changed files with 1285 additions and 301 deletions
17
.github/workflows/tests.yaml
vendored
Normal file
17
.github/workflows/tests.yaml
vendored
Normal file
|
@ -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
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
/vendor/
|
||||
composer.lock
|
||||
/vendor
|
||||
/composer.lock
|
||||
/.phpunit.result.cache
|
||||
/coverage
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
4
phpstan.neon
Normal file
4
phpstan.neon
Normal file
|
@ -0,0 +1,4 @@
|
|||
parameters:
|
||||
level: max
|
||||
paths:
|
||||
- src
|
31
phpunit.xml
31
phpunit.xml
|
@ -1,7 +1,30 @@
|
|||
<phpunit>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
executionOrder="random"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
failOnEmptyTestSuite="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
verbose="true">
|
||||
<php>
|
||||
<ini name="display_errors" value="On" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<ini name="xdebug.mode" value="coverage" />
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="All Tests">
|
||||
<directory>tests</directory>
|
||||
<testsuite name="Tests">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
<report>
|
||||
<html outputDirectory="coverage" lowUpperBound="50" highLowerBound="90" />
|
||||
</report>
|
||||
</coverage>
|
||||
</phpunit>
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
/* HTML Object Strings | https://gitlab.com/byjoby/html-object-strings | MIT License */
|
||||
namespace HtmlObjectStrings;
|
||||
|
||||
class A extends GenericTag
|
||||
{
|
||||
const TAG = 'a';
|
||||
const SELFCLOSING = false;
|
||||
}
|
12
src/ContainerInterface.php
Normal file
12
src/ContainerInterface.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML;
|
||||
|
||||
use Stringable;
|
||||
use Traversable;
|
||||
|
||||
interface ContainerInterface extends Stringable
|
||||
{
|
||||
/** @return array<int,NodeInterface> */
|
||||
public function children(): array;
|
||||
}
|
30
src/ContainerMutableInterface.php
Normal file
30
src/ContainerMutableInterface.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML;
|
||||
|
||||
use Stringable;
|
||||
|
||||
interface ContainerMutableInterface extends ContainerInterface
|
||||
{
|
||||
public function addChild(
|
||||
NodeInterface|Stringable|string $child,
|
||||
bool $prepend = false,
|
||||
bool $skip_sanitize = false
|
||||
): static;
|
||||
|
||||
public function removeChild(
|
||||
NodeInterface|Stringable|string $child
|
||||
): static;
|
||||
|
||||
public function addChildBefore(
|
||||
NodeInterface|Stringable|string $new_child,
|
||||
NodeInterface|Stringable|string $before_child,
|
||||
bool $skip_sanitize = false
|
||||
): static;
|
||||
|
||||
public function addChildAfter(
|
||||
NodeInterface|Stringable|string $new_child,
|
||||
NodeInterface|Stringable|string $after_child,
|
||||
bool $skip_sanitize = false
|
||||
): static;
|
||||
}
|
11
src/Containers/AbstractDocument.php
Normal file
11
src/Containers/AbstractDocument.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers;
|
||||
|
||||
use ByJoby\HTML\ContainerInterface;
|
||||
use ByJoby\HTML\Traits\ContainerTrait;
|
||||
|
||||
abstract class AbstractDocument implements DocumentInterface
|
||||
{
|
||||
use ContainerTrait;
|
||||
}
|
17
src/Containers/DocumentInterface.php
Normal file
17
src/Containers/DocumentInterface.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers;
|
||||
|
||||
use ByJoby\HTML\ContainerInterface;
|
||||
use ByJoby\HTML\Containers\DocumentTags\BodyTagInterface;
|
||||
use ByJoby\HTML\Containers\DocumentTags\DoctypeInterface;
|
||||
use ByJoby\HTML\Containers\DocumentTags\HeadTagInterface;
|
||||
use ByJoby\HTML\Containers\DocumentTags\HtmlTagInterface;
|
||||
|
||||
interface DocumentInterface extends ContainerInterface
|
||||
{
|
||||
public function doctype(): DoctypeInterface;
|
||||
public function html(): HtmlTagInterface;
|
||||
public function head(): HeadTagInterface;
|
||||
public function body(): BodyTagInterface;
|
||||
}
|
9
src/Containers/DocumentTags/BodyTagInterface.php
Normal file
9
src/Containers/DocumentTags/BodyTagInterface.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers\DocumentTags;
|
||||
|
||||
use ByJoby\HTML\Tags\ContainerTagInterface;
|
||||
|
||||
interface BodyTagInterface extends ContainerTagInterface
|
||||
{
|
||||
}
|
9
src/Containers/DocumentTags/DoctypeInterface.php
Normal file
9
src/Containers/DocumentTags/DoctypeInterface.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers\DocumentTags;
|
||||
|
||||
use PhpParser\Node;
|
||||
|
||||
interface DoctypeInterface extends Node
|
||||
{
|
||||
}
|
10
src/Containers/DocumentTags/HeadTagInterface.php
Normal file
10
src/Containers/DocumentTags/HeadTagInterface.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers\DocumentTags;
|
||||
|
||||
use ByJoby\HTML\Tags\ContainerTagInterface;
|
||||
|
||||
interface HeadTagInterface extends ContainerTagInterface
|
||||
{
|
||||
public function title(): TitleTagInterface;
|
||||
}
|
12
src/Containers/DocumentTags/HtmlTagInterface.php
Normal file
12
src/Containers/DocumentTags/HtmlTagInterface.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers\DocumentTags;
|
||||
|
||||
use ByJoby\HTML\ContainerInterface;
|
||||
use ByJoby\HTML\Tags\TagInterface;
|
||||
|
||||
interface HtmlTagInterface extends ContainerInterface, TagInterface
|
||||
{
|
||||
public function head(): HeadTagInterface;
|
||||
public function body(): BodyTagInterface;
|
||||
}
|
11
src/Containers/DocumentTags/TitleTagInterface.php
Normal file
11
src/Containers/DocumentTags/TitleTagInterface.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers\DocumentTags;
|
||||
|
||||
use ByJoby\HTML\Tags\TagInterface;
|
||||
|
||||
interface TitleTagInterface extends TagInterface
|
||||
{
|
||||
public function title(): string;
|
||||
public function setTitle(string $title): static;
|
||||
}
|
17
src/Containers/Fragment.php
Normal file
17
src/Containers/Fragment.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers;
|
||||
|
||||
use ByJoby\HTML\Traits\ContainerMutableTrait;
|
||||
use ByJoby\HTML\Traits\ContainerTrait;
|
||||
|
||||
class Fragment implements FragmentInterface
|
||||
{
|
||||
use ContainerTrait;
|
||||
use ContainerMutableTrait;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode(PHP_EOL, $this->children());
|
||||
}
|
||||
}
|
10
src/Containers/FragmentInterface.php
Normal file
10
src/Containers/FragmentInterface.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Containers;
|
||||
|
||||
use ByJoby\HTML\ContainerInterface;
|
||||
use ByJoby\HTML\ContainerMutableInterface;
|
||||
|
||||
interface FragmentInterface extends ContainerMutableInterface
|
||||
{
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
/* HTML Object Strings | https://gitlab.com/byjoby/html-object-strings | MIT License */
|
||||
namespace HtmlObjectStrings;
|
||||
|
||||
class GenericTag implements TagInterface
|
||||
{
|
||||
use TagTrait;
|
||||
|
||||
const TAG = 'span';
|
||||
const SELFCLOSING = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->htmlInit();
|
||||
}
|
||||
|
||||
protected function htmlInit()
|
||||
{
|
||||
$this->tag = static::TAG;
|
||||
$this->selfClosing = static::SELFCLOSING;
|
||||
}
|
||||
}
|
92
src/Helpers/Attributes.php
Normal file
92
src/Helpers/Attributes.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Helpers;
|
||||
|
||||
use ArrayAccess;
|
||||
use ArrayIterator;
|
||||
use Exception;
|
||||
use IteratorAggregate;
|
||||
use Stringable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Holds and validates a set of HTML attribute name/value pairs for use in tags.
|
||||
*
|
||||
* @implements ArrayAccess<string,null|string|Stringable>
|
||||
* @implements IteratorAggregate<string,null|string|Stringable>
|
||||
*/
|
||||
class Attributes implements IteratorAggregate, ArrayAccess
|
||||
{
|
||||
/** @var array<string,null|string|Stringable> */
|
||||
protected $array = [];
|
||||
/** @var bool */
|
||||
protected $sorted = true;
|
||||
/** @var array<mixed,string> */
|
||||
protected $disallowed = [];
|
||||
|
||||
/**
|
||||
* @param null|array<string,null|string|Stringable> $array
|
||||
* @param array<mixed,string> $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<string,null|string|Stringable>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
96
src/Helpers/Classes.php
Normal file
96
src/Helpers/Classes.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Helpers;
|
||||
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use Exception;
|
||||
use IteratorAggregate;
|
||||
use Stringable;
|
||||
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
|
||||
{
|
||||
/** @var array<int,string|Stringable> */
|
||||
protected $classes = [];
|
||||
/** @var bool */
|
||||
protected $sorted = true;
|
||||
|
||||
/**
|
||||
* @param null|array<mixed,string|Stringable>|Traversable<mixed,string|Stringable> $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<int,string|Stringable>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
118
src/Helpers/Styles.php
Normal file
118
src/Helpers/Styles.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Helpers;
|
||||
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use Stringable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* A key difference in strategy between this class and Attributes or Classes is
|
||||
* that it does not make significant validation attempts. CSS is an evolving
|
||||
* language, and it would be a fool's errand to try and thoroughly validate it.
|
||||
*
|
||||
* To that end, this class is very accepting of not-obviously-malformed property
|
||||
* names and values.
|
||||
*
|
||||
* @implements ArrayAccess<string,null|string|Stringable>
|
||||
*/
|
||||
class Styles implements Countable, ArrayAccess, Stringable
|
||||
{
|
||||
/** @var array<string|string> */
|
||||
protected $styles = [];
|
||||
/** @var bool */
|
||||
protected $sorted = true;
|
||||
|
||||
/**
|
||||
* @param null|array<string,string|Stringable>|Traversable<string,string|Stringable>|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<string,string>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
/* HTML Object Strings | https://gitlab.com/byjoby/html-object-strings | MIT License */
|
||||
namespace HtmlObjectStrings;
|
||||
|
||||
class Input extends GenericTag
|
||||
{
|
||||
const TAG = 'input';
|
||||
const SELFCLOSING = true;
|
||||
const TYPE = 'text';
|
||||
|
||||
protected function htmlInit()
|
||||
{
|
||||
parent::htmlInit();
|
||||
$this->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;
|
||||
}
|
||||
}
|
23
src/NodeInterface.php
Normal file
23
src/NodeInterface.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML;
|
||||
|
||||
use ByJoby\HTML\Containers\DocumentInterface;
|
||||
use Stringable;
|
||||
|
||||
interface NodeInterface extends Stringable
|
||||
{
|
||||
public function parent(): null|NodeInterface;
|
||||
|
||||
public function setParent(
|
||||
null|NodeInterface $parent
|
||||
): static;
|
||||
|
||||
public function document(): null|DocumentInterface;
|
||||
|
||||
public function setDocument(
|
||||
null|DocumentInterface $parent
|
||||
): static;
|
||||
|
||||
public function detach(): static;
|
||||
}
|
27
src/Nodes/Comment.php
Normal file
27
src/Nodes/Comment.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use ByJoby\HTML\Traits\NodeTrait;
|
||||
use Stringable;
|
||||
|
||||
class Comment implements CommentInterface
|
||||
{
|
||||
use NodeTrait;
|
||||
|
||||
public function __construct(protected Stringable|string $value)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'<!-- %s -->',
|
||||
str_replace(
|
||||
'--', // regular hyphens
|
||||
'‑‑', // non-breaking hyphens
|
||||
$this->value
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
11
src/Nodes/CommentInterface.php
Normal file
11
src/Nodes/CommentInterface.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
use Stringable;
|
||||
|
||||
interface CommentInterface extends NodeInterface
|
||||
{
|
||||
public function __construct(Stringable|string $value);
|
||||
}
|
20
src/Nodes/Text.php
Normal file
20
src/Nodes/Text.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use ByJoby\HTML\Traits\NodeTrait;
|
||||
use Stringable;
|
||||
|
||||
class Text implements TextInterface
|
||||
{
|
||||
use NodeTrait;
|
||||
|
||||
public function __construct(protected Stringable|string $value)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return htmlentities(strip_tags($this->value));
|
||||
}
|
||||
}
|
11
src/Nodes/TextInterface.php
Normal file
11
src/Nodes/TextInterface.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
use Stringable;
|
||||
|
||||
interface TextInterface extends NodeInterface
|
||||
{
|
||||
public function __construct(Stringable|string $value);
|
||||
}
|
20
src/Nodes/UnsanitizedText.php
Normal file
20
src/Nodes/UnsanitizedText.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use ByJoby\HTML\Traits\NodeTrait;
|
||||
use Stringable;
|
||||
|
||||
class UnsanitizedText implements TextInterface
|
||||
{
|
||||
use NodeTrait;
|
||||
|
||||
public function __construct(protected Stringable|string $value)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
/* HTML Object Strings | https://gitlab.com/byjoby/html-object-strings | MIT License */
|
||||
namespace HtmlObjectStrings;
|
||||
|
||||
interface TagInterface
|
||||
{
|
||||
public function attr(string $name, $value = null);
|
||||
public function data(string $name, $value = null);
|
||||
|
||||
public function addClass(string $name);
|
||||
public function hasClass(string $name) : bool;
|
||||
public function removeClass(string $name);
|
||||
public function classes() : array;
|
||||
public function hidden($hidden=null);
|
||||
|
||||
public function string() : string;
|
||||
|
||||
public function __toString();
|
||||
}
|
127
src/TagTrait.php
127
src/TagTrait.php
|
@ -1,127 +0,0 @@
|
|||
<?php
|
||||
/* HTML Object Strings | https://gitlab.com/byjoby/html-object-strings | MIT License */
|
||||
namespace HtmlObjectStrings;
|
||||
|
||||
trait TagTrait
|
||||
{
|
||||
public $tag = 'span';
|
||||
public $selfClosing = false;
|
||||
public $content = null;
|
||||
|
||||
protected $classes = [];
|
||||
protected $attributes = [];
|
||||
protected $hidden = false;
|
||||
|
||||
public function hidden($hidden=null)
|
||||
{
|
||||
if ($hidden !== null) {
|
||||
$this->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 .= '</'.$this->tag.'>';
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->string();
|
||||
}
|
||||
}
|
20
src/Tags/AbstractContainerTag.php
Normal file
20
src/Tags/AbstractContainerTag.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Tags;
|
||||
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use ByJoby\HTML\Traits\ContainerMutableTrait;
|
||||
use ByJoby\HTML\Traits\ContainerTagTrait;
|
||||
use ByJoby\HTML\Traits\ContainerTrait;
|
||||
use ByJoby\HTML\Traits\TagTrait;
|
||||
use ByJoby\HTML\Traits\NodeTrait;
|
||||
|
||||
abstract class AbstractContainerTag implements ContainerTagInterface
|
||||
{
|
||||
use NodeTrait, TagTrait;
|
||||
use ContainerTrait, ContainerMutableTrait;
|
||||
use ContainerTagTrait {
|
||||
ContainerTagTrait::__toString insteadof TagTrait;
|
||||
}
|
||||
}
|
13
src/Tags/AbstractTag.php
Normal file
13
src/Tags/AbstractTag.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Tags;
|
||||
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use ByJoby\HTML\Traits\TagTrait;
|
||||
use ByJoby\HTML\Traits\NodeTrait;
|
||||
|
||||
abstract class AbstractTag implements TagInterface
|
||||
{
|
||||
use NodeTrait, TagTrait;
|
||||
}
|
10
src/Tags/ContainerTagInterface.php
Normal file
10
src/Tags/ContainerTagInterface.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Tags;
|
||||
|
||||
use ByJoby\HTML\ContainerMutableInterface;
|
||||
use ByJoby\HTML\NodeCollectionInterface;
|
||||
|
||||
interface ContainerTagInterface extends TagInterface, ContainerMutableInterface
|
||||
{
|
||||
}
|
19
src/Tags/TagInterface.php
Normal file
19
src/Tags/TagInterface.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Tags;
|
||||
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use ByJoby\HTML\Helpers\Styles;
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
use Stringable;
|
||||
|
||||
interface TagInterface extends NodeInterface
|
||||
{
|
||||
public function tag(): string;
|
||||
public function id(): null|string;
|
||||
public function setID(null|string|Stringable $id): static;
|
||||
public function classes(): Classes;
|
||||
public function attributes(): Attributes;
|
||||
public function styles(): Styles;
|
||||
}
|
62
src/Traits/ContainerMutableTrait.php
Normal file
62
src/Traits/ContainerMutableTrait.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Traits;
|
||||
|
||||
use ByJoby\HTML\ContainerMutableInterface;
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
use ByJoby\HTML\Nodes\Text;
|
||||
use ByJoby\HTML\Nodes\UnsanitizedText;
|
||||
use Stringable;
|
||||
|
||||
trait ContainerMutableTrait
|
||||
{
|
||||
use ContainerTrait;
|
||||
|
||||
public function addChild(
|
||||
NodeInterface|Stringable|string $child,
|
||||
bool $prepend = false,
|
||||
bool $skip_sanitize = false
|
||||
): static {
|
||||
if (!($child instanceof NodeInterface)) {
|
||||
if ($skip_sanitize) $child = new UnsanitizedText($child);
|
||||
else $child = new Text($child);
|
||||
}
|
||||
if ($this instanceof NodeInterface) {
|
||||
$child->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;
|
||||
}
|
||||
}
|
23
src/Traits/ContainerTagTrait.php
Normal file
23
src/Traits/ContainerTagTrait.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Traits;
|
||||
|
||||
trait ContainerTagTrait
|
||||
{
|
||||
use ContainerTrait;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$openingTag = sprintf('<%s>', implode(' ', $this->openingTagStrings()));
|
||||
$closingTag = sprintf('</%s>', $this->tag());
|
||||
if (!$this->children()) return $openingTag . $closingTag;
|
||||
else return implode(
|
||||
PHP_EOL,
|
||||
[
|
||||
$openingTag,
|
||||
implode(PHP_EOL, $this->children()),
|
||||
$closingTag
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
17
src/Traits/ContainerTrait.php
Normal file
17
src/Traits/ContainerTrait.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Traits;
|
||||
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
|
||||
trait ContainerTrait
|
||||
{
|
||||
/** @var array<int,NodeInterface> */
|
||||
protected $children = [];
|
||||
|
||||
/** @return array<int,NodeInterface> */
|
||||
public function children(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
}
|
69
src/Traits/NodeTrait.php
Normal file
69
src/Traits/NodeTrait.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Traits;
|
||||
|
||||
use ByJoby\HTML\ContainerInterface;
|
||||
use ByJoby\HTML\ContainerMutableInterface;
|
||||
use ByJoby\HTML\Containers\DocumentInterface;
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
use DeepCopy\DeepCopy;
|
||||
use Exception;
|
||||
|
||||
trait NodeTrait
|
||||
{
|
||||
/** @var null|NodeInterface */
|
||||
protected $parent;
|
||||
/** @var null|DocumentInterface */
|
||||
protected $document;
|
||||
|
||||
abstract function __toString();
|
||||
|
||||
public function parent(): null|NodeInterface
|
||||
{
|
||||
return $this->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);
|
||||
}
|
||||
}
|
101
src/Traits/TagTrait.php
Normal file
101
src/Traits/TagTrait.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Traits;
|
||||
|
||||
use ArrayIterator;
|
||||
use ByJoby\HTML\ContainerInterface;
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use ByJoby\HTML\Helpers\Styles;
|
||||
use ByJoby\HTML\NodeInterface;
|
||||
use Exception;
|
||||
use Stringable;
|
||||
|
||||
trait TagTrait
|
||||
{
|
||||
/** @var null|string */
|
||||
protected $id;
|
||||
/** @var Attributes */
|
||||
protected $attributes;
|
||||
/** @var Classes */
|
||||
protected $classes;
|
||||
/** @var Styles */
|
||||
protected $styles;
|
||||
|
||||
abstract function tag(): string;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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<int,string>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
/* HTML Object Strings | https://gitlab.com/byjoby/html-object-strings | MIT License */
|
||||
declare(strict_types=1);
|
||||
namespace HtmlObjectStrings;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class GenericTagTest extends TestCase
|
||||
{
|
||||
use \SteveGrunwell\PHPUnit_Markup_Assertions\MarkupAssertionsTrait;
|
||||
|
||||
public function testBasicClassManagement()
|
||||
{
|
||||
/*
|
||||
This section tests the basic class adding, removing, and output features
|
||||
*/
|
||||
$h = new GenericTag();
|
||||
//default is no classes
|
||||
$this->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");
|
||||
}
|
||||
}
|
59
tests/Helpers/AttributesTest.php
Normal file
59
tests/Helpers/AttributesTest.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Helpers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AttributesTest extends TestCase
|
||||
{
|
||||
public function testConstruction(): Attributes
|
||||
{
|
||||
$attributes = new Attributes();
|
||||
$this->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';
|
||||
}
|
||||
}
|
40
tests/Helpers/ClassesTest.php
Normal file
40
tests/Helpers/ClassesTest.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Helpers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ClassesTest extends TestCase
|
||||
{
|
||||
public function testConstruction(): Classes
|
||||
{
|
||||
$classes = new Classes();
|
||||
$this->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');
|
||||
}
|
||||
}
|
45
tests/Helpers/StylesTest.php
Normal file
45
tests/Helpers/StylesTest.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Helpers;
|
||||
|
||||
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();
|
||||
$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']);
|
||||
}
|
||||
}
|
16
tests/Nodes/CommentTest.php
Normal file
16
tests/Nodes/CommentTest.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CommentTest extends TestCase
|
||||
{
|
||||
public function testSimpleText(): void
|
||||
{
|
||||
$this->assertEquals('<!-- -->', new Comment(''));
|
||||
$this->assertEquals('<!-- foo -->', new Comment('foo'));
|
||||
$this->assertEquals('<!-- foo-bar -->', new Comment('foo-bar'));
|
||||
$this->assertNotEquals('<!-- foo--bar -->', new Comment('foo--bar'));
|
||||
}
|
||||
}
|
15
tests/Nodes/TextTest.php
Normal file
15
tests/Nodes/TextTest.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TextTest extends TestCase
|
||||
{
|
||||
public function testSimpleText(): void
|
||||
{
|
||||
$this->assertEquals('', new Text(''));
|
||||
$this->assertEquals('foo', new Text('foo'));
|
||||
$this->assertEquals('foo', new Text('<strong>foo</strong>'));
|
||||
}
|
||||
}
|
15
tests/Nodes/UnsanitizedTextTest.php
Normal file
15
tests/Nodes/UnsanitizedTextTest.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Nodes;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UnsanitizedTextTest extends TestCase
|
||||
{
|
||||
public function testSimpleText(): void
|
||||
{
|
||||
$this->assertEquals('', new UnsanitizedText(''));
|
||||
$this->assertEquals('foo', new UnsanitizedText('foo'));
|
||||
$this->assertEquals('<strong>foo</strong>', new UnsanitizedText('<strong>foo</strong>'));
|
||||
}
|
||||
}
|
61
tests/Tags/AbstractContainerTagTest.php
Normal file
61
tests/Tags/AbstractContainerTagTest.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Tags;
|
||||
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AbstractContainerTagTest extends TestCase
|
||||
{
|
||||
public function testDIV(): AbstractContainerTag
|
||||
{
|
||||
$div = $this->getMockForAbstractClass(AbstractContainerTag::class);
|
||||
$div->method('tag')->will($this->returnValue('div'));
|
||||
$this->assertEquals('<div></div>', $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 a="b">',
|
||||
'<span></span>',
|
||||
'</div>',
|
||||
]),
|
||||
$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 a="b">',
|
||||
'<span>',
|
||||
'<span></span>',
|
||||
'</span>',
|
||||
'</div>',
|
||||
]),
|
||||
$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 a="b"></div>', $div->__toString());
|
||||
$this->assertNull($span1->parent());
|
||||
}
|
||||
}
|
73
tests/Tags/AbstractTagTest.php
Normal file
73
tests/Tags/AbstractTagTest.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace ByJoby\HTML\Tags;
|
||||
|
||||
use ByJoby\HTML\Helpers\Attributes;
|
||||
use ByJoby\HTML\Helpers\Classes;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AbstractTagTest extends TestCase
|
||||
{
|
||||
public function testBR(): AbstractTag
|
||||
{
|
||||
$br = $this->getMockForAbstractClass(AbstractTag::class);
|
||||
$br->method('tag')->will($this->returnValue('br'));
|
||||
$this->assertEquals('<br/>', $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('<br id="foo"/>', $tag->__toString());
|
||||
$tag->setID(null);
|
||||
$this->assertNull($tag->id());
|
||||
$this->assertEquals('<br/>', $tag->__toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends clone testBR
|
||||
*/
|
||||
public function testAttributes(AbstractTag $tag): void
|
||||
{
|
||||
$tag->attributes()['b'] = 'c';
|
||||
$tag->attributes()['a'] = 'b';
|
||||
$this->assertEquals('<br a="b" b="c"/>', $tag->__toString());
|
||||
unset($tag->attributes()['a']);
|
||||
$this->assertEquals('<br b="c"/>', $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';
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue