diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:38:04 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:38:04 +0000 |
commit | 1ff5c35de5dbd70a782875a91dd2232fd01b002b (patch) | |
tree | 77d9ce5e1bf78b3e6ef79f8f6e7861e2ced3c09b /vendor/ipl/html/src | |
parent | Initial commit. (diff) | |
download | icinga-php-library-1ff5c35de5dbd70a782875a91dd2232fd01b002b.tar.xz icinga-php-library-1ff5c35de5dbd70a782875a91dd2232fd01b002b.zip |
Adding upstream version 0.10.1.upstream/0.10.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ipl/html/src')
42 files changed, 5344 insertions, 0 deletions
diff --git a/vendor/ipl/html/src/Attribute.php b/vendor/ipl/html/src/Attribute.php new file mode 100644 index 0000000..0cf13b8 --- /dev/null +++ b/vendor/ipl/html/src/Attribute.php @@ -0,0 +1,301 @@ +<?php + +namespace ipl\Html; + +use InvalidArgumentException; + +/** + * HTML Attribute + * + * Every single HTML attribute is (or should be) an instance of this class. + * This guarantees that every attribute is safe and escaped correctly. + * + * Usually attributes are not instantiated directly, but created through an HTML + * element's exposed methods. + */ +class Attribute +{ + /** @var string */ + protected $name; + + /** @var string|array|bool|null */ + protected $value; + + /** + * Create a new HTML attribute from the given name and value + * + * @param string $name The name of the attribute + * @param string|bool|array|null $value The value of the attribute + * + * @throws InvalidArgumentException If the name of the attribute contains special characters + */ + public function __construct($name, $value = null) + { + $this->setName($name)->setValue($value); + } + + /** + * Create a new HTML attribute from the given name and value + * + * @param string $name The name of the attribute + * @param string|bool|array|null $value The value of the attribute + * + * @return static + * + * @throws InvalidArgumentException If the name of the attribute contains special characters + */ + public static function create($name, $value) + { + return new static($name, $value); + } + + /** + * Create a new empty HTML attribute from the given name + * + * The value of the attribute will be null after construction. + * + * @param string $name The name of the attribute + * + * @return static + * + * @throws InvalidArgumentException If the name of the attribute contains special characters + */ + public static function createEmpty($name) + { + return new static($name, null); + } + + /** + * Escape the name of an attribute + * + * Makes sure that the name of an attribute really is a string. + * + * @param string $name + * + * @return string + */ + public static function escapeName($name) + { + return (string) $name; + } + + /** + * Escape the value of an attribute + * + * If the value is an array, returns the string representation + * of all array elements joined with the specified glue string. + * + * Values are escaped according to the HTML5 double-quoted attribute value syntax: + * {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 }. + * + * @param string|array $value + * @param string $glue Glue string to join elements if value is an array + * + * @return string + */ + public static function escapeValue($value, $glue = ' ') + { + if (is_array($value)) { + $value = implode($glue, $value); + } + + // We force double-quoted attribute value syntax so let's start by escaping double quotes + $value = str_replace('"', '"', $value); + + // In addition, values must not contain ambiguous ampersands + $value = preg_replace_callback( + '/&[0-9A-Z]+;/i', + function ($match) { + $subject = $match[0]; + + if (htmlspecialchars_decode($subject, ENT_COMPAT | ENT_HTML5) === $subject) { + // Ambiguous ampersand + return str_replace('&', '&', $subject); + } + + return $subject; + }, + $value + ); + + return $value; + } + + /** + * Get the name of the attribute + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the name of the attribute + * + * @param string $name + * + * @return $this + * + * @throws InvalidArgumentException If the name contains special characters + */ + protected function setName($name) + { + if (! preg_match('/^[a-z][a-z0-9:-]*$/i', $name)) { + throw new InvalidArgumentException(sprintf( + 'Attribute names with special characters are not yet allowed: %s', + $name + )); + } + + $this->name = $name; + + return $this; + } + + /** + * Get the value of the attribute + * + * @return string|bool|array|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Set the value of the attribute + * + * @param string|bool|array|null $value + * + * @return $this + */ + public function setValue($value) + { + $this->value = $value; + + return $this; + } + + /** + * Add the given value(s) to the attribute + * + * @param string|array $value The value(s) to add + * + * @return $this + */ + public function addValue($value) + { + $this->value = array_merge((array) $this->value, (array) $value); + + return $this; + } + + /** + * Remove the given value(s) from the attribute + * + * The current value is set to null if it matches the value to remove + * or is in the array of values to remove. + * + * If the current value is an array, all elements are removed which + * match the value(s) to remove. + * + * Does nothing if there is no such value to remove. + * + * @param string|array $value The value(s) to remove + * + * @return $this + */ + public function removeValue($value) + { + $value = (array) $value; + + $current = $this->getValue(); + + if (is_array($current)) { + $this->setValue(array_diff($current, $value)); + } elseif (in_array($current, $value, true)) { + $this->setValue(null); + } + + return $this; + } + + /** + * Test and return true if the attribute is boolean, false otherwise + * + * @return bool + */ + public function isBoolean() + { + return is_bool($this->value); + } + + /** + * Test and return true if the attribute is empty, false otherwise + * + * Null and the empty array will be considered empty. + * + * @return bool + */ + public function isEmpty() + { + return $this->value === null || $this->value === []; + } + + /** + * Render the attribute to HTML + * + * If the value of the attribute is of type boolean, it will be rendered as + * {@link http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes boolean attribute}. + * Note that in this case if the value of the attribute is false, the empty string will be returned. + * + * If the value of the attribute is null or an empty array, + * the empty string will be returned as well. + * + * Escaping of the attribute's value takes place automatically using {@link Attribute::escapeValue()}. + * + * @return string + */ + public function render() + { + if ($this->isEmpty()) { + return ''; + } + + if ($this->isBoolean()) { + if ($this->value) { + return $this->renderName(); + } + + return ''; + } else { + return sprintf( + '%s="%s"', + $this->renderName(), + $this->renderValue() + ); + } + } + + /** + * Render the name of the attribute to HTML + * + * @return string + */ + public function renderName() + { + return static::escapeName($this->name); + } + + /** + * Render the value of the attribute to HTML + * + * @return string + */ + public function renderValue() + { + return static::escapeValue($this->value); + } +} diff --git a/vendor/ipl/html/src/Attributes.php b/vendor/ipl/html/src/Attributes.php new file mode 100644 index 0000000..ae15ef8 --- /dev/null +++ b/vendor/ipl/html/src/Attributes.php @@ -0,0 +1,521 @@ +<?php + +namespace ipl\Html; + +use ArrayAccess; +use ArrayIterator; +use InvalidArgumentException; +use IteratorAggregate; +use Traversable; + +use function ipl\Stdlib\get_php_type; + +/** + * HTML attributes + * + * HTML attributes provide additional information about HTML elements, that configure the elements or adjust their + * behavior in various ways. + * + * Attributes usually come in name-value pairs and are rendered as name="value". + */ +class Attributes implements ArrayAccess, IteratorAggregate +{ + /** @var Attribute[] */ + protected $attributes = []; + + /** @var callable[] */ + protected $callbacks = []; + + /** @var string */ + protected $prefix = ''; + + /** @var callable[] */ + protected $setterCallbacks = []; + + /** + * Create new HTML attributes + * + * @param array $attributes + */ + public function __construct(array $attributes = null) + { + if (empty($attributes)) { + return; + } + + foreach ($attributes as $key => $value) { + if ($value instanceof Attribute) { + $this->addAttribute($value); + } elseif (is_string($key)) { + $this->add($key, $value); + } elseif (is_array($value) && count($value) === 2) { + $this->add(array_shift($value), array_shift($value)); + } + } + } + + /** + * Create new HTML attributes + * + * @param array $attributes + * + * @return static + */ + public static function create(array $attributes = null) + { + return new static($attributes); + } + + /** + * Ensure that the given attributes of mixed type are converted to an instance of attributes + * + * The conversion procedure is as follows: + * + * If the given attributes is already an instance of Attributes, returns the very same element. + * If the attributes are given as an array of attribute name-value pairs, they are used to + * construct and return a new Attributes instance. + * If the attributes are null, an empty new instance of Attributes is returned. + * + * @param array|static|null $attributes + * + * @return static + * + * @throws InvalidArgumentException In case the given attributes are of an unsupported type + */ + public static function wantAttributes($attributes) + { + if ($attributes instanceof self) { + return $attributes; + } + + if (is_array($attributes)) { + return new static($attributes); + } + + if ($attributes === null) { + return new static(); + } + + throw new InvalidArgumentException(sprintf( + 'Attributes instance, array or null expected. Got %s instead.', + get_php_type($attributes) + )); + } + + /** + * Get the collection of attributes as array + * + * @return Attribute[] + */ + 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); + } + + foreach ($attributes->callbacks as $name => $getter) { + $setter = null; + if (isset($attributes->setterCallbacks[$name])) { + $setter = $attributes->setterCallbacks[$name]; + } + + $this->registerAttributeCallback($name, $getter, $setter); + } + + return $this; + } + + /** + * Return true if the attribute with the given name exists, false otherwise + * + * @param string $name + * + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->attributes); + } + + /** + * Get the attribute with the given name + * + * If the attribute does not yet exist, it is automatically created and registered to this Attributes instance. + * + * @param string $name + * + * @return Attribute + * + * @throws InvalidArgumentException If the attribute does not yet exist and its name contains special characters + */ + public function get($name) + { + if (! $this->has($name)) { + $this->attributes[$name] = Attribute::createEmpty($name); + } + + return $this->attributes[$name]; + } + + /** + * Set the given attribute(s) + * + * If the attribute with the given name already exists, it gets overridden. + * + * @param string|array|Attribute|self $attribute The attribute(s) to add + * @param string|bool|array $value The value of the attribute + * + * @return $this + * + * @throws InvalidArgumentException If the attribute name contains special characters + */ + public function set($attribute, $value = null) + { + if ($attribute instanceof self) { + foreach ($attribute as $a) { + $this->setAttribute($a); + } + + return $this; + } + + if ($attribute instanceof Attribute) { + $this->setAttribute($attribute); + + return $this; + } + + if (is_array($attribute)) { + foreach ($attribute as $name => $value) { + $this->set($name, $value); + } + + return $this; + } + + if (array_key_exists($attribute, $this->setterCallbacks)) { + $callback = $this->setterCallbacks[$attribute]; + + $callback($value); + + return $this; + } + + $this->attributes[$attribute] = Attribute::create($attribute, $value); + + return $this; + } + + /** + * Add the given attribute(s) + * + * If an attribute with the same name already exists, the attribute's value will be added to the current value of + * the attribute. + * + * @param string|array|Attribute|self $attribute The attribute(s) to add + * @param string|bool|array $value The value of the attribute + * + * @return $this + * + * @throws InvalidArgumentException If the attribute does not yet exist and its name contains special characters + */ + public function add($attribute, $value = null) + { + if ($attribute === null) { + return $this; + } + + if ($attribute instanceof self) { + foreach ($attribute as $attr) { + $this->add($attr); + } + + return $this; + } + + if (is_array($attribute)) { + foreach ($attribute as $name => $value) { + $this->add($name, $value); + } + + return $this; + } + + if ($attribute instanceof Attribute) { + $this->addAttribute($attribute); + + return $this; + } + + if (array_key_exists($attribute, $this->setterCallbacks)) { + $callback = $this->setterCallbacks[$attribute]; + + $callback($value); + + return $this; + } + + if (! array_key_exists($attribute, $this->attributes)) { + $this->attributes[$attribute] = Attribute::create($attribute, $value); + } else { + $this->attributes[$attribute]->addValue($value); + } + + return $this; + } + + /** + * Remove the attribute with the given name or remove the given value from the attribute + * + * @param string $name The name of the attribute + * @param null|string|array $value The value to remove if specified + * + * @return Attribute|false + */ + public function remove($name, $value = null) + { + if (! $this->has($name)) { + return false; + } + + $attribute = $this->attributes[$name]; + + if ($value === null) { + unset($this->attributes[$name]); + } else { + $attribute->removeValue($value); + } + + return $attribute; + } + + /** + * Set the specified attribute + * + * @param Attribute $attribute + * + * @return $this + */ + public function setAttribute(Attribute $attribute) + { + $this->attributes[$attribute->getName()] = $attribute; + + return $this; + } + + /** + * Add the specified attribute + * + * If an attribute with the same name already exists, the given attribute's value + * will be added to the current value of the attribute. + * + * @param Attribute $attribute + * + * @return $this + */ + public function addAttribute(Attribute $attribute) + { + $name = $attribute->getName(); + + if ($this->has($name)) { + $this->attributes[$name]->addValue($attribute->getValue()); + } else { + $this->attributes[$name] = $attribute; + } + + return $this; + } + + /** + * Get the attributes name prefix + * + * @return string|null + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Set the attributes name prefix + * + * @param string $prefix + * + * @return $this + */ + public function setPrefix($prefix) + { + $this->prefix = $prefix; + + return $this; + } + + /** + * Register callback for an attribute + * + * @param string $name Name of the attribute to register the callback for + * @param callable $callback Callback to call when retrieving the attribute + * @param callable $setterCallback Callback to call when setting the attribute + * + * @return $this + * + * @throws InvalidArgumentException If $callback is not callable or if $setterCallback is set and not callable + */ + public function registerAttributeCallback($name, $callback, $setterCallback = null) + { + if ($callback !== null) { + if (! is_callable($callback)) { + throw new InvalidArgumentException(__METHOD__ . ' expects a callable callback'); + } + + $this->callbacks[$name] = $callback; + } + + if ($setterCallback !== null) { + if (! is_callable($setterCallback)) { + throw new InvalidArgumentException(__METHOD__ . ' expects a callable setterCallback'); + } + + $this->setterCallbacks[$name] = $setterCallback; + } + + return $this; + } + + /** + * Render attributes to HTML + * + * If the value of an attribute is of type boolean, it will be rendered as + * {@link http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes boolean attribute}. + * + * If the value of an attribute is null, it will be skipped. + * + * HTML-escaping of the attributes' values takes place automatically using {@link Attribute::escapeValue()}. + * + * @return string + * + * @throws InvalidArgumentException If the result of a callback is invalid + */ + public function render() + { + $attributes = $this->attributes; + foreach ($this->callbacks as $name => $callback) { + $attribute = call_user_func($callback); + if ($attribute instanceof Attribute) { + if ($attribute->isEmpty()) { + continue; + } + } elseif ($attribute === null) { + continue; + } elseif (is_scalar($attribute)) { + $attribute = Attribute::create($name, $attribute); + } else { + throw new InvalidArgumentException(sprintf( + 'A registered attribute callback must return a scalar, null' + . ' or an Attribute, got a %s', + get_php_type($attribute) + )); + } + + $name = $attribute->getName(); + if (isset($attributes[$name])) { + $attributes[$name] = clone $attributes[$name]; + $attributes[$name]->addValue($attribute->getValue()); + } else { + $attributes[$name] = $attribute; + } + } + + $parts = []; + foreach ($attributes as $attribute) { + if ($attribute->isEmpty()) { + continue; + } + + $parts[] = $attribute->render(); + } + + if (empty($parts)) { + return ''; + } + + $separator = ' ' . $this->getPrefix(); + + return $separator . implode($separator, $parts); + } + + /** + * Get whether the attribute with the given name exists + * + * @param string $name Name of the attribute + * + * @return bool + */ + public function offsetExists($name): bool + { + return $this->has($name); + } + + /** + * Get the attribute with the given name + * + * If the attribute does not yet exist, it is automatically created and registered to this Attributes instance. + * + * @param string $name Name of the attribute + * + * @return Attribute + * + * @throws InvalidArgumentException If the attribute does not yet exist and its name contains special characters + */ + public function offsetGet($name): Attribute + { + return $this->get($name); + } + + /** + * Set the given attribute + * + * If the attribute with the given name already exists, it gets overridden. + * + * @param string $name Name of the attribute + * @param mixed $value Value of the attribute + * + * @throws InvalidArgumentException If the attribute name contains special characters + */ + public function offsetSet($name, $value): void + { + $this->set($name, $value); + } + + /** + * Remove the attribute with the given name + * + * @param string $name Name of the attribute + */ + public function offsetUnset($name): void + { + $this->remove($name); + } + + /** + * Get an iterator for traversing the attributes + * + * @return Attribute[]|ArrayIterator + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->attributes); + } +} diff --git a/vendor/ipl/html/src/BaseHtmlElement.php b/vendor/ipl/html/src/BaseHtmlElement.php new file mode 100644 index 0000000..da8348d --- /dev/null +++ b/vendor/ipl/html/src/BaseHtmlElement.php @@ -0,0 +1,355 @@ +<?php + +namespace ipl\Html; + +use RuntimeException; + +/** + * Base class for HTML elements + * + * Extend this class in order to provide concrete HTML elements or series of HTML elements, e.g. widgets. + * When extending this class you should provide the element's tag with {@link $tag}. Setting default attributes is + * possible via {@link $defaultAttributes}. And the content of the element is provided in {@link assemble()}. + * + * # Example Usage + * ``` + * namespace Acme\Widgets; + * + * use ipl\Html\BaseHtmlElement; + * + * class Dashboard extends BaseHtmlElement + * { + * protected $defaultAttributes = ['class' => 'acme-dashboard']; + * + * protected $tag = 'div'; + * + * protected function assemble() + * { + * // ... + * $this->add($content); + * } + * } + * ``` + */ +abstract class BaseHtmlElement extends HtmlDocument +{ + /** + * List of void elements which must not contain end tags or content + * + * If {@link $isVoid} is null, this property should be used to decide whether the content and end tag has to be + * rendered. + * + * @var array + * + * @see https://www.w3.org/TR/html5/syntax.html#void-elements + */ + protected static $voidElements = [ + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1 + ]; + + /** @var Attributes */ + protected $attributes; + + /** @var bool Whether possible attribute callbacks have been registered */ + protected $attributeCallbacksRegistered = false; + + /** @var bool|null Whether the element is void. If null, void check should use {@link $voidElements} */ + protected $isVoid; + + /** @var array You may want to set default attributes when extending this class */ + protected $defaultAttributes; + + /** @var string Tag of element. Set this property in order to provide the element's tag when extending this class */ + protected $tag; + + /** + * Get the attributes of the element + * + * @return Attributes + */ + public function getAttributes() + { + if ($this->attributes === null) { + $default = $this->getDefaultAttributes(); + if (empty($default)) { + $this->attributes = new Attributes(); + } else { + $this->attributes = Attributes::wantAttributes($default); + } + + $this->ensureAttributeCallbacksRegistered(); + } + + return $this->attributes; + } + + /** + * Set the attributes of the element + * + * @param Attributes|array|null $attributes + * + * @return $this + */ + public function setAttributes($attributes) + { + $this->attributes = Attributes::wantAttributes($attributes); + + $this->attributeCallbacksRegistered = false; + $this->ensureAttributeCallbacksRegistered(); + + return $this; + } + + /** + * Set the attribute with the given name and value + * + * If the attribute with the given name already exists, it gets overridden. + * + * @param string $name The name of the attribute + * @param string|bool|array $value The value of the attribute + * + * @return $this + */ + public function setAttribute($name, $value) + { + $this->getAttributes()->set($name, $value); + + return $this; + } + + /** + * Add the given attributes + * + * @param Attributes|array $attributes + * + * @return $this + */ + public function addAttributes($attributes) + { + $this->getAttributes()->add($attributes); + + return $this; + } + + /** + * Get the default attributes of the element + * + * @return array + */ + public function getDefaultAttributes() + { + return $this->defaultAttributes; + } + + /** + * Get the tag of the element + * + * Since HTML Elements must have a tag, this method throws an exception if the element does not have a tag. + * + * @return string + * + * @throws RuntimeException If the element does not have a tag + */ + final public function getTag() + { + $tag = $this->tag(); + + if (! $tag) { + throw new RuntimeException('Element must have a tag'); + } + + return $tag; + } + + /** + * Set the tag of the element + * + * @param string $tag + * + * @return $this + */ + public function setTag($tag) + { + $this->tag = $tag; + + return $this; + } + + /** + * Get whether the element is void + * + * The default void detection which checks whether the element's tag is in the list of void elements according to + * https://www.w3.org/TR/html5/syntax.html#void-elements. + * + * If you want to override this behavior, use {@link setVoid()}. + * + * @return bool + */ + public function isVoid() + { + if ($this->isVoid !== null) { + return $this->isVoid; + } + + $tag = $this->getTag(); + + return isset(self::$voidElements[$tag]); + } + + /** + * Set whether the element is void + * + * You may use this method to override the default void detection which checks whether the element's tag is in the + * list of void elements according to https://www.w3.org/TR/html5/syntax.html#void-elements. + * + * If you specify null, void detection is reset to its default behavior. + * + * @param bool|null $void + * + * @return $this + */ + public function setVoid($void = true) + { + $this->isVoid = $void === null ?: (bool) $void; + + return $this; + } + + /** + * Ensure that possible attribute callbacks have been registered + * + * Note that this method is automatically called in {@link getAttributes()} and {@link setAttributes()}. + * + * @return $this + */ + public function ensureAttributeCallbacksRegistered() + { + if (! $this->attributeCallbacksRegistered) { + $this->attributeCallbacksRegistered = true; + $this->registerAttributeCallbacks($this->attributes); + } + + return $this; + } + + /** + * Render the content of the element to HTML + * + * @return string + */ + public function renderContent() + { + return parent::renderUnwrapped(); + } + + /** + * Get whether the closing tag should be rendered + * + * @return bool True for void elements, false otherwise + */ + public function wantsClosingTag() + { + // TODO: There is more. SVG and MathML namespaces + return ! $this->isVoid(); + } + + /** + * Use this element to wrap the given document + * + * @param HtmlDocument $document + * + * @return $this + */ + public function wrap(HtmlDocument $document) + { + $document->addWrapper($this); + + return $this; + } + + /** + * Internal method for accessing the tag + * + * You may override this method in order to provide the tag dynamically + * + * @return string + */ + protected function tag() + { + return $this->tag; + } + + /** + * Register attribute callbacks + * + * Override this method in order to register attribute callbacks in concrete classes. + */ + protected function registerAttributeCallbacks(Attributes $attributes) + { + } + + public function addHtml(ValidHtml ...$content) + { + $this->ensureAssembled(); + + parent::addHtml(...$content); + + return $this; + } + + /** + * @inheritdoc + * + * @throws RuntimeException If the element does not have a tag or is void but has content + */ + public function renderUnwrapped() + { + $this->ensureAssembled(); + + $tag = $this->getTag(); + $content = $this->renderContent(); + $attributes = $this->getAttributes()->render(); + + if (strlen($this->contentSeparator)) { + $length = strlen($content); + if ($length > 0) { + if ($content[0] === '<') { + $content = $this->contentSeparator . $content; + $length++; + } + if ($content[$length - 1] === '>') { + $content .= $this->contentSeparator; + } + } + } + + if (! $this->wantsClosingTag()) { + if (strlen($content)) { + throw new RuntimeException('Void elements must not have content'); + } + + return sprintf('<%s%s />', $tag, $attributes); + } + + return sprintf( + '<%s%s>%s</%s>', + $tag, + $attributes, + $content, + $tag + ); + } +} diff --git a/vendor/ipl/html/src/Contract/FormElement.php b/vendor/ipl/html/src/Contract/FormElement.php new file mode 100644 index 0000000..6aa1456 --- /dev/null +++ b/vendor/ipl/html/src/Contract/FormElement.php @@ -0,0 +1,125 @@ +<?php + +namespace ipl\Html\Contract; + +use ipl\Html\Attributes; +use ipl\Html\Form; + +/** + * Representation of form elements + */ +interface FormElement extends Wrappable +{ + /** + * Get the attributes or options of the element + * + * @return Attributes + */ + public function getAttributes(); + + /** + * Add attributes or options to the form element + * + * @param iterable $attributes + * + * @return $this + */ + public function addAttributes($attributes); + + /** + * Get the description for the element, if any + * + * @return string|null + */ + public function getDescription(); + + /** + * Get the label for the element, if any + * + * @return string|null + */ + public function getLabel(); + + /** + * Get the validation error messages + * + * @return array + */ + public function getMessages(); + + /** + * Add a validation error message + * + * @param string $message + * + * @return $this + */ + public function addMessage($message); + + /** + * Get the name of the element + * + * @return string + */ + public function getName(); + + /** + * Get whether the element has a value + * + * @return bool False if the element's value is null, the empty string or the empty array; true otherwise + */ + public function hasValue(); + + /** + * Get the value of the element + * + * @return mixed + */ + public function getValue(); + + /** + * Set the value of the element + * + * @param mixed $value + * + * @return $this + */ + public function setValue($value); + + /** + * Get whether the element has been validated + * + * @return bool + */ + public function hasBeenValidated(); + + /** + * Get whether the element is ignored + * + * @return bool + */ + public function isIgnored(); + + /** + * Get whether the element is required + * + * @return bool + */ + public function isRequired(); + + /** + * Get whether the element is valid + * + * @return bool + */ + public function isValid(); + + /** + * Handler which is called after this element has been registered + * + * @param Form $form + * + * @return void + */ + public function onRegistered(Form $form); +} diff --git a/vendor/ipl/html/src/Contract/FormElementDecorator.php b/vendor/ipl/html/src/Contract/FormElementDecorator.php new file mode 100644 index 0000000..1e862bb --- /dev/null +++ b/vendor/ipl/html/src/Contract/FormElementDecorator.php @@ -0,0 +1,18 @@ +<?php + +namespace ipl\Html\Contract; + +use ipl\Html\ValidHtml; + +/** + * Representation of form element decorators + */ +interface FormElementDecorator extends ValidHtml +{ + /** + * Decorate the given form element + * + * @param FormElement $formElement + */ + public function decorate(FormElement $formElement); +} diff --git a/vendor/ipl/html/src/Contract/FormSubmitElement.php b/vendor/ipl/html/src/Contract/FormSubmitElement.php new file mode 100644 index 0000000..4909df8 --- /dev/null +++ b/vendor/ipl/html/src/Contract/FormSubmitElement.php @@ -0,0 +1,13 @@ +<?php + +namespace ipl\Html\Contract; + +interface FormSubmitElement extends FormElement +{ + /** + * Get whether the element has been pressed + * + * @return bool + */ + public function hasBeenPressed(); +} diff --git a/vendor/ipl/html/src/Contract/ValueCandidates.php b/vendor/ipl/html/src/Contract/ValueCandidates.php new file mode 100644 index 0000000..0271500 --- /dev/null +++ b/vendor/ipl/html/src/Contract/ValueCandidates.php @@ -0,0 +1,22 @@ +<?php + +namespace ipl\Html\Contract; + +interface ValueCandidates +{ + /** + * Get value candidates of this element + * + * @return array + */ + public function getValueCandidates(); + + /** + * Set value candidates of this element + * + * @param array $values + * + * @return $this + */ + public function setValueCandidates(array $values); +} diff --git a/vendor/ipl/html/src/Contract/Wrappable.php b/vendor/ipl/html/src/Contract/Wrappable.php new file mode 100644 index 0000000..c8cf924 --- /dev/null +++ b/vendor/ipl/html/src/Contract/Wrappable.php @@ -0,0 +1,45 @@ +<?php + +namespace ipl\Html\Contract; + +use ipl\Html\ValidHtml; + +/** + * Representation of wrappable elements + */ +interface Wrappable extends ValidHtml +{ + /** + * Get the wrapper, if any + * + * @return Wrappable|null + */ + public function getWrapper(); + + /** + * Set the wrapper + * + * @param Wrappable $wrapper + * + * @return $this + */ + public function setWrapper(Wrappable $wrapper); + + /** + * Add a wrapper + * + * @param Wrappable $wrapper + * + * @return $this + */ + public function addWrapper(Wrappable $wrapper); + + /** + * Prepend a wrapper + * + * @param Wrappable $wrapper + * + * @return $this + */ + public function prependWrapper(Wrappable $wrapper); +} diff --git a/vendor/ipl/html/src/DeferredText.php b/vendor/ipl/html/src/DeferredText.php new file mode 100644 index 0000000..2d308f1 --- /dev/null +++ b/vendor/ipl/html/src/DeferredText.php @@ -0,0 +1,114 @@ +<?php + +namespace ipl\Html; + +use Exception; + +/** + * Text node where content creation is deferred until rendering + * + * The content is created by a passed in callback which is only called when the node is going to be rendered and + * automatically escaped to HTML. + * If the created content is already escaped, see {@link setEscaped()} to indicate this. + * + * # Example Usage + * ``` + * $benchmark = new Benchmark(); + * + * $performance = new DeferredText(function () use ($benchmark) { + * return $benchmark->summary(); + * }); + * + * execute_query(); + * + * $benchmark->tick('Fetched results'); + * + * generate_report(); + * + * $benchmark->tick('Report generated'); + * + * echo $performance; + * ``` + */ +class DeferredText implements ValidHtml +{ + /** @var callable will return the text that should be rendered */ + protected $callback; + + /** @var bool */ + protected $escaped = false; + + /** + * Create a new text node where content creation is deferred until rendering + * + * @param callable $callback Must return the content that should be rendered + */ + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + /** + * Create a new text node where content creation is deferred until rendering + * + * @param callable $callback Must return the content that should be rendered + * + * @return static + */ + public static function create(callable $callback) + { + return new static($callback); + } + + /** + * Get whether the callback promises that its content is already escaped + * + * @return bool + */ + public function isEscaped() + { + return $this->escaped; + } + + /** + * Set whether the callback's content is already escaped + * + * @param bool $escaped + * + * @return $this + */ + public function setEscaped($escaped = true) + { + $this->escaped = $escaped; + + return $this; + } + + /** + * Render text to HTML when treated like a string + * + * Calls {@link render()} internally in order to render the text to HTML. + * Exceptions will be automatically caught and returned as HTML string as well using {@link Error::render()}. + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return Error::render($e); + } + } + + public function render() + { + $callback = $this->callback; + + if ($this->escaped) { + return $callback(); + } else { + return Html::escape($callback()); + } + } +} diff --git a/vendor/ipl/html/src/Error.php b/vendor/ipl/html/src/Error.php new file mode 100644 index 0000000..a4ab253 --- /dev/null +++ b/vendor/ipl/html/src/Error.php @@ -0,0 +1,117 @@ +<?php + +namespace ipl\Html; + +use Exception; +use Throwable; + +use function ipl\Stdlib\get_php_type; + +/** + * Class Error + * + * TODO: Eventually allow to (statically) inject a custom error renderer. + * + * @package ipl\Html + */ +abstract class Error +{ + /** @var bool */ + protected static $showTraces = true; + + /** + * + * @param Exception|Throwable|string $error + * @return HtmlDocument + */ + public static function show($error) + { + if ($error instanceof Throwable) { + // PHP 7+ + $msg = static::createMessageForException($error); + } elseif ($error instanceof Exception) { + // PHP 5.x + $msg = static::createMessageForException($error); + } elseif (is_string($error)) { + $msg = $error; + } else { + // TODO: translate? + $msg = 'Got an invalid error'; + } + + $result = static::renderErrorMessage($msg); + if (static::showTraces()) { + $result->addHtml(Html::tag('pre', $error->getTraceAsString())); + } + + return $result; + } + + /** + * + * @param Exception|Throwable|string $error + * @return string + */ + public static function render($error) + { + return static::show($error)->render(); + } + + /** + * @param bool|null $show + * @return bool + */ + public static function showTraces($show = null) + { + if ($show !== null) { + self::$showTraces = (bool) $show; + } + + return self::$showTraces; + } + + /** + * @deprecated Use {@link get_php_type()} instead + */ + public static function getPhpTypeName($any) + { + return get_php_type($any); + } + + /** + * @param Exception|Throwable $exception + * @return string + */ + protected static function createMessageForException($exception) + { + $file = preg_split('/[\/\\\]/', $exception->getFile(), -1, PREG_SPLIT_NO_EMPTY); + $file = array_pop($file); + return sprintf( + '%s (%s:%d)', + $exception->getMessage(), + $file, + $exception->getLine() + ); + } + + /** + * @param string + * @return HtmlDocument + */ + protected static function renderErrorMessage($message) + { + $output = new HtmlDocument(); + $output->addHtml( + Html::tag('div', ['class' => 'exception'], [ + Html::tag('h1', [ + Html::tag('i', ['class' => 'icon-bug']), + // TODO: Translate? More hints? + 'Oops, an error occurred!' + ]), + Html::tag('pre', $message) + ]) + ); + + return $output; + } +} diff --git a/vendor/ipl/html/src/Form.php b/vendor/ipl/html/src/Form.php new file mode 100644 index 0000000..e301af1 --- /dev/null +++ b/vendor/ipl/html/src/Form.php @@ -0,0 +1,384 @@ +<?php + +namespace ipl\Html; + +use Exception; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\FormElement\FormElements; +use ipl\Stdlib\Messages; +use Psr\Http\Message\ServerRequestInterface; + +class Form extends BaseHtmlElement +{ + use FormElements { + FormElements::remove as private removeElement; + } + use Messages; + + const ON_ELEMENT_REGISTERED = 'elementRegistered'; + const ON_ERROR = 'error'; + const ON_REQUEST = 'request'; + const ON_SUCCESS = 'success'; + const ON_SENT = 'sent'; + const ON_VALIDATE = 'validate'; + + /** @var string Form submission URL */ + protected $action; + + /** @var string HTTP method to submit the form with */ + protected $method = 'POST'; + + /** @var FormSubmitElement Primary submit button */ + protected $submitButton; + + /** @var FormSubmitElement[] Other elements that may submit the form */ + protected $submitElements = []; + + /** @var bool Whether the form is valid */ + protected $isValid; + + /** @var ServerRequestInterface The server request being processed */ + protected $request; + + /** @var string */ + protected $redirectUrl; + + protected $tag = 'form'; + + /** + * Get the Form submission URL + * + * @return string|null + */ + public function getAction() + { + return $this->action; + } + + /** + * Set the Form submission URL + * + * @param string $action + * + * @return $this + */ + public function setAction($action) + { + $this->action = $action; + + return $this; + } + + /** + * Get the HTTP method to submit the form with + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Set the HTTP method to submit the form with + * + * @param string $method + * + * @return $this + */ + public function setMethod($method) + { + $this->method = strtoupper($method); + + return $this; + } + + /** + * Get whether the form has a primary submit button + * + * @return bool + */ + public function hasSubmitButton() + { + return $this->submitButton !== null; + } + + /** + * Get the primary submit button + * + * @return FormSubmitElement|null + */ + public function getSubmitButton() + { + return $this->submitButton; + } + + /** + * Set the primary submit button + * + * @param FormSubmitElement $element + * + * @return $this + */ + public function setSubmitButton(FormSubmitElement $element) + { + $this->submitButton = $element; + + return $this; + } + + /** + * Get the submit element used to send the form + * + * @return FormSubmitElement|null + */ + public function getPressedSubmitElement() + { + foreach ($this->submitElements as $submitElement) { + if ($submitElement->hasBeenPressed()) { + return $submitElement; + } + } + + return null; + } + + /** + * @return ServerRequestInterface|null + */ + public function getRequest() + { + return $this->request; + } + + public function setRequest($request) + { + $this->request = $request; + $this->emit(Form::ON_REQUEST, [$request]); + + return $this; + } + + /** + * Get the url to redirect to on success + * + * @return string + */ + public function getRedirectUrl() + { + return $this->redirectUrl; + } + + /** + * Set the url to redirect to on success + * + * @param string $url + * + * @return $this + */ + public function setRedirectUrl($url) + { + $this->redirectUrl = $url; + + return $this; + } + + /** + * @param ServerRequestInterface $request + * @return $this + */ + public function handleRequest(ServerRequestInterface $request) + { + $this->setRequest($request); + + if (! $this->hasBeenSent()) { + // Always assemble + $this->ensureAssembled(); + + return $this; + } + + switch ($request->getMethod()) { + case 'POST': + $params = $request->getParsedBody(); + + break; + case 'GET': + parse_str($request->getUri()->getQuery(), $params); + + break; + default: + $params = []; + } + + $this->populate($params); + + // Assemble after populate in order to conditionally provide form elements + $this->ensureAssembled(); + + if ($this->hasBeenSubmitted()) { + if ($this->isValid()) { + try { + $this->emit(Form::ON_SENT, [$this]); + $this->onSuccess(); + $this->emitOnce(Form::ON_SUCCESS, [$this]); + } catch (Exception $e) { + $this->addMessage($e); + $this->onError(); + $this->emit(Form::ON_ERROR, [$e, $this]); + } + } else { + $this->onError(); + } + } else { + $this->validatePartial(); + $this->emit(Form::ON_SENT, [$this]); + } + + return $this; + } + + /** + * Get whether the form has been sent + * + * A form is considered sent if the request's method equals the form's method. + * + * @return bool + */ + public function hasBeenSent() + { + if ($this->request === null) { + return false; + } + + return $this->request->getMethod() === $this->getMethod(); + } + + /** + * Get whether the form has been submitted + * + * A form is submitted when it has been sent and when the primary submit button, if set, has been pressed. + * This method calls {@link hasBeenSent()} in order to detect whether the form has been sent. + * + * @return bool + */ + public function hasBeenSubmitted() + { + if (! $this->hasBeenSent()) { + return false; + } + + if ($this->hasSubmitButton()) { + return $this->getSubmitButton()->hasBeenPressed(); + } + + return true; + } + + /** + * Get whether the form is valid + * + * {@link validate()} is called automatically if the form has not been validated before. + * + * @return bool + */ + public function isValid() + { + if ($this->isValid === null) { + $this->validate(); + + $this->emit(self::ON_VALIDATE, [$this]); + } + + return $this->isValid; + } + + /** + * Validate all elements + * + * @return $this + */ + public function validate() + { + $valid = true; + foreach ($this->elements as $element) { + if ($element->isRequired() && ! $element->hasValue()) { + $element->addMessage('This field is required'); + $valid = false; + continue; + } + if (! $element->isValid()) { + $valid = false; + } + } + + $this->isValid = $valid; + + return $this; + } + + /** + * Validate all elements that have a value + * + * @return $this + */ + public function validatePartial() + { + foreach ($this->getElements() as $element) { + $element->validate(); + } + + return $this; + } + + public function remove(ValidHtml $elementOrHtml) + { + if ($this->submitButton === $elementOrHtml) { + $this->submitButton = null; + } + + $this->removeElement($elementOrHtml); + } + + protected function onError() + { + $errors = Html::tag('ul', ['class' => 'errors']); + foreach ($this->getMessages() as $message) { + if ($message instanceof Exception) { + $message = $message->getMessage(); + } + + $errors->addHtml(Html::tag('li', $message)); + } + + if (! $errors->isEmpty()) { + $this->prependHtml($errors); + } + } + + protected function onSuccess() + { + // $this->redirectOnSuccess(); + } + + protected function onElementRegistered(FormElement $element) + { + if ($element instanceof FormSubmitElement) { + $this->submitElements[$element->getName()] = $element; + + if (! $this->hasSubmitButton()) { + $this->setSubmitButton($element); + } + } + + $element->onRegistered($this); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + $attributes + ->registerAttributeCallback('action', [$this, 'getAction'], [$this, 'setAction']) + ->registerAttributeCallback('method', [$this, 'getMethod'], [$this, 'setMethod']); + } +} diff --git a/vendor/ipl/html/src/FormDecorator/CallbackDecorator.php b/vendor/ipl/html/src/FormDecorator/CallbackDecorator.php new file mode 100644 index 0000000..1048bfb --- /dev/null +++ b/vendor/ipl/html/src/FormDecorator/CallbackDecorator.php @@ -0,0 +1,41 @@ +<?php + +namespace ipl\Html\FormDecorator; + +use Closure; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\FormElementDecorator; +use ipl\Html\HtmlDocument; + +class CallbackDecorator extends HtmlDocument implements FormElementDecorator +{ + /** @var Closure The decorating callback */ + protected $callback; + + /** @var FormElement The decorated form element */ + protected $formElement; + + /** + * Create a new CallbackDecorator + * + * @param Closure $callback + */ + public function __construct(Closure $callback) + { + $this->callback = $callback; + } + + public function decorate(FormElement $formElement) + { + $decorator = clone $this; + + $decorator->formElement = $formElement; + + $formElement->prependWrapper($decorator); + } + + protected function assemble() + { + call_user_func($this->callback, $this->formElement, $this); + } +} diff --git a/vendor/ipl/html/src/FormDecorator/DdDtDecorator.php b/vendor/ipl/html/src/FormDecorator/DdDtDecorator.php new file mode 100644 index 0000000..ae0f8e6 --- /dev/null +++ b/vendor/ipl/html/src/FormDecorator/DdDtDecorator.php @@ -0,0 +1,141 @@ +<?php + +namespace ipl\Html\FormDecorator; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\Html; +use ipl\Html\ValidHtml; + +class DdDtDecorator extends BaseHtmlElement implements DecoratorInterface +{ + protected $tag = 'dl'; + + protected $dt; + + protected $dd; + + /** @var BaseFormElement */ + protected $wrappedElement; + + protected $ready = false; + + /** + * @param BaseFormElement $element + * @return static + */ + public function decorate(BaseFormElement $element) + { + // TODO: ignore hidden? + $newWrapper = clone($this); + $newWrapper->wrappedElement = $element; + $element->prependWrapper($newWrapper); + + return $newWrapper; + } + + protected function renderLabel() + { + if ($this->wrappedElement instanceof BaseFormElement) { + $label = $this->wrappedElement->getLabel(); + if ($label) { + return Html::tag('label', null, $label); + } + } + + return null; + } + + public function getAttributes() + { + $attributes = parent::getAttributes(); + + // TODO: only when sent?! + if ($this->wrappedElement->hasBeenValidatedAndIsNotValid()) { + $classes = $attributes->get('class'); + if ( + empty($classes) + || (is_array($classes) && ! in_array('errors', $classes)) + || (is_string($classes) && $classes !== 'errors') + ) { + $attributes->add('class', 'errors'); + } + } + + return $attributes; + } + + protected function renderDescription() + { + if ($this->wrappedElement instanceof BaseFormElement) { + $description = $this->wrappedElement->getDescription(); + if ($description) { + return Html::tag('p', ['class' => 'description'], $description); + } + } + + return null; + } + + protected function renderErrors() + { + if ($this->wrappedElement instanceof BaseFormElement) { + $errors = []; + foreach ($this->wrappedElement->getMessages() as $message) { + $errors[] = Html::tag('p', ['class' => 'error'], $message); + } + + if (! empty($errors)) { + return $errors; + } + } + + return null; + } + + public function addHtml(ValidHtml ...$content) + { + // TODO: is this required? + if (! in_array($this->wrappedElement, $content, true)) { + parent::addHtml(...$content); + } + + return $this; + } + + protected function assemble() + { + $this->addHtml($this->dt(), $this->dd()); + $this->ready = true; + } + + protected function dt() + { + if ($this->dt === null) { + $this->dt = Html::tag('dt', null, $this->renderLabel()); + } + + return $this->dt; + } + + /** + * @return \ipl\Html\HtmlElement + */ + protected function dd() + { + if ($this->dd === null) { + $this->dd = Html::tag('dd', null, [ + $this->wrappedElement, + $this->renderErrors(), + $this->renderDescription() + ]); + } + + return $this->dd; + } + + public function __destruct() + { + $this->wrapper = null; + } +} diff --git a/vendor/ipl/html/src/FormDecorator/DecoratorInterface.php b/vendor/ipl/html/src/FormDecorator/DecoratorInterface.php new file mode 100644 index 0000000..1ed1a38 --- /dev/null +++ b/vendor/ipl/html/src/FormDecorator/DecoratorInterface.php @@ -0,0 +1,19 @@ +<?php + +namespace ipl\Html\FormDecorator; + +use ipl\Html\Contract\FormElementDecorator; +use ipl\Html\FormElement\BaseFormElement; + +/** @deprecated Use {@link FormElementDecorator} instead */ +interface DecoratorInterface +{ + /** + * Set the form element to decorate + * + * @param BaseFormElement $formElement + * + * @return static + */ + public function decorate(BaseFormElement $formElement); +} diff --git a/vendor/ipl/html/src/FormDecorator/DivDecorator.php b/vendor/ipl/html/src/FormDecorator/DivDecorator.php new file mode 100644 index 0000000..574bc26 --- /dev/null +++ b/vendor/ipl/html/src/FormDecorator/DivDecorator.php @@ -0,0 +1,132 @@ +<?php + +namespace ipl\Html\FormDecorator; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\FormElementDecorator; +use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\FormElement\HiddenElement; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\Text; + +/** + * Form element decorator based on div elements + */ +class DivDecorator extends BaseHtmlElement implements FormElementDecorator +{ + /** @var string CSS class to use for submit elements */ + const SUBMIT_ELEMENT_CLASS = 'form-control'; + + /** @var string CSS class to use for all input elements */ + const INPUT_ELEMENT_CLASS = 'form-element'; + + /** @var string CSS class to use for form descriptions */ + const DESCRIPTION_CLASS = 'form-element-description'; + + /** @var string CSS class to use for form errors */ + const ERROR_CLASS = 'form-element-errors'; + + /** @var string CSS class to set on the decorator if the element has errors */ + const ERROR_HINT_CLASS = 'has-error'; + + /** @var FormElement The decorated form element */ + protected $formElement; + + protected $tag = 'div'; + + public function decorate(FormElement $formElement) + { + if ($formElement instanceof HiddenElement) { + return; + } + + $decorator = clone $this; + + $decorator->formElement = $formElement; + + $classes = [static::INPUT_ELEMENT_CLASS]; + if ($formElement instanceof FormSubmitElement) { + $classes[] = static::SUBMIT_ELEMENT_CLASS; + } + + $decorator->getAttributes()->add('class', $classes); + + $formElement->prependWrapper($decorator); + } + + protected function assembleDescription() + { + $description = $this->formElement->getDescription(); + + if ($description !== null) { + $descriptionId = null; + if ($this->formElement->getAttributes()->has('id')) { + $descriptionId = 'desc_' . $this->formElement->getAttributes()->get('id')->getValue(); + $this->formElement->getAttributes()->set('aria-describedby', $descriptionId); + } + + return Html::tag('p', ['id' => $descriptionId, 'class' => static::DESCRIPTION_CLASS], $description); + } + + return null; + } + + protected function assembleElement() + { + if ($this->formElement->isRequired()) { + $this->formElement->getAttributes()->set('aria-required', 'true'); + } + + return $this->formElement; + } + + protected function assembleErrors() + { + $errors = new HtmlElement('ul', Attributes::create(['class' => static::ERROR_CLASS])); + + foreach ($this->formElement->getMessages() as $message) { + $errors->addHtml( + new HtmlElement('li', Attributes::create(['class' => static::ERROR_CLASS]), Text::create($message)) + ); + } + + if (! $errors->isEmpty()) { + return $errors; + } + + return null; + } + + protected function assembleLabel() + { + $label = $this->formElement->getLabel(); + + if ($label !== null) { + $attributes = null; + if ($this->formElement->getAttributes()->has('id')) { + $attributes = new Attributes(['for' => $this->formElement->getAttributes()->get('id')->getValue()]); + } + + return Html::tag('label', $attributes, $label); + } + + return null; + } + + protected function assemble() + { + if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) { + $this->getAttributes()->add('class', static::ERROR_HINT_CLASS); + } + + $this->addHtml(...Html::wantHtmlList([ + $this->assembleLabel(), + $this->assembleElement(), + $this->assembleDescription(), + $this->assembleErrors() + ])); + } +} diff --git a/vendor/ipl/html/src/FormElement/BaseFormElement.php b/vendor/ipl/html/src/FormElement/BaseFormElement.php new file mode 100644 index 0000000..c75a636 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/BaseFormElement.php @@ -0,0 +1,348 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attribute; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\ValueCandidates; +use ipl\Html\Form; +use ipl\Stdlib\Messages; +use ipl\Validator\ValidatorChain; + +abstract class BaseFormElement extends BaseHtmlElement implements FormElement, ValueCandidates +{ + use Messages; + + /** @var string Description of the element */ + protected $description; + + /** @var string Label of the element */ + protected $label; + + /** @var string Name of the element */ + protected $name; + + /** @var bool Whether the element is ignored */ + protected $ignored = false; + + /** @var bool Whether the element is required */ + protected $required = false; + + /** @var null|bool Whether the element is valid; null if the element has not been validated yet, bool otherwise */ + protected $valid; + + /** @var ValidatorChain Registered validators */ + protected $validators; + + /** @var mixed Value of the element */ + protected $value; + + /** @var array Value candidates of the element */ + protected $valueCandidates = []; + + /** + * Create a new form element + * + * @param string $name Name of the form element + * @param mixed $attributes Attributes of the form element + */ + public function __construct($name, $attributes = null) + { + if ($attributes !== null) { + $this->addAttributes($attributes); + } + $this->setName($name); + } + + public function getDescription() + { + return $this->description; + } + + /** + * Set the description of the element + * + * @param string $description + * + * @return $this + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + public function getLabel() + { + return $this->label; + } + + /** + * Set the label of the element + * + * @param string $label + * + * @return $this + */ + public function setLabel($label) + { + $this->label = $label; + + return $this; + } + + public function getName() + { + return $this->name; + } + + /** + * Set the name for the element + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + public function isIgnored() + { + return $this->ignored; + } + + /** + * Set whether the element is ignored + * + * @param bool $ignored + * + * @return $this + */ + public function setIgnored($ignored = true) + { + $this->ignored = (bool) $ignored; + + return $this; + } + + public function isRequired() + { + return $this->required; + } + + /** + * Set whether the element is required + * + * @param bool $required + * + * @return $this + */ + public function setRequired($required = true) + { + $this->required = (bool) $required; + + return $this; + } + + public function isValid() + { + if ($this->valid === null) { + $this->validate(); + } + + return $this->valid; + } + + /** + * Get whether the element has been validated and is not valid + * + * @return bool + * + * @deprecated Use {@link hasBeenValidated()} in combination with {@link isValid()} instead + */ + public function hasBeenValidatedAndIsNotValid() + { + return $this->valid !== null && ! $this->valid; + } + + /** + * Get the validators + * + * @return ValidatorChain + */ + public function getValidators() + { + if ($this->validators === null) { + $this->validators = new ValidatorChain(); + $this->addDefaultValidators(); + } + + return $this->validators; + } + + /** + * Add default validators + */ + public function addDefaultValidators() + { + } + + /** + * Set the validators + * + * @param iterable $validators + * + * @return $this + */ + public function setValidators($validators) + { + $this + ->getValidators() + ->clearValidators() + ->addValidators($validators); + + return $this; + } + + /** + * Add validators + * + * @param iterable $validators + * + * @return $this + */ + public function addValidators($validators) + { + $this->getValidators()->addValidators($validators); + + return $this; + } + + public function hasValue() + { + $value = $this->getValue(); + + return $value !== null && $value !== '' && $value !== []; + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + if ($value === '') { + $this->value = null; + } else { + $this->value = $value; + } + $this->valid = null; + + return $this; + } + + public function getValueCandidates() + { + return $this->valueCandidates; + } + + public function setValueCandidates(array $values) + { + $this->valueCandidates = $values; + + return $this; + } + + public function onRegistered(Form $form) + { + } + + /** + * Validate the element using all registered validators + * + * @return $this + */ + public function validate() + { + $this->valid = $this->getValidators()->isValid($this->getValue()); + $this->addMessages($this->getValidators()->getMessages()); + + return $this; + } + + public function hasBeenValidated() + { + return $this->valid !== null; + } + + /** + * Callback for the name attribute + * + * @return Attribute|string + */ + public function getNameAttribute() + { + return $this->getName(); + } + + /** + * Callback for the required attribute + * + * @return Attribute|null + */ + public function getRequiredAttribute() + { + if ($this->isRequired()) { + return new Attribute('required', true); + } + + return null; + } + + /** + * Callback for the value attribute + * + * @return mixed + */ + public function getValueAttribute() + { + return $this->getValue(); + } + + protected function registerValueCallback(Attributes $attributes) + { + $attributes->registerAttributeCallback( + 'value', + [$this, 'getValueAttribute'], + [$this, 'setValue'] + ); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + $this->registerValueCallback($attributes); + + $attributes + ->registerAttributeCallback('label', null, [$this, 'setLabel']) + ->registerAttributeCallback('name', [$this, 'getNameAttribute'], [$this, 'setName']) + ->registerAttributeCallback('description', null, [$this, 'setDescription']) + ->registerAttributeCallback('validators', null, [$this, 'setValidators']) + ->registerAttributeCallback('ignore', null, [$this, 'setIgnored']) + ->registerAttributeCallback('required', [$this, 'getRequiredAttribute'], [$this, 'setRequired']); + + $this->registerCallbacks(); + } + + /** @deprecated Use {@link registerAttributeCallbacks()} instead */ + protected function registerCallbacks() + { + } +} diff --git a/vendor/ipl/html/src/FormElement/ButtonElement.php b/vendor/ipl/html/src/FormElement/ButtonElement.php new file mode 100644 index 0000000..63ae540 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/ButtonElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class ButtonElement extends BaseFormElement +{ + protected $tag = 'button'; +} diff --git a/vendor/ipl/html/src/FormElement/CheckboxElement.php b/vendor/ipl/html/src/FormElement/CheckboxElement.php new file mode 100644 index 0000000..5e16fc2 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/CheckboxElement.php @@ -0,0 +1,125 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; + +class CheckboxElement extends InputElement +{ + /** @var bool Whether the checkbox is checked */ + protected $checked = false; + + /** @var string Value of the checkbox when it is checked */ + protected $checkedValue = 'y'; + + /** @var string Value of the checkbox when it is not checked */ + protected $uncheckedValue = 'n'; + + protected $type = 'checkbox'; + + /** + * Get whether the checkbox is checked + * + * @return bool + */ + public function isChecked() + { + return $this->checked; + } + + /** + * Set whether the checkbox is checked + * + * @param bool $checked + * + * @return $this + */ + public function setChecked($checked) + { + $this->checked = (bool) $checked; + + return $this; + } + + /** + * Get the value of the checkbox when it is checked + * + * @return string + */ + public function getCheckedValue() + { + return $this->checkedValue; + } + + /** + * Set the value of the checkbox when it is checked + * + * @param string $checkedValue + * + * @return $this + */ + public function setCheckedValue($checkedValue) + { + $this->checkedValue = $checkedValue; + + return $this; + } + + /** + * Get the value of the checkbox when it is not checked + * + * @return string + */ + public function getUncheckedValue() + { + return $this->uncheckedValue; + } + + /** + * Set the value of the checkbox when it is not checked + * + * @param string $uncheckedValue + * + * @return $this + */ + public function setUncheckedValue($uncheckedValue) + { + $this->uncheckedValue = $uncheckedValue; + + return $this; + } + + public function setValue($value) + { + if (is_bool($value)) { + $value = $value ? $this->getCheckedValue() : $this->getUncheckedValue(); + } + + $this->setChecked($value === $this->getCheckedValue()); + + return parent::setValue($value); + } + + public function getValueAttribute() + { + return $this->getCheckedValue(); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback( + 'checked', + [$this, 'isChecked'], + [$this, 'setChecked'] + ); + } + + public function renderUnwrapped() + { + $html = parent::renderUnwrapped(); + + return (new HiddenElement($this->getName(), ['value' => $this->getUncheckedValue()])) . $html; + } +} diff --git a/vendor/ipl/html/src/FormElement/DateElement.php b/vendor/ipl/html/src/FormElement/DateElement.php new file mode 100644 index 0000000..2f73b3c --- /dev/null +++ b/vendor/ipl/html/src/FormElement/DateElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class DateElement extends InputElement +{ + protected $type = 'date'; +} diff --git a/vendor/ipl/html/src/FormElement/FormElements.php b/vendor/ipl/html/src/FormElement/FormElements.php new file mode 100644 index 0000000..d8bb784 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/FormElements.php @@ -0,0 +1,502 @@ +<?php + +namespace ipl\Html\FormElement; + +use InvalidArgumentException; +use ipl\Html\Contract\FormElement; +use ipl\Html\Contract\FormElementDecorator; +use ipl\Html\Contract\ValueCandidates; +use ipl\Html\Form; +use ipl\Html\FormDecorator\DecoratorInterface; +use ipl\Html\ValidHtml; +use ipl\Stdlib\Events; +use ipl\Stdlib\Plugins; +use UnexpectedValueException; + +use function ipl\Stdlib\get_php_type; + +trait FormElements +{ + use Events; + use Plugins; + + /** @var FormElementDecorator|null */ + private $defaultElementDecorator; + + /** @var bool Whether the default element decorator loader has been registered */ + private $defaultElementDecoratorLoaderRegistered = false; + + /** @var bool Whether the default element loader has been registered */ + private $defaultElementLoaderRegistered = false; + + /** @var FormElement[] */ + private $elements = []; + + /** @var array */ + private $populatedValues = []; + + /** + * Get all elements + * + * @return FormElement[] + */ + public function getElements() + { + return $this->elements; + } + + /** + * Get whether the given element exists + * + * @param string|FormElement $element + * + * @return bool + */ + public function hasElement($element) + { + if (is_string($element)) { + return array_key_exists($element, $this->elements); + } + + if ($element instanceof FormElement) { + return in_array($element, $this->elements, true); + } + + return false; + } + + /** + * Get the element by the given name + * + * @param string $name + * + * @return FormElement + * + * @throws InvalidArgumentException If no element with the given name exists + */ + public function getElement($name) + { + if (! array_key_exists($name, $this->elements)) { + throw new InvalidArgumentException(sprintf( + "Can't get element '%s'. Element does not exist", + $name + )); + } + + return $this->elements[$name]; + } + + /** + * Add an element + * + * @param string|FormElement $typeOrElement Type of the element as string or an instance of FormElement + * @param string $name Name of the element + * @param mixed $options Element options as key-value pairs + * + * @return $this + * + * @throws InvalidArgumentException If $typeOrElement is neither a string nor an instance of FormElement + * or if $typeOrElement is a string and $name is not set + * or if $typeOrElement is a string but type is unknown + * or if $typeOrElement is an instance of FormElement but does not have a name + */ + public function addElement($typeOrElement, $name = null, $options = null) + { + if (is_string($typeOrElement)) { + if ($name === null) { + throw new InvalidArgumentException(sprintf( + '%s expects parameter 2 to be set if parameter 1 is a string', + __METHOD__ + )); + } + + $element = $this->createElement($typeOrElement, $name, $options); + } elseif ($typeOrElement instanceof FormElement) { + $element = $typeOrElement; + } else { + throw new InvalidArgumentException(sprintf( + '%s() expects parameter 1 to be a string or an instance of %s, %s given', + __METHOD__, + FormElement::class, + get_php_type($typeOrElement) + )); + } + + $this + ->registerElement($element) // registerElement() must be called first because of the name check + ->decorate($element) + ->addHtml($element); + + return $this; + } + + /** + * Create an element + * + * @param string $type Type of the element + * @param string $name Name of the element + * @param mixed $options Element options as key-value pairs + * + * @return FormElement + * + * @throws InvalidArgumentException If the type of the element is unknown + */ + public function createElement($type, $name, $options = null) + { + $this->ensureDefaultElementLoaderRegistered(); + + $class = $this->loadPlugin('element', $type); + + if (! $class) { + throw new InvalidArgumentException(sprintf( + "Can't create element of unknown type '%s", + $type + )); + } + + /** @var FormElement $element */ + $element = new $class($name); + + if ($options !== null) { + $element->addAttributes($options); + } + + return $element; + } + + /** + * Register an element + * + * Registers the element for value and validation handling but does not add it to the render stack. + * + * @param FormElement $element + * + * @return $this + * + * @throws InvalidArgumentException If $element does not provide a name + */ + public function registerElement(FormElement $element) + { + $name = $element->getName(); + + if ($name === null) { + throw new InvalidArgumentException(sprintf( + '%s expects the element to provide a name', + __METHOD__ + )); + } + + $this->elements[$name] = $element; + + if (array_key_exists($name, $this->populatedValues)) { + $element->setValue($this->populatedValues[$name][count($this->populatedValues[$name]) - 1]); + + if ($element instanceof ValueCandidates) { + $element->setValueCandidates($this->populatedValues[$name]); + } + } + + $this->onElementRegistered($element); + $this->emit(Form::ON_ELEMENT_REGISTERED, [$element]); + + return $this; + } + + /** + * Get whether a default element decorator exists + * + * @return bool + */ + public function hasDefaultElementDecorator() + { + return $this->defaultElementDecorator !== null; + } + + /** + * Get the default element decorator, if any + * + * @return FormElementDecorator|null + */ + public function getDefaultElementDecorator() + { + return $this->defaultElementDecorator; + } + + /** + * Set the default element decorator + * + * If $decorator is a string, the decorator will be automatically created from a registered decorator loader. + * A loader for the namespace ipl\\Html\\FormDecorator is automatically registered by default. + * See {@link addDecoratorLoader()} for registering a custom loader. + * + * @param FormElementDecorator|string $decorator + * + * @return $this + * + * @throws InvalidArgumentException If $decorator is a string and can't be loaded from registered decorator loaders + * or if a decorator loader does not return an instance of + * {@link FormElementDecorator} + */ + public function setDefaultElementDecorator($decorator) + { + if ($decorator instanceof FormElementDecorator || $decorator instanceof DecoratorInterface) { + $this->defaultElementDecorator = $decorator; + } else { + $this->ensureDefaultElementDecoratorLoaderRegistered(); + + $d = $this->loadPlugin('decorator', $decorator); + + if (! $d instanceof FormElementDecorator && ! $d instanceof DecoratorInterface) { + throw new InvalidArgumentException(sprintf( + "Expected instance of %s for decorator '%s'," + . " got %s from a decorator loader instead", + FormElementDecorator::class, + $decorator, + get_php_type($d) + )); + } + + $this->defaultElementDecorator = $d; + } + + return $this; + } + + /** + * Get the value of the element specified by name + * + * Returns $default if the element does not exist or has no value. + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getValue($name, $default = null) + { + if ($this->hasElement($name)) { + $value = $this->getElement($name)->getValue(); + if ($value !== null) { + return $value; + } + } + + return $default; + } + + /** + * Get the values for all but ignored elements + * + * @return array Values as name-value pairs + */ + public function getValues() + { + $values = []; + foreach ($this->getElements() as $element) { + if (! $element->isIgnored()) { + $values[$element->getName()] = $element->getValue(); + } + } + + return $values; + } + + /** + * Populate values of registered elements + * + * @param iterable $values Values as name-value pairs + * + * @return $this + */ + public function populate($values) + { + foreach ($values as $name => $value) { + $this->populatedValues[$name][] = $value; + if ($this->hasElement($name)) { + $this->getElement($name)->setValue($value); + } + } + + return $this; + } + + /** + * Get the populated value of the element specified by name + * + * Returns $default if there is no populated value for this element. + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getPopulatedValue($name, $default = null) + { + return isset($this->populatedValues[$name]) + ? $this->populatedValues[$name][count($this->populatedValues[$name]) - 1] + : $default; + } + + /** + * Clear populated value of the given element + * + * @param string $name + * + * @return $this + */ + public function clearPopulatedValue($name) + { + if (! $this->hasBeenSubmitted() && isset($this->populatedValues[$name])) { + unset($this->populatedValues[$name]); + } + + return $this; + } + + /** + * Add all elements from the given element collection + * + * @param Form|SubFormElement $form + * + * @return $this + */ + public function addElementsFrom($form) + { + foreach ($form->getElements() as $element) { + $this->addElement($element); + } + + return $this; + } + + /** + * Add a decorator loader + * + * @param string $namespace Namespace of the decorators + * @param string $postfix Decorator name postfix, if any + * + * @return $this + */ + public function addDecoratorLoader($namespace, $postfix = null) + { + $this->addPluginLoader('decorator', $namespace, $postfix); + + return $this; + } + + /** + * Add an element loader + * + * @param string $namespace Namespace of the elements + * @param string $postfix Element name postfix, if any + * + * @return $this + */ + public function addElementLoader($namespace, $postfix = null) + { + $this->addPluginLoader('element', $namespace, $postfix); + + return $this; + } + + /** + * Ensure that our default element decorator loader is registered + * + * @return $this + */ + protected function ensureDefaultElementDecoratorLoaderRegistered() + { + if (! $this->defaultElementDecoratorLoaderRegistered) { + $this->addDefaultPluginLoader( + 'decorator', + 'ipl\\Html\\FormDecorator', + 'Decorator' + ); + + $this->defaultElementDecoratorLoaderRegistered = true; + } + + return $this; + } + + /** + * Ensure that our default element loader is registered + * + * @return $this + */ + protected function ensureDefaultElementLoaderRegistered() + { + if (! $this->defaultElementLoaderRegistered) { + $this->addDefaultPluginLoader('element', __NAMESPACE__, 'Element'); + + $this->defaultElementLoaderRegistered = true; + } + + return $this; + } + + /** + * Decorate the given element + * + * @param FormElement $element + * + * @return $this + * + * @throws UnexpectedValueException If the default decorator is set but not an instance of + * {@link FormElementDecorator} + */ + protected function decorate(FormElement $element) + { + if ($this->hasDefaultElementDecorator()) { + $decorator = $this->getDefaultElementDecorator(); + + if (! $decorator instanceof FormElementDecorator && ! $decorator instanceof DecoratorInterface) { + throw new UnexpectedValueException(sprintf( + '%s expects the default decorator to be an instance of %s, got %s instead', + __METHOD__, + FormElementDecorator::class, + get_php_type($decorator) + )); + } + + $decorator->decorate($element); + } + + return $this; + } + + public function isValidEvent($event) + { + return in_array($event, [ + Form::ON_SUCCESS, + Form::ON_SENT, + Form::ON_ERROR, + Form::ON_REQUEST, + Form::ON_VALIDATE, + Form::ON_ELEMENT_REGISTERED, + ]); + } + + public function remove(ValidHtml $elementOrHtml) + { + if ($elementOrHtml instanceof FormElement) { + if ($this->hasElement($elementOrHtml)) { + $name = array_search($elementOrHtml, $this->elements, true); + if ($name !== false) { + unset($this->elements[$name]); + } + } + } + + parent::remove($elementOrHtml); + } + + /** + * Handler which is called after an element has been registered + * + * @param FormElement $element + */ + protected function onElementRegistered(FormElement $element) + { + } +} diff --git a/vendor/ipl/html/src/FormElement/HiddenElement.php b/vendor/ipl/html/src/FormElement/HiddenElement.php new file mode 100644 index 0000000..bffc7eb --- /dev/null +++ b/vendor/ipl/html/src/FormElement/HiddenElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class HiddenElement extends InputElement +{ + protected $type = 'hidden'; +} diff --git a/vendor/ipl/html/src/FormElement/InputElement.php b/vendor/ipl/html/src/FormElement/InputElement.php new file mode 100644 index 0000000..d5f945d --- /dev/null +++ b/vendor/ipl/html/src/FormElement/InputElement.php @@ -0,0 +1,49 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attribute; +use ipl\Html\Attributes; + +class InputElement extends BaseFormElement +{ + /** @var string Type of the input */ + protected $type; + + protected $tag = 'input'; + + /** + * Get the type of the input + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set the type of the input + * + * @param string $type + * + * @return $this + */ + public function setType($type) + { + $this->type = (string) $type; + + return $this; + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback( + 'type', + [$this, 'getType'], + [$this, 'setType'] + ); + } +} diff --git a/vendor/ipl/html/src/FormElement/LocalDateTimeElement.php b/vendor/ipl/html/src/FormElement/LocalDateTimeElement.php new file mode 100644 index 0000000..e165c7d --- /dev/null +++ b/vendor/ipl/html/src/FormElement/LocalDateTimeElement.php @@ -0,0 +1,52 @@ +<?php + +namespace ipl\Html\FormElement; + +use DateTime; +use ipl\Validator\DateTimeValidator; + +class LocalDateTimeElement extends InputElement +{ + const FORMAT = 'Y-m-d\TH:i:s'; + + protected $type = 'datetime-local'; + + protected $defaultAttributes = ['step' => '1']; + + /** @var DateTime */ + protected $value; + + public function setValue($value) + { + if (is_string($value)) { + $originalVal = $value; + $value = DateTime::createFromFormat(static::FORMAT, $value); + // In Chrome, if the seconds are set to 00, DateTime::createFromFormat() returns false. + // Create DateTime without seconds in format + if ($value === false) { + $format = substr(static::FORMAT, 0, strrpos(static::FORMAT, ':')); + $value = DateTime::createFromFormat($format, $originalVal); + } + + if ($value === false) { + $value = $originalVal; + } + } + + return parent::setValue($value); + } + + public function getValueAttribute() + { + if (! $this->value instanceof DateTime) { + return $this->value; + } + + return $this->value->format(static::FORMAT); + } + + public function addDefaultValidators() + { + $this->getValidators()->add(new DateTimeValidator()); + } +} diff --git a/vendor/ipl/html/src/FormElement/NumberElement.php b/vendor/ipl/html/src/FormElement/NumberElement.php new file mode 100644 index 0000000..b593135 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/NumberElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class NumberElement extends InputElement +{ + protected $type = 'number'; +} diff --git a/vendor/ipl/html/src/FormElement/PasswordElement.php b/vendor/ipl/html/src/FormElement/PasswordElement.php new file mode 100644 index 0000000..6e0e25f --- /dev/null +++ b/vendor/ipl/html/src/FormElement/PasswordElement.php @@ -0,0 +1,55 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; +use ipl\Html\Form; + +class PasswordElement extends InputElement +{ + /** @var string Dummy passwd of this element to be rendered */ + const DUMMYPASSWORD = '_ipl_form_5847ed1b5b8ca'; + + protected $type = 'password'; + + /** @var bool Status of the form */ + protected $isFormValid = true; + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + + $attributes->registerAttributeCallback( + 'value', + function () { + if ($this->hasValue() && count($this->getValueCandidates()) === 1 && $this->isFormValid) { + return self::DUMMYPASSWORD; + } + + if (parent::getValue() === self::DUMMYPASSWORD) { + return self::DUMMYPASSWORD; + } + + return null; + } + ); + } + + public function onRegistered(Form $form) + { + $form->on(Form::ON_VALIDATE, function ($form) { + $this->isFormValid = $form->isValid(); + }); + } + + public function getValue() + { + $value = parent::getValue(); + $candidates = $this->getValueCandidates(); + while ($value === self::DUMMYPASSWORD) { + $value = array_pop($candidates); + } + + return $value; + } +} diff --git a/vendor/ipl/html/src/FormElement/SelectElement.php b/vendor/ipl/html/src/FormElement/SelectElement.php new file mode 100644 index 0000000..3933222 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SelectElement.php @@ -0,0 +1,144 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Html; + +class SelectElement extends BaseFormElement +{ + protected $tag = 'select'; + + /** @var SelectOption[] */ + protected $options = []; + + protected $optionContent = []; + + public function __construct($name, $attributes = null) + { + $this->getAttributes()->registerAttributeCallback( + 'options', + null, + [$this, 'setOptions'] + ); + // ZF1 compatibility: + $this->getAttributes()->registerAttributeCallback( + 'multiOptions', + null, + [$this, 'setOptions'] + ); + + parent::__construct($name, $attributes); + } + + public function hasOption($value) + { + return isset($this->options[$value]); + } + + public function validate() + { + $value = $this->getValue(); + if ( + $value !== null && ( + ! ($option = $this->getOption($value)) + || $option->getAttributes()->has('disabled') + ) + ) { + $this->valid = false; + $this->addMessage("'$value' is not allowed here"); + } elseif ($this->isRequired() && $value === null) { + $this->valid = false; + } else { + parent::validate(); + } + + return $this; + } + + public function deselect() + { + $this->setValue(null); + + return $this; + } + + public function disableOption($value) + { + if ($option = $this->getOption($value)) { + $option->getAttributes()->add('disabled', true); + } + if ($this->getValue() == $value) { + $this->valid = false; + $this->addMessage("'$value' is not allowed here"); + } + + return $this; + } + + public function disableOptions($values) + { + foreach ($values as $value) { + $this->disableOption($value); + } + + return $this; + } + + /** + * @param $value + * @return SelectOption|null + */ + public function getOption($value) + { + if ($this->hasOption($value)) { + return $this->options[$value]; + } else { + return null; + } + } + + /** + * @param array $options + * @return $this + */ + public function setOptions(array $options) + { + $this->options = []; + foreach ($options as $value => $label) { + $this->optionContent[$value] = $this->makeOption($value, $label); + } + + return $this; + } + + protected function makeOption($value, $label) + { + if (is_array($label)) { + $grp = Html::tag('optgroup', ['label' => $value]); + foreach ($label as $option => $val) { + $grp->addHtml($this->makeOption($option, $val)); + } + + return $grp; + } else { + $option = new SelectOption($value, $label); + $option->getAttributes()->registerAttributeCallback('selected', function () use ($option) { + $optionValue = $option->getValue(); + + return is_int($optionValue) + // The loose comparison is required because PHP casts + // numeric strings to integers if used as array keys + ? $this->getValue() == $optionValue + : $this->getValue() === $optionValue; + }); + $this->options[$value] = $option; + + return $this->options[$value]; + } + } + + protected function assemble() + { + $this->addHtml(...array_values($this->optionContent)); + } +} diff --git a/vendor/ipl/html/src/FormElement/SelectOption.php b/vendor/ipl/html/src/FormElement/SelectOption.php new file mode 100644 index 0000000..d0dbf7c --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SelectOption.php @@ -0,0 +1,45 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\BaseHtmlElement; + +class SelectOption extends BaseHtmlElement +{ + protected $tag = 'option'; + + /** @var mixed */ + protected $value; + + /** + * SelectOption constructor. + * @param string|null $value + * @param string|null $label + */ + public function __construct($value = null, $label = null) + { + $this->value = $value; + $this->add($label); + + $this->getAttributes()->registerAttributeCallback('value', [$this, 'getValue']); + } + + /** + * @param $label + * @return $this + */ + public function setLabel($label) + { + $this->setContent($label); + + return $this; + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } +} diff --git a/vendor/ipl/html/src/FormElement/SubFormElement.php b/vendor/ipl/html/src/FormElement/SubFormElement.php new file mode 100644 index 0000000..5e0f77e --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SubFormElement.php @@ -0,0 +1,57 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attributes; + +class SubFormElement extends BaseFormElement +{ + use FormElements; + + protected $tag = 'div'; + + protected $defaultAttributes = [ + 'class' => 'ipl-subform' + ]; + + public function getValue($name = null) + { + if ($name === null) { + return $this->getValues(); + } else { + return $this->getElement($name)->getValue(); + } + } + + public function setValue($value) + { + $this->populate($value); + + return $this; + } + + public function isValid() + { + foreach ($this->getElements() as $element) { + if (! $element->isValid()) { + return false; + } + } + + return true; + } + + public function hasSubmitButton() + { + return true; + } + + protected function registerValueCallback(Attributes $attributes) + { + $attributes->registerAttributeCallback( + 'value', + null, + [$this, 'setValue'] + ); + } +} diff --git a/vendor/ipl/html/src/FormElement/SubmitButtonElement.php b/vendor/ipl/html/src/FormElement/SubmitButtonElement.php new file mode 100644 index 0000000..e94b681 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SubmitButtonElement.php @@ -0,0 +1,27 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Contract\FormSubmitElement; + +class SubmitButtonElement extends ButtonElement implements FormSubmitElement +{ + protected $defaultAttributes = ['type' => 'submit']; + + protected $value = 'y'; + + public function setLabel($label) + { + return $this->setContent($label); + } + + public function hasBeenPressed() + { + return (bool) $this->getValue(); + } + + public function isIgnored() + { + return true; + } +} diff --git a/vendor/ipl/html/src/FormElement/SubmitElement.php b/vendor/ipl/html/src/FormElement/SubmitElement.php new file mode 100644 index 0000000..51d4aa5 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/SubmitElement.php @@ -0,0 +1,50 @@ +<?php + +namespace ipl\Html\FormElement; + +use ipl\Html\Attribute; +use ipl\Html\Contract\FormSubmitElement; + +class SubmitElement extends InputElement implements FormSubmitElement +{ + protected $type = 'submit'; + + protected $buttonLabel; + + public function setLabel($label) + { + $this->buttonLabel = $label; + + return $this; + } + + /** + * @return string + */ + public function getButtonLabel() + { + if ($this->buttonLabel === null) { + return $this->getName(); + } else { + return $this->buttonLabel; + } + } + + /** + * @return mixed|static + */ + public function getValueAttribute() + { + return new Attribute('value', $this->getButtonLabel()); + } + + public function hasBeenPressed() + { + return $this->getButtonLabel() === $this->getValue(); + } + + public function isIgnored() + { + return true; + } +} diff --git a/vendor/ipl/html/src/FormElement/TextElement.php b/vendor/ipl/html/src/FormElement/TextElement.php new file mode 100644 index 0000000..0e3423d --- /dev/null +++ b/vendor/ipl/html/src/FormElement/TextElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class TextElement extends InputElement +{ + protected $type = 'text'; +} diff --git a/vendor/ipl/html/src/FormElement/TextareaElement.php b/vendor/ipl/html/src/FormElement/TextareaElement.php new file mode 100644 index 0000000..dc5c42b --- /dev/null +++ b/vendor/ipl/html/src/FormElement/TextareaElement.php @@ -0,0 +1,24 @@ +<?php + +namespace ipl\Html\FormElement; + +class TextareaElement extends BaseFormElement +{ + protected $tag = 'textarea'; + + public function setValue($value) + { + parent::setValue($value); + + // A textarea's content actually is the value + $this->setContent($value); + + return $this; + } + + public function getValueAttribute() + { + // textarea elements don't have a value attribute + return null; + } +} diff --git a/vendor/ipl/html/src/FormElement/TimeElement.php b/vendor/ipl/html/src/FormElement/TimeElement.php new file mode 100644 index 0000000..1ee0323 --- /dev/null +++ b/vendor/ipl/html/src/FormElement/TimeElement.php @@ -0,0 +1,8 @@ +<?php + +namespace ipl\Html\FormElement; + +class TimeElement extends InputElement +{ + protected $type = 'time'; +} diff --git a/vendor/ipl/html/src/FormattedString.php b/vendor/ipl/html/src/FormattedString.php new file mode 100644 index 0000000..1ef9b5b --- /dev/null +++ b/vendor/ipl/html/src/FormattedString.php @@ -0,0 +1,101 @@ +<?php + +namespace ipl\Html; + +use Exception; +use InvalidArgumentException; + +use function ipl\Stdlib\get_php_type; + +/** + * {@link sprintf()}-like formatted HTML string supporting lazy rendering of {@link ValidHtml} element arguments + * + * # Example Usage + * ``` + * $info = new FormattedString( + * 'Follow the %s for more information on %s', + * [ + * new Link('doc/html', 'HTML documentation'), + * Html::tag('strong', 'HTML elements') + * ] + * ); + * ``` + */ +class FormattedString implements ValidHtml +{ + /** @var ValidHtml[] */ + protected $args = []; + + /** @var ValidHtml */ + protected $format; + + /** + * Create a new {@link sprintf()}-like formatted HTML string + * + * @param string $format + * @param iterable $args + * + * @throws InvalidArgumentException If arguments given but not iterable + */ + public function __construct($format, $args = null) + { + $this->format = Html::wantHtml($format); + + if ($args !== null) { + if (! is_iterable($args)) { + throw new InvalidArgumentException(sprintf( + '%s expects parameter two to be iterable, got %s instead', + __METHOD__, + get_php_type($args) + )); + } + + foreach ($args as $key => $val) { + if (! is_scalar($val) || (is_string($val) && ! is_numeric($val))) { + $val = Html::wantHtml($val); + } + + $this->args[$key] = $val; + } + } + } + + + /** + * Create a new {@link sprintf()}-like formatted HTML string + * + * @param string $format + * @param mixed ...$args + * + * @return static + */ + public static function create($format, ...$args) + { + return new static($format, $args); + } + + /** + * Render text to HTML when treated like a string + * + * Calls {@link render()} internally in order to render the text to HTML. + * Exceptions will be automatically caught and returned as HTML string as well using {@link Error::render()}. + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return Error::render($e); + } + } + + public function render() + { + return vsprintf( + $this->format->render(), + $this->args + ); + } +} diff --git a/vendor/ipl/html/src/Html.php b/vendor/ipl/html/src/Html.php new file mode 100644 index 0000000..afcba39 --- /dev/null +++ b/vendor/ipl/html/src/Html.php @@ -0,0 +1,241 @@ +<?php + +namespace ipl\Html; + +use InvalidArgumentException; + +use function ipl\Stdlib\get_php_type; +use function ipl\Stdlib\iterable_key_first; + +/** + * Main utility class when working with ipl\Html + */ +abstract class Html +{ + /** + * Create a HTML element from the given tag, attributes and content + * + * This method does not render the HTML element but creates a {@link HtmlElement} + * instance from the given tag, attributes and content + * + * @param string $name The desired HTML tag name + * @param mixed $attributes HTML attributes or content for the element + * @param mixed $content The content of the element if no attributes have been given + * + * @return HtmlElement The created element + */ + public static function tag($name, $attributes = null, $content = null) + { + 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; + } + + 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 ?: [])); + } + + /** + * Convert special characters to HTML5 entities using the UTF-8 character + * set for encoding + * + * This method internally uses {@link htmlspecialchars} with the following + * flags: + * + * * Single quotes are not escaped (ENT_COMPAT) + * * Uses HTML5 entities, disallowing 
 (ENT_HTML5) + * * Invalid characters are replaced with � (ENT_SUBSTITUTE) + * + * Already existing HTML entities will be encoded as well. + * + * @param string $content The content to encode + * + * @return string The encoded content + */ + public static function escape($content) + { + return htmlspecialchars($content, ENT_COMPAT | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8'); + } + + /** + * Factory for {@link sprintf()}-like formatted HTML strings + * + * This allows to use {@link sprintf()}-like format strings with {@link ValidHtml} element arguments, but with the + * advantage that they'll not be rendered immediately. + * + * # Example Usage + * ``` + * echo Html::sprintf('Hello %s!', Html::tag('strong', $name)); + * ``` + * + * @param string $format + * @param mixed ...$args + * + * @return FormattedString + */ + public static function sprintf($format, ...$args) + { + return new FormattedString($format, $args); + } + + /** + * Wrap each item of then given list + * + * $wrapper is a simple HTML tag per entry if a string is given, + * otherwise the given callable is called with key and value of each list item as parameters. + * + * @param iterable $list + * @param string|callable $wrapper + * + * @return HtmlDocument + */ + public static function wrapEach($list, $wrapper) + { + if (! is_iterable($list)) { + throw new InvalidArgumentException(sprintf( + 'Html::wrapEach() requires a traversable list, got "%s"', + get_php_type($list) + )); + } + $result = new HtmlDocument(); + foreach ($list as $name => $value) { + if (is_string($wrapper)) { + $result->addHtml(Html::tag($wrapper, $value)); + } elseif (is_callable($wrapper)) { + $result->add($wrapper($name, $value)); + } else { + throw new InvalidArgumentException(sprintf( + 'Wrapper must be callable or a string in Html::wrapEach(), got "%s"', + get_php_type($wrapper) + )); + } + } + + return $result; + } + + /** + * Ensure that the given content of mixed type is converted to an instance of {@link ValidHtml} + * + * Returns the very same element in case it's already an instance of {@link ValidHtml}. + * + * @param mixed $any + * + * @return ValidHtml + * + * @throws InvalidArgumentException In case the given content is of an unsupported type + */ + public static function wantHtml($any) + { + if ($any instanceof ValidHtml) { + return $any; + } elseif (static::canBeRenderedAsString($any)) { + return new Text($any); + } elseif (is_iterable($any)) { + $html = new HtmlDocument(); + foreach ($any as $el) { + if ($el !== null) { + $html->addHtml(static::wantHtml($el)); + } + } + + return $html; + } else { + throw new InvalidArgumentException(sprintf( + 'String, Html Element or Array of such expected, got "%s"', + get_php_type($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 + * + * @param mixed $any + * + * @return bool + */ + public static function canBeRenderedAsString($any) + { + return is_scalar($any) || is_null($any) || ( + is_object($any) && method_exists($any, '__toString') + ); + } + + /** + * Forward inaccessible static method calls to {@link Html::tag()} with the method's name as tag + * + * @param string $name + * @param array $arguments + * + * @return HtmlElement + */ + public static function __callStatic($name, $arguments) + { + $attributes = array_shift($arguments); + $content = array_shift($arguments); + + return static::tag($name, $attributes, $content); + } + + /** + * @deprecated Use {@link Html::encode()} instead + */ + public static function escapeForHtml($content) + { + return static::escape($content); + } + + /** + * @deprecated Use {@link Error::render()} instead + */ + public static function renderError($error) + { + return Error::render($error); + } +} diff --git a/vendor/ipl/html/src/HtmlDocument.php b/vendor/ipl/html/src/HtmlDocument.php new file mode 100644 index 0000000..dbd53a1 --- /dev/null +++ b/vendor/ipl/html/src/HtmlDocument.php @@ -0,0 +1,538 @@ +<?php + +namespace ipl\Html; + +use Countable; +use Exception; +use InvalidArgumentException; +use ipl\Html\Contract\Wrappable; +use RuntimeException; + +/** + * HTML document + * + * An HTML document is composed of a tree of HTML nodes, i.e. text nodes and HTML elements. + */ +class HtmlDocument implements Countable, Wrappable +{ + /** @var string Content separator */ + protected $contentSeparator = ''; + + /** @var bool Whether the document has been assembled */ + protected $hasBeenAssembled = false; + + /** @var Wrappable Wrapper */ + protected $wrapper; + + /** @var Wrappable Wrapped element */ + private $wrapped; + + /** @var HtmlDocument The currently responsible wrapper */ + private $renderedBy; + + /** @var ValidHtml[] Content */ + private $content = []; + + /** @var array */ + private $contentIndex = []; + + /** + * Set the element to wrap + * + * @param Wrappable $element + * + * @return $this + */ + private function setWrapped(Wrappable $element) + { + $this->wrapped = $element; + + return $this; + } + + /** + * Consume the wrapped element + * + * @return Wrappable + */ + private function consumeWrapped() + { + $wrapped = $this->wrapped; + $this->wrapped = null; + + return $wrapped; + } + + /** + * Get the content + * + * return ValidHtml[] + */ + public function getContent() + { + return $this->content; + } + + /** + * Set the content + * + * @param mixed $content + * + * @return $this + */ + public function setContent($content) + { + $this->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; + } + + /** + * Get the content separator + * + * @return string + */ + public function getSeparator() + { + return $this->contentSeparator; + } + + /** + * Set the content separator + * + * @param string $separator + * + * @return $this + */ + public function setSeparator($separator) + { + $this->contentSeparator = $separator; + + return $this; + } + + /** + * Get the first {@link BaseHtmlElement} with the given tag + * + * @param string $tag + * + * @return BaseHtmlElement + * + * @throws InvalidArgumentException If no {@link BaseHtmlElement} with the given tag exists + */ + public function getFirst($tag) + { + foreach ($this->content as $c) { + if ($c instanceof BaseHtmlElement && $c->getTag() === $tag) { + return $c; + } + } + + throw new InvalidArgumentException(sprintf( + 'Trying to get first %s, but there is no such', + $tag + )); + } + + /** + * Add content + * + * @param mixed $content + * + * @return $this + */ + public function add($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; + } + + /** + * Add content from the given document + * + * @param HtmlDocument $from + * @param callable $callback Optional callback in order to transform the content to add + * + * @return $this + */ + public function addFrom(HtmlDocument $from, $callback = null) + { + $from->ensureAssembled(); + + $isCallable = is_callable($callback); + foreach ($from->getContent() as $item) { + $this->add($isCallable ? $callback($item) : $item); + } + + return $this; + } + + /** + * Check whether the given element is a direct or indirect child of this document + * + * A direct child is one that is part of this document's content. An indirect child + * is one that is either part of a direct child's content (recursively) or a wrapper + * of such (recursively). + * + * @param ValidHtml $element + * + * @return bool + */ + public function contains(ValidHtml $element) + { + $key = spl_object_hash($element); + if (array_key_exists($key, $this->contentIndex)) { + return true; + } + + foreach ($this->content as $child) { + if ($child instanceof self && ($child->contains($element) || $child->wrappedBy($element))) { + return true; + } + } + + return false; + } + + /** + * Prepend content + * + * @param mixed $content + * + * @return $this + */ + public function prepend($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, 0); + } + + return $this; + } + + /** + * Remove content + * + * @param ValidHtml $html + * + * @return $this + */ + public function remove(ValidHtml $html) + { + $key = spl_object_hash($html); + if (array_key_exists($key, $this->contentIndex)) { + foreach ($this->contentIndex[$key] as $pos) { + unset($this->content[$pos]); + } + } + $this->content = array_values($this->content); + + $this->reIndexContent(); + + return $this; + } + + /** + * Ensure that the document has been assembled + * + * @return $this + */ + public function ensureAssembled() + { + if (! $this->hasBeenAssembled) { + $this->hasBeenAssembled = true; + $this->assemble(); + } + + return $this; + } + + /** + * Get whether the document is empty + * + * @return bool + */ + public function isEmpty() + { + $this->ensureAssembled(); + + return empty($this->content); + } + + /** + * Render the content to HTML but ignore any wrapper + * + * @return string + */ + public function renderUnwrapped() + { + $this->ensureAssembled(); + $html = []; + + // This **must** be consumed after the document's assembly but before rendering the content. + // If the document consumes it during assembly, nothing happens. If the document is used as + // wrapper for another element, consuming it asap prevents a left-over reference and avoids + // the element from getting rendered multiple times. + $wrapped = $this->consumeWrapped(); + + $content = $this->getContent(); + if ($wrapped !== null && ! $this->contains($wrapped)) { + $content[] = $wrapped; + } + + foreach ($content as $element) { + if ($element instanceof self) { + $element->renderedBy = $this; + } + + $html[] = $element->render(); + + if ($element instanceof self) { + unset($element->renderedBy); + } + } + + return implode($this->contentSeparator, $html); + } + + public function __clone() + { + foreach ($this->content as $key => $element) { + $this->content[$key] = clone $element; + } + + $this->reIndexContent(); + } + + /** + * Render content to HTML when treated like a string + * + * Calls {@link render()} internally in order to render the text to HTML. + * Exceptions will be automatically caught and returned as HTML string as well using {@link Error::render()}. + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return Error::render($e); + } + } + + /** + * Assemble the document + * + * Override this method in order to provide content in concrete classes. + */ + protected function assemble() + { + } + + /** + * Render the document to HTML respecting the set wrapper + * + * @return string + */ + protected function renderWrapped() + { + $wrapper = $this->wrapper; + + if (isset($this->renderedBy)) { + if ($wrapper === $this->renderedBy || $wrapper->contains($this->renderedBy)) { + // $this might be an intermediate wrapper that's already about to be rendered. + // In case of an element (referencing $this as a wrapper) that is a child of an + // outer wrapper, it is required to ignore $wrapper as otherwise it's a loop. + // ($wrapper then is in the render path of the outer wrapper and sideways "stolen") + return $this->renderUnwrapped(); + } + + $wrapper->renderedBy = $this->renderedBy; + } elseif (isset($wrapper->renderedBy)) { + throw new RuntimeException('Wrapper loop detected'); + } else { + $this->renderedBy = $wrapper; + } + + $html = $wrapper->renderWrappedDocument($this); + + if (isset($this->renderedBy)) { + if ($this->renderedBy === $wrapper) { + unset($this->renderedBy); + } elseif ($wrapper->renderedBy === $this->renderedBy) { + unset($wrapper->renderedBy); + } + } + + return $html; + } + + /** + * Render the given document to HTML by treating this document as the wrapper + * + * @param HtmlDocument $document + * + * @return string + */ + protected function renderWrappedDocument(HtmlDocument $document) + { + return $this->setWrapped($document)->render(); + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->content); + } + + public function getWrapper() + { + return $this->wrapper; + } + + public function setWrapper(Wrappable $wrapper) + { + $this->wrapper = $wrapper; + + return $this; + } + + public function addWrapper(Wrappable $wrapper) + { + if ($this->wrapper === null) { + $this->setWrapper($wrapper); + } else { + $this->wrapper->addWrapper($wrapper); + } + + return $this; + } + + public function prependWrapper(Wrappable $wrapper) + { + if ($this->wrapper === null) { + $this->setWrapper($wrapper); + } else { + $wrapper->addWrapper($this->wrapper); + $this->setWrapper($wrapper); + } + + return $this; + } + + /** + * Check whether the given element wraps this document (recursively) + * + * @param ValidHtml $element + * + * @return bool + */ + protected function wrappedBy(ValidHtml $element) + { + if ($this->wrapper === null) { + return false; + } + + if ($this->wrapper === $element || $this->wrapper->wrappedBy($element)) { + return true; + } + + return false; + } + + public function render() + { + $this->ensureAssembled(); + if ($this->wrapper === null) { + return $this->renderUnwrapped(); + } else { + return $this->renderWrapped(); + } + } + + private function addIndexedContent(ValidHtml $html) + { + $pos = count($this->content); + $this->content[$pos] = $html; + $this->addObjectPosition($html, $pos); + } + + private function addObjectPosition(ValidHtml $html, $pos) + { + $key = spl_object_hash($html); + if (array_key_exists($key, $this->contentIndex)) { + $this->contentIndex[$key][] = $pos; + } else { + $this->contentIndex[$key] = [$pos]; + } + } + + private function incrementIndexKeys() + { + foreach ($this->contentIndex as & $index) { + foreach ($index as & $pos) { + $pos++; + } + } + } + + private function reIndexContent() + { + $this->contentIndex = []; + foreach ($this->content as $pos => $html) { + $this->addObjectPosition($html, $pos); + } + } +} diff --git a/vendor/ipl/html/src/HtmlElement.php b/vendor/ipl/html/src/HtmlElement.php new file mode 100644 index 0000000..4f5d162 --- /dev/null +++ b/vendor/ipl/html/src/HtmlElement.php @@ -0,0 +1,43 @@ +<?php + +namespace ipl\Html; + +/** + * The HtmlElement represents any HTML element + * + * A typical HTML element includes a tag, attributes and content. + */ +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 $attributes The HTML attributes for the element + * @param ValidHtml ...$content The content of the element + */ + public function __construct($tag, Attributes $attributes = null, ValidHtml ...$content) + { + $this->tag = $tag; + + if ($attributes !== null) { + $this->getAttributes()->merge($attributes); + } + + $this->setHtmlContent(...$content); + } + + /** + * Create a new HTML element from the given tag, attributes and content + * + * @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::wantAttributes($attributes), ...Html::wantHtmlList($content)); + } +} diff --git a/vendor/ipl/html/src/HtmlString.php b/vendor/ipl/html/src/HtmlString.php new file mode 100644 index 0000000..e62086b --- /dev/null +++ b/vendor/ipl/html/src/HtmlString.php @@ -0,0 +1,13 @@ +<?php + +namespace ipl\Html; + +/** + * HTML string + * + * HTML strings promise to be already escaped and can be anything from simple text to full HTML markup. + */ +class HtmlString extends Text +{ + protected $escaped = true; +} diff --git a/vendor/ipl/html/src/Table.php b/vendor/ipl/html/src/Table.php new file mode 100644 index 0000000..28a6738 --- /dev/null +++ b/vendor/ipl/html/src/Table.php @@ -0,0 +1,226 @@ +<?php + +namespace ipl\Html; + +use RuntimeException; +use stdClass; + +class Table extends BaseHtmlElement +{ + protected $contentSeparator = "\n"; + + /** @var string */ + protected $tag = 'table'; + + /** @var HtmlElement */ + private $caption; + + /** @var HtmlElement */ + private $header; + + /** @var HtmlElement */ + private $body; + + /** @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 <caption> tag' + ); + } + + break; + default: + $this->getBody()->addHtml(static::row([$html])); + } + } else { + $this->getBody()->addHtml(static::row([$html])); + } + } + + return $this; + } + + /** + * @param mixed $content + * @return $this + */ + public function add($content) + { + if ($content instanceof stdClass) { + $this->getBody()->addHtml(static::row((array) $content)); + } elseif (is_iterable($content)) { + $this->getBody()->addHtml(static::row($content)); + } elseif ($content instanceof ValidHtml) { + $this->addHtml($content); + } else { + $this->getBody()->addHtml(static::row([$content])); + } + + return $this; + } + + /** + * Set the table title + * + * Will be rendered as a "caption" HTML element + * + * @param mixed $caption + * @return $this + */ + public function setCaption($caption) + { + if ($caption instanceof BaseHtmlElement && $caption->getTag() === 'caption') { + $this->caption = $caption; + $this->prependHtml($caption); + } elseif ($this->caption === null) { + $this->caption = new HtmlElement('caption', null, ...Html::wantHtmlList($caption)); + $this->prependHtml($this->caption); + } else { + $this->caption->setContent($caption); + } + + return $this; + } + + /** + * Static helper creating a tr element + * + * @param Attributes|array $attributes + * @param Html|array|string $content + * @return HtmlElement + */ + public static function tr($content = null, $attributes = null) + { + return Html::tag('tr', $attributes, $content); + } + + /** + * Static helper creating a th element + * + * @param Attributes|array $attributes + * @param Html|array|string $content + * @return HtmlElement + */ + public static function th($content = null, $attributes = null) + { + return Html::tag('th', $attributes, $content); + } + + /** + * Static helper creating a td element + * + * @param Attributes|array $attributes + * @param Html|array|string $content + * @return HtmlElement + */ + public static function td($content = null, $attributes = null) + { + return Html::tag('td', $attributes, $content); + } + + /** + * @param $row + * @param null $attributes + * @param string $tag + * @return HtmlElement + */ + public static function row($row, $attributes = null, $tag = 'td') + { + $tr = static::tr(); + foreach ((array) $row as $value) { + $tr->addHtml(Html::tag($tag, null, $value)); + } + + if ($attributes !== null) { + $tr->setAttributes($attributes); + } + + return $tr; + } + + /** + * @return HtmlElement + */ + public function getBody() + { + if ($this->body === null) { + $this->addHtml(Html::tag('tbody')->setSeparator("\n")); + } + + return $this->body; + } + + /** + * @return HtmlElement + */ + public function getHeader() + { + if ($this->header === null) { + $this->addHtml(Html::tag('thead')->setSeparator("\n")); + } + + return $this->header; + } + + /** + * @return HtmlElement + */ + public function getFooter() + { + if ($this->footer === null) { + $this->addHtml(Html::tag('tfoot')->setSeparator("\n")); + } + + return $this->footer; + } + + /** + * @return HtmlElement + */ + public function nextBody() + { + $this->body = null; + + return $this->getBody(); + } + + /** + * @return HtmlElement + */ + public function nextHeader() + { + $this->header = null; + + return $this->getHeader(); + } +} diff --git a/vendor/ipl/html/src/TemplateString.php b/vendor/ipl/html/src/TemplateString.php new file mode 100644 index 0000000..611ea12 --- /dev/null +++ b/vendor/ipl/html/src/TemplateString.php @@ -0,0 +1,175 @@ +<?php + +namespace ipl\Html; + +use Exception; + +/** + * Render {{#mustache}}Mustache{{/mustache}}-like string from {@link ValidHtml} element arguments + * + * # Example Usage + * ``` + * $info = TemplateString::create( + * 'Follow the {{#doc}}HTML documentation{{/doc}} for more information on {{#strong}}HTML elements{{/strong}}', + * [ + * 'doc' => new Link(null, 'doc/html'), + * 'strong' => Html::tag('strong') + * ] + * ); + * ``` + */ +class TemplateString extends FormattedString +{ + /** @var array */ + protected $templateArgs = []; + + /** @var int */ + protected $pos = 0; + + /** @var string */ + protected $string; + + /** @var int */ + protected $length; + + public function __construct($format, $args = null) + { + $parentArgs = []; + foreach ($args ?: [] as $val) { + if (is_array($val) && is_string(key($val))) { + $this->templateArgs += $val; + } else { + $parentArgs[] = $val; + } + } + + parent::__construct($format, $parentArgs); + } + + /** + * Parse template strings + * + * @param null $for template name + * @return HtmlDocument + * @throws Exception in case of missing template argument or unbounded open or close templates + */ + protected function parseTemplates($for = null) + { + $buffer = ''; + + while (($char = $this->readChar()) !== false) { + if ($char !== '{') { + $buffer .= $char; + continue; + } + + $nextChar = $this->readChar(); + if ($nextChar !== '{') { + $buffer .= $char . $nextChar; + continue; + } + + $templateHandle = $this->readChar(); + $start = $templateHandle === '#'; + $end = $templateHandle === '/'; + + $templateKey = $this->readUntil('}'); + // if the string following '{{#' is read up to the last character or (length - 1)th character + // then it is not a template + if ($this->pos >= $this->length - 1) { + $buffer .= $char . $nextChar . $templateHandle . $templateKey; + continue; + } + + $this->pos++; + $closeChar = $this->readChar(); + + if ($closeChar !== '}') { + $buffer .= $char . $nextChar . $templateHandle . $templateKey . '}' . $closeChar; + continue; + } + + if ($start) { + if (isset($this->templateArgs[$templateKey])) { + $wrapper = $this->templateArgs[$templateKey]; + + $buffer .= $this->parseTemplates($templateKey)->prependWrapper($wrapper); + } else { + throw new Exception(sprintf( + 'Missing template argument: %s ', + $templateKey + )); + } + } elseif ($for === $templateKey && $end) { + // close the template + $for = null; + break; + } else { + // throw exception for unbounded closing of templates + throw new Exception(sprintf( + 'Unbound closing of template: %s', + $templateKey + )); + } + } + + if ($this->pos === $this->length && $for !== null) { + throw new Exception(sprintf( + 'Unbound opening of template: %s', + $for + )); + } + + return (new HtmlDocument())->addHtml(HtmlString::create($buffer)); + } + + /** + * Read until any of the given chars appears + * + * @param string ...$chars + * + * @return string + */ + protected function readUntil(...$chars) + { + $buffer = ''; + while (($c = $this->readChar()) !== false) { + if (in_array($c, $chars, true)) { + $this->pos--; + break; + } + + $buffer .= $c; + } + + return $buffer; + } + + /** + * Read a single character + * + * @return false|string false if there is no character left + */ + protected function readChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos++]; + } + + return false; + } + + public function render() + { + $formattedstring = parent::render(); + if (empty($this->templateArgs)) { + return $formattedstring; + } + + $this->string = $formattedstring; + + $this->length = strlen($formattedstring); + + return $this->parseTemplates()->render(); + } +} diff --git a/vendor/ipl/html/src/Text.php b/vendor/ipl/html/src/Text.php new file mode 100644 index 0000000..710e3d9 --- /dev/null +++ b/vendor/ipl/html/src/Text.php @@ -0,0 +1,116 @@ +<?php + +namespace ipl\Html; + +use Exception; + +/** + * A text node + * + * Primitive element that renders text to HTML while automatically escaping its content. + * If the passed content is already escaped, see {@link setEscaped()} to indicate this. + */ +class Text implements ValidHtml +{ + /** @var string */ + protected $content; + + /** @var bool Whether the content is already escaped */ + protected $escaped = false; + + /** + * Create a new text node + * + * @param string $content + */ + public function __construct($content) + { + $this->setContent($content); + } + + /** + * Create a new text node + * + * @param string $content + * + * @return static + */ + public static function create($content) + { + return new static($content); + } + + /** + * Get the content + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Set the content + * + * @param string $content + * + * @return $this + */ + public function setContent($content) + { + $this->content = (string) $content; + + return $this; + } + + /** + * Get whether the content promises to be already escaped + * + * @return bool + */ + public function isEscaped() + { + return $this->escaped; + } + + /** + * Set whether the content is already escaped + * + * @param bool $escaped + * + * @return $this + */ + public function setEscaped($escaped = true) + { + $this->escaped = $escaped; + + return $this; + } + + /** + * Render text to HTML when treated like a string + * + * Calls {@link render()} internally in order to render the text to HTML. + * Exceptions will be automatically caught and returned as HTML string as well using {@link Error::render()}. + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return Error::render($e); + } + } + + public function render() + { + if ($this->escaped) { + return $this->content; + } else { + return Html::escape($this->content); + } + } +} diff --git a/vendor/ipl/html/src/ValidHtml.php b/vendor/ipl/html/src/ValidHtml.php new file mode 100644 index 0000000..2b88af4 --- /dev/null +++ b/vendor/ipl/html/src/ValidHtml.php @@ -0,0 +1,17 @@ +<?php + +namespace ipl\Html; + +/** + * Interface for HTML elements or primitives that promise to render valid UTF-8 encoded HTML5 with special characters + * converted to HTML entities + */ +interface ValidHtml +{ + /** + * Render to HTML + * + * @return string UTF-8 encoded HTML5 with special characters converted to HTML entities + */ + public function render(); +} |