diff --git a/src/Attributes.php b/src/Attributes.php index 71ceecba..8499ea91 100644 --- a/src/Attributes.php +++ b/src/Attributes.php @@ -111,6 +111,22 @@ public function getAttributes() return $this->attributes; } + /** + * Merge the given attributes + * + * @param Attributes $attributes + * + * @return $this + */ + public function merge(Attributes $attributes) + { + foreach ($attributes as $attribute) { + $this->addAttribute($attribute); + } + + return $this; + } + /** * Return true if the attribute with the given name exists, false otherwise * diff --git a/src/BaseHtmlElement.php b/src/BaseHtmlElement.php index 61d09cd8..6a443ff2 100644 --- a/src/BaseHtmlElement.php +++ b/src/BaseHtmlElement.php @@ -301,26 +301,11 @@ protected function registerAttributeCallbacks(Attributes $attributes) { } - public function add($content) + public function addHtml(ValidHtml ...$content) { $this->ensureAssembled(); - parent::add($content); - - return $this; - } - - public function setContent($content) - { - // setContent() calls $this->add() which would assemble the element and that does not make any sense here - // Plus, this allows subclasses of BaseHtmlElement to add content before assemble() -- - // in the constructor for example - - $this->hasBeenAssembled = true; - - parent::setContent($content); - - $this->hasBeenAssembled = false; + parent::addHtml(...$content); return $this; } diff --git a/src/Error.php b/src/Error.php index 9decab86..a4ab2531 100644 --- a/src/Error.php +++ b/src/Error.php @@ -41,7 +41,7 @@ public static function show($error) $result = static::renderErrorMessage($msg); if (static::showTraces()) { - $result->add(Html::tag('pre', $error->getTraceAsString())); + $result->addHtml(Html::tag('pre', $error->getTraceAsString())); } return $result; @@ -101,7 +101,7 @@ protected static function createMessageForException($exception) protected static function renderErrorMessage($message) { $output = new HtmlDocument(); - $output->add( + $output->addHtml( Html::tag('div', ['class' => 'exception'], [ Html::tag('h1', [ Html::tag('i', ['class' => 'icon-bug']), diff --git a/src/Form.php b/src/Form.php index 7cadc000..8264bab3 100644 --- a/src/Form.php +++ b/src/Form.php @@ -346,11 +346,11 @@ protected function onError() $message = $message->getMessage(); } - $errors->add(Html::tag('li', $message)); + $errors->addHtml(Html::tag('li', $message)); } if (! $errors->isEmpty()) { - $this->prepend($errors); + $this->prependHtml($errors); } } diff --git a/src/FormDecorator/DdDtDecorator.php b/src/FormDecorator/DdDtDecorator.php index bf955d84..fcdeacd5 100644 --- a/src/FormDecorator/DdDtDecorator.php +++ b/src/FormDecorator/DdDtDecorator.php @@ -5,6 +5,7 @@ use ipl\Html\BaseHtmlElement; use ipl\Html\FormElement\BaseFormElement; use ipl\Html\Html; +use ipl\Html\ValidHtml; class DdDtDecorator extends BaseHtmlElement implements DecoratorInterface { @@ -92,11 +93,11 @@ protected function renderErrors() return null; } - public function add($content) + public function addHtml(ValidHtml ...$content) { // TODO: is this required? - if ($content !== $this->wrappedElement) { - parent::add($content); + if (! in_array($this->wrappedElement, $content, true)) { + parent::addHtml(...$content); } return $this; @@ -104,7 +105,7 @@ public function add($content) protected function assemble() { - $this->add([$this->dt(), $this->dd()]); + $this->addHtml($this->dt(), $this->dd()); $this->ready = true; } diff --git a/src/FormDecorator/DivDecorator.php b/src/FormDecorator/DivDecorator.php index 1af246af..574bc265 100644 --- a/src/FormDecorator/DivDecorator.php +++ b/src/FormDecorator/DivDecorator.php @@ -10,6 +10,7 @@ use ipl\Html\FormElement\HiddenElement; use ipl\Html\Html; use ipl\Html\HtmlElement; +use ipl\Html\Text; /** * Form element decorator based on div elements @@ -84,10 +85,12 @@ protected function assembleElement() protected function assembleErrors() { - $errors = new HtmlElement('ul', ['class' => static::ERROR_CLASS]); + $errors = new HtmlElement('ul', Attributes::create(['class' => static::ERROR_CLASS])); foreach ($this->formElement->getMessages() as $message) { - $errors->add(new HtmlElement('li', ['class' => static::ERROR_CLASS], $message)); + $errors->addHtml( + new HtmlElement('li', Attributes::create(['class' => static::ERROR_CLASS]), Text::create($message)) + ); } if (! $errors->isEmpty()) { @@ -119,7 +122,7 @@ protected function assemble() $this->getAttributes()->add('class', static::ERROR_HINT_CLASS); } - $this->add(array_filter([ + $this->addHtml(...Html::wantHtmlList([ $this->assembleLabel(), $this->assembleElement(), $this->assembleDescription(), diff --git a/src/FormElement/FormElements.php b/src/FormElement/FormElements.php index 59605bb1..078d0043 100644 --- a/src/FormElement/FormElements.php +++ b/src/FormElement/FormElements.php @@ -124,7 +124,7 @@ public function addElement($typeOrElement, $name = null, $options = null) $this ->registerElement($element) // registerElement() must be called first because of the name check ->decorate($element) - ->add($element); + ->addHtml($element); return $this; } diff --git a/src/FormElement/SelectElement.php b/src/FormElement/SelectElement.php index 488c5c7c..1e168545 100644 --- a/src/FormElement/SelectElement.php +++ b/src/FormElement/SelectElement.php @@ -111,7 +111,7 @@ protected function makeOption($value, $label) if (is_array($label)) { $grp = Html::tag('optgroup', ['label' => $value]); foreach ($label as $option => $val) { - $grp->add($this->makeOption($option, $val)); + $grp->addHtml($this->makeOption($option, $val)); } return $grp; @@ -134,8 +134,6 @@ protected function makeOption($value, $label) protected function assemble() { - foreach ($this->optionContent as $value => $option) { - $this->add($option); - } + $this->addHtml(...array_values($this->optionContent)); } } diff --git a/src/Html.php b/src/Html.php index 9db26ecc..afcba39b 100644 --- a/src/Html.php +++ b/src/Html.php @@ -26,17 +26,32 @@ abstract class Html */ public static function tag($name, $attributes = null, $content = null) { - if ($attributes instanceof ValidHtml || is_scalar($attributes)) { - $content = $attributes; + if ($content !== null) { + // If not null, it's html content, no question + $content = static::wantHtmlList($content); + } elseif ($attributes instanceof ValidHtml || is_scalar($attributes)) { + // Otherwise $attributes may be $content, but only if definitely **NOT** attributes + $content = static::wantHtmlList($attributes); $attributes = null; - } elseif (is_iterable($attributes)) { - if (is_int(iterable_key_first($attributes))) { - $content = $attributes; + } + + if ($attributes !== null) { + if (! is_iterable($attributes) || ! is_int(iterable_key_first($attributes))) { + // Not an array (e.g. instance of Attributes) or an associative array + $attributes = Attributes::wantAttributes($attributes); + } elseif (is_iterable($attributes)) { + // $attributes may still be $content, in case of a sequenced array + if ($content !== null) { + // But not if there's already $content + throw new InvalidArgumentException('Value of argument $attributes are no attributes'); + } + + $content = static::wantHtmlList($attributes); $attributes = null; } } - return new HtmlElement($name, $attributes, $content); + return new HtmlElement($name, $attributes, ...($content ?: [])); } /** @@ -104,7 +119,7 @@ public static function wrapEach($list, $wrapper) $result = new HtmlDocument(); foreach ($list as $name => $value) { if (is_string($wrapper)) { - $result->add(Html::tag($wrapper, $value)); + $result->addHtml(Html::tag($wrapper, $value)); } elseif (is_callable($wrapper)) { $result->add($wrapper($name, $value)); } else { @@ -139,7 +154,7 @@ public static function wantHtml($any) $html = new HtmlDocument(); foreach ($any as $el) { if ($el !== null) { - $html->add(static::wantHtml($el)); + $html->addHtml(static::wantHtml($el)); } } @@ -152,6 +167,32 @@ public static function wantHtml($any) } } + /** + * Accept any input and return it as list of ValidHtml + * + * @param mixed $content + * + * @return ValidHtml[] + */ + public static function wantHtmlList($content) + { + $list = []; + + if ($content === null) { + return $list; + } elseif (! is_iterable($content)) { + $list[] = static::wantHtml($content); + } elseif ($content instanceof ValidHtml) { + $list[] = $content; + } else { + foreach ($content as $part) { + $list = array_merge($list, static::wantHtmlList($part)); + } + } + + return $list; + } + /** * Get whether the given variable be rendered as a string * diff --git a/src/HtmlDocument.php b/src/HtmlDocument.php index 794055d2..f545700f 100644 --- a/src/HtmlDocument.php +++ b/src/HtmlDocument.php @@ -83,7 +83,24 @@ public function getContent() public function setContent($content) { $this->content = []; - $this->add($content); + $this->setHtmlContent(...Html::wantHtmlList($content)); + + return $this; + } + + /** + * Set content + * + * @param ValidHtml ...$content + * + * @return $this + */ + public function setHtmlContent(ValidHtml ...$content) + { + $this->content = []; + foreach ($content as $element) { + $this->addIndexedContent($element); + } return $this; } @@ -144,12 +161,22 @@ public function getFirst($tag) */ public function add($content) { - if (is_iterable($content) && ! $content instanceof ValidHtml) { - foreach ($content as $c) { - $this->add($c); - } - } elseif ($content !== null) { - $this->addIndexedContent(Html::wantHtml($content)); + $this->addHtml(...Html::wantHtmlList($content)); + + return $this; + } + + /** + * Add content + * + * @param ValidHtml ...$content + * + * @return $this + */ + public function addHtml(ValidHtml ...$content) + { + foreach ($content as $element) { + $this->addIndexedContent($element); } return $this; @@ -211,16 +238,24 @@ public function contains(ValidHtml $element) */ public function prepend($content) { - if (is_iterable($content) && ! $content instanceof ValidHtml) { - foreach (array_reverse(is_array($content) ? $content : iterator_to_array($content)) as $c) { - $this->prepend($c); - } - } elseif ($content !== null) { - $pos = 0; - $html = Html::wantHtml($content); + $this->prependHtml(...Html::wantHtmlList($content)); + + return $this; + } + + /** + * Prepend content + * + * @param ValidHtml ...$content + * + * @return $this + */ + public function prependHtml(ValidHtml ...$content) + { + foreach (array_reverse($content) as $html) { array_unshift($this->content, $html); $this->incrementIndexKeys(); - $this->addObjectPosition($html, $pos); + $this->addObjectPosition($html, 0); } return $this; diff --git a/src/HtmlElement.php b/src/HtmlElement.php index 75eeff2f..4f5d1628 100644 --- a/src/HtmlElement.php +++ b/src/HtmlElement.php @@ -12,34 +12,32 @@ class HtmlElement extends BaseHtmlElement /** * Create a new HTML element from the given tag, attributes and content * - * @param string $tag The tag for the element - * @param Attributes|array $attributes The HTML attributes for the element - * @param ValidHtml|string|array $content The content of the element + * @param string $tag The tag for the element + * @param Attributes $attributes The HTML attributes for the element + * @param ValidHtml ...$content The content of the element */ - public function __construct($tag, $attributes = null, $content = null) + public function __construct($tag, Attributes $attributes = null, ValidHtml ...$content) { $this->tag = $tag; if ($attributes !== null) { - $this->getAttributes()->add($attributes); + $this->getAttributes()->merge($attributes); } - if ($content !== null) { - $this->setContent($content); - } + $this->setHtmlContent(...$content); } /** * Create a new HTML element from the given tag, attributes and content * - * @param string $tag The tag for the element - * @param Attributes|array $attributes The HTML attributes for the element - * @param ValidHtml|string|array $content The content of the element + * @param string $tag The tag for the element + * @param mixed $attributes The HTML attributes for the element + * @param mixed $content The content of the element * * @return static */ public static function create($tag, $attributes = null, $content = null) { - return new static($tag, $attributes, $content); + return new static($tag, Attributes::wantAttributes($attributes), ...Html::wantHtmlList($content)); } } diff --git a/src/Table.php b/src/Table.php index 7087b2a3..28a67387 100644 --- a/src/Table.php +++ b/src/Table.php @@ -4,7 +4,6 @@ use RuntimeException; use stdClass; -use Traversable; class Table extends BaseHtmlElement { @@ -25,55 +24,66 @@ class Table extends BaseHtmlElement /** @var HtmlElement */ private $footer; + public function addHtml(ValidHtml ...$content) + { + foreach ($content as $html) { + if ($html instanceof BaseHtmlElement) { + switch ($html->getTag()) { + case 'tr': + $this->getBody()->addHtml($html); + + break; + case 'thead': + parent::addHtml($html); + $this->header = $html; + + break; + case 'tbody': + parent::addHtml($html); + $this->body = $html; + + break; + case 'tfoot': + parent::addHtml($html); + $this->footer = $html; + + break; + case 'caption': + if ($this->caption === null) { + $this->prependHtml($html); + $this->caption = $html; + } else { + throw new RuntimeException( + 'Tables allow only one tag' + ); + } + + break; + default: + $this->getBody()->addHtml(static::row([$html])); + } + } else { + $this->getBody()->addHtml(static::row([$html])); + } + } + + return $this; + } + /** - * @param array|ValidHtml|string $content + * @param mixed $content * @return $this */ public function add($content) { - $this->ensureAssembled(); - - if ($content instanceof BaseHtmlElement) { - switch ($content->getTag()) { - case 'tr': - $this->getBody()->add($content); - break; - - case 'thead': - parent::add($content); - $this->header = $content; - break; - - case 'tbody': - parent::add($content); - $this->body = $content; - break; - - case 'tfoot': - parent::add($content); - $this->footer = $content; - break; - - case 'caption': - if ($this->caption === null) { - $this->prepend($content); - $this->caption = $content; - } else { - throw new RuntimeException( - 'Tables allow only one tag' - ); - } - break; - - default: - $this->getBody()->add(static::row([$content])); - } - } elseif ($content instanceof stdClass) { - $this->getBody()->add(static::row((array) $content)); + if ($content instanceof stdClass) { + $this->getBody()->addHtml(static::row((array) $content)); } elseif (is_iterable($content)) { - $this->getBody()->add(static::row($content)); + $this->getBody()->addHtml(static::row($content)); + } elseif ($content instanceof ValidHtml) { + $this->addHtml($content); } else { - $this->getBody()->add(static::row([$content])); + $this->getBody()->addHtml(static::row([$content])); } return $this; @@ -84,17 +94,17 @@ public function add($content) * * Will be rendered as a "caption" HTML element * - * @param $caption + * @param mixed $caption * @return $this */ public function setCaption($caption) { if ($caption instanceof BaseHtmlElement && $caption->getTag() === 'caption') { $this->caption = $caption; - $this->prepend($caption); + $this->prependHtml($caption); } elseif ($this->caption === null) { - $this->caption = new HtmlElement('caption', null, $caption); - $this->prepend($this->caption); + $this->caption = new HtmlElement('caption', null, ...Html::wantHtmlList($caption)); + $this->prependHtml($this->caption); } else { $this->caption->setContent($caption); } @@ -148,7 +158,7 @@ public static function row($row, $attributes = null, $tag = 'td') { $tr = static::tr(); foreach ((array) $row as $value) { - $tr->add(Html::tag($tag, null, $value)); + $tr->addHtml(Html::tag($tag, null, $value)); } if ($attributes !== null) { @@ -164,7 +174,7 @@ public static function row($row, $attributes = null, $tag = 'td') public function getBody() { if ($this->body === null) { - $this->add(Html::tag('tbody')->setSeparator("\n")); + $this->addHtml(Html::tag('tbody')->setSeparator("\n")); } return $this->body; @@ -176,7 +186,7 @@ public function getBody() public function getHeader() { if ($this->header === null) { - $this->add(Html::tag('thead')->setSeparator("\n")); + $this->addHtml(Html::tag('thead')->setSeparator("\n")); } return $this->header; @@ -188,7 +198,7 @@ public function getHeader() public function getFooter() { if ($this->footer === null) { - $this->add(Html::tag('tfoot')->setSeparator("\n")); + $this->addHtml(Html::tag('tfoot')->setSeparator("\n")); } return $this->footer; diff --git a/src/TemplateString.php b/src/TemplateString.php index bb61e68f..611ea121 100644 --- a/src/TemplateString.php +++ b/src/TemplateString.php @@ -120,7 +120,7 @@ protected function parseTemplates($for = null) )); } - return (new HtmlDocument())->add(HtmlString::create($buffer)); + return (new HtmlDocument())->addHtml(HtmlString::create($buffer)); } /** diff --git a/tests/HtmlDocumentTest.php b/tests/HtmlDocumentTest.php index 7584552f..a7f9298b 100644 --- a/tests/HtmlDocumentTest.php +++ b/tests/HtmlDocumentTest.php @@ -162,6 +162,70 @@ public function testAcceptsObjectsWhichCanBeCastedToString() $this->assertEquals('Some String <:-)', $a->render()); } + public function testSetContentFlattensNestedArrays() + { + $doc = new HtmlDocument(); + $doc->setSeparator(';'); + $doc->setContent([ + h::tag('span'), + [ + 'foo', + 'bar', + [ + h::tag('p', 'test'), + h::tag('strong', 'bla') + ] + ] + ]); + $this->assertHtml( + ';foo;bar;

test

;bla', + $doc + ); + } + + + public function testAddFlattensNestedArrays() + { + $doc = new HtmlDocument(); + $doc->setSeparator(';'); + $doc->add([ + h::tag('span'), + [ + 'foo', + 'bar', + [ + h::tag('p', 'test'), + h::tag('strong', 'bla') + ] + ] + ]); + $this->assertHtml( + ';foo;bar;

test

;bla', + $doc + ); + } + + public function testPrependFlattensNestedArrays() + { + $doc = new HtmlDocument(); + $doc->setSeparator(';'); + $doc->prepend([ + h::tag('span'), + [ + 'foo', + 'bar', + [ + h::tag('p', 'test'), + h::tag('strong', 'bla') + ] + ] + ]); + $this->assertHtml( + ';foo;bar;

test

;bla', + $doc + ); + } + public function testSkipsNullValues() { $a = new HtmlDocument(); diff --git a/tests/HtmlTest.php b/tests/HtmlTest.php index 55ba44c6..426afb53 100644 --- a/tests/HtmlTest.php +++ b/tests/HtmlTest.php @@ -2,6 +2,7 @@ namespace ipl\Tests\Html; +use InvalidArgumentException; use ipl\Html\Html; class HtmlTest extends TestCase @@ -16,16 +17,36 @@ public function testTagSupportsIterable() $html = Html::tag('div', $content()); $this->assertHtml('
foobar
', $html); + + $html = Html::tag('div', ['class' => 'foobar'], $content()); + + $this->assertHtml('
foobar
', $html); } public function testWrapsListsWithSimpleHtmlTags() { - $this->assertXmlStringEqualsXmlString( + $this->assertHtml( '', - Html::tag('ul', Html::wrapEach(['a', 'b', 'c'], 'li'))->render() + Html::tag('ul', Html::wrapEach(['a', 'b', 'c'], 'li')) + ); + $this->assertHtml( + '', + Html::tag('ul', ['class' => 'simple'], Html::wrapEach(['a', 'b', 'c'], 'li')) ); } + public function testTagComplainsAboutAttributesNotBeingAttributes() + { + $this->expectException(InvalidArgumentException::class); + Html::tag('span', ['foo-class'], ['foo-content']); + } + + public function testTagDoesNotIgnoreContent() + { + $this->expectException(InvalidArgumentException::class); + Html::tag('span', Html::tag('a'), Html::tag('b')); + } + public function testWrapsListsWithCallback() { $options = [ @@ -39,13 +60,13 @@ public function testWrapsListsWithCallback() ], $value); })); - $this->assertXmlStringEqualsXmlString( + $this->assertHtml( '', - $select->render() + $select ); } }