From 49574885a4f2119b2496a02a933d8823717dc6a9 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 5 Mar 2024 06:11:59 -0300
Subject: [PATCH 1/2] Prepare for first release.
---
CHANGELOG.md | 6 +-
README.md | 350 ++++++++++++++++++++++++--
composer.json | 25 +-
ecs.php | 1 -
phpunit.xml.dist | 2 +-
src/Attributes.php | 207 +++++++++++++++
src/CssClass.php | 97 +++++++
src/Encode.php | 55 ++++
src/Example.php | 13 -
src/Sanitize.php | 82 ++++++
src/Utils.php | 209 +++++++++++++++
src/Validator.php | 101 ++++++++
tests/AttributesTest.php | 18 ++
tests/CssClassTest.php | 108 ++++++++
tests/EncodeTest.php | 36 +++
tests/ExampleTest.php | 18 --
tests/Provider/AttributesProvider.php | 136 ++++++++++
tests/Provider/EncodeProvider.php | 32 +++
tests/Provider/UtilsProvider.php | 54 ++++
tests/SanitizeTest.php | 77 ++++++
tests/Support/InputWidget.php | 20 ++
tests/UtilsTest.php | 119 +++++++++
tests/ValidatorTest.php | 93 +++++++
23 files changed, 1796 insertions(+), 63 deletions(-)
create mode 100644 src/Attributes.php
create mode 100644 src/CssClass.php
create mode 100644 src/Encode.php
delete mode 100644 src/Example.php
create mode 100644 src/Sanitize.php
create mode 100644 src/Utils.php
create mode 100644 src/Validator.php
create mode 100644 tests/AttributesTest.php
create mode 100644 tests/CssClassTest.php
create mode 100644 tests/EncodeTest.php
delete mode 100644 tests/ExampleTest.php
create mode 100644 tests/Provider/AttributesProvider.php
create mode 100644 tests/Provider/EncodeProvider.php
create mode 100644 tests/Provider/UtilsProvider.php
create mode 100644 tests/SanitizeTest.php
create mode 100644 tests/Support/InputWidget.php
create mode 100644 tests/UtilsTest.php
create mode 100644 tests/ValidatorTest.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 144319e..2e22acd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
# Change Log
-## 0.1.0 Under development
+## 0.1.1 Under development
+
+## 0.1.0 March 5, 2024
+
+- Initial release
diff --git a/README.md b/README.md
index 6de01d8..726059a 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,36 @@
-
+
-
Template.
+ UI Awesome HTML Helpers Code Generator for PHP.
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
-
-
-
+HTML Helper is a PHP library that simplifies the creation of HTML elements. It provides a set of classes to generate
+HTML attributes, encode content, sanitize HTML content, and more.
+
+
## Installation
The preferred way to install this extension is through [composer](https://getcomposer.org/download/).
@@ -34,20 +38,328 @@ The preferred way to install this extension is through [composer](https://getcom
Either run
```shell
-composer require --prefer-dist package
+composer require --prefer-dist ui-awesome/html-helper:^0.1
```
or add
```json
-"package": "version"
+"ui-awesome/html-helper": "^0.1"
```
-to the require-dev section of your `composer.json` file.
+to the require section of your `composer.json` file.
## Usage
-[Check the documentation docs](docs/README.md) to learn about usage.
+### Add CSS classes
+
+The `CssClasses::class` helper can be used to add CSS classes to an HTML element.
+
+The method accepts three parameters:
+
+- `attributes:` (array): The HTML attributes of the element.
+- `classes:` (array|string): The CSS classes to add.
+- `overwrite:` (bool): Whether to overwrite the `class` attribute or not. For default, it is `false`.
+
+```php
+attributes, ['btn', 'btn-primary', 'btn-lg']);
+```
+
+Overwriting the `class` attribute:
+
+```php
+ 'btn'];
+
+$classes = CssClasses::add($this->attributes, ['btn-primary', 'btn-lg'], true);
+```
+
+### Convert regular expression to pattern
+
+The `Utils::class` helper can be used to normalize a regular expression.
+
+The method accepts one parameter:
+
+- `regexp:` (string): The pattern to normalize.
+- `delimiter:` (string): The delimiter to use. For default, it is `null`.
+
+```php
+alert("Hello, World!")');
+```
+
+### Encode value
+
+The `Encode::class` helper can be used to encode HTML value.
+
+The method accepts tree parameters:
+
+- `value:` (string): The value to encode.
+- `doubleEncode:` (bool): Whether to double encode the value or not. For default, it is `true`.
+- `charset:` (string): The charset to use. For default, it is `UTF-8`.
+
+```php
+alert("Hello, World!")');
+```
+
+### Get short class name
+
+The `Utils::class` helper can be used to get the short class name.
+
+The method accepts one parameter:
+
+- `class:` (string): The class name to get the short name.
+- `suffix:` (string): Whether to append the `::class` suffix to the class name.
+ For default, it is `true`. If it is `false`, the method will return the short name without the `::class` suffix.
+- `lowercase:` (bool): Whether to convert the class name to lowercase or not.
+ For default, it is `false`.
+
+```php
+alert("Hello, World!")');
+```
+
+### Render HTML attributes
+
+The `Attributes::class` helper can be used to `render` HTML attributes in a programmatic way.
+
+```php
+ 'btn btn-primary',
+ 'id' => 'submit-button',
+ 'disabled' => true,
+ ]
+);
+```
+
+### Validate value in list
+
+The `Validator::class` helper can be used to validate a value in a list.
+
+The method accepts tree parameters:
+
+- `value:` (mixed): The value to validate.
+- `exceptionMessage:` (string): The exception message to throw if the value is not in the list.
+- `list:` (...string): The list to validate the value.
+
+```php
+withPhpCsFixerSets(perCS20: true)
->withPreparedSets(
- arrays: true,
cleanCode: true,
comments:true,
docblocks: true,
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 8ff515d..e77699d 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,7 +11,7 @@
stopOnFailure="false"
>
-
+
tests
diff --git a/src/Attributes.php b/src/Attributes.php
new file mode 100644
index 0000000..c8ac770
--- /dev/null
+++ b/src/Attributes.php
@@ -0,0 +1,207 @@
+ 'xyz', 'age' => 13]`, two attributes will be
+ * generated instead of one: `data-name="xyz" data-age="13"`.
+ */
+ private const DATA = ['aria', 'data', 'data-ng', 'ng'];
+
+ /**
+ * @var int the JSON encoding options used in {@see renderAttribute()} when rendering array values.
+ */
+ private const JSON_FLAGS = JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS |
+ JSON_THROW_ON_ERROR;
+
+ /**
+ * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes that are
+ * rendered by {@see render()}.
+ *
+ * @psalm-var string[]
+ */
+ private const ORDER = [
+ 'class',
+ 'id',
+ 'name',
+ 'type',
+ 'http-equiv',
+ 'value',
+ 'href',
+ 'src',
+ 'for',
+ 'title',
+ 'alt',
+ 'role',
+ 'tabindex',
+ 'srcset',
+ 'form',
+ 'action',
+ 'method',
+ 'selected',
+ 'checked',
+ 'readonly',
+ 'disabled',
+ 'multiple',
+ 'size',
+ 'maxlength',
+ 'width',
+ 'height',
+ 'rows',
+ 'cols',
+ 'alt',
+ 'title',
+ 'rel',
+ 'media',
+ ];
+
+ /**
+ * Renders the HTML tag attributes.
+ *
+ * Attributes whose values are of boolean type will be treated as
+ * [boolean attributes](http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes).
+ *
+ * Attributes whose values are null will not be rendered.
+ *
+ * The values of attributes will be HTML-encoded using {@see encode()}.
+ *
+ * @param array $attributes Attributes to be rendered. The attribute values will be HTML-encoded using
+ * {@see Encode}.
+ *
+ * `aria` and `data` attributes get special handling when they are set to an array value. In these cases, the array
+ * will be "expanded" and a list of ARIA/data attributes will be rendered. For example,
+ * `'aria' => ['role' => 'checkbox', 'value' => 'true']` would be rendered as
+ * `aria-role="checkbox" aria-value="true"`.
+ *
+ * If a nested `data` value is set to an array, it will be JSON-encoded. For example,
+ * `'data' => ['params' => ['id' => 1, 'name' => 'yii']]` would be rendered as
+ * `data-params='{"id":1,"name":"yii"}'`.
+ *
+ * @return string The rendering result. If the attributes are not empty, they will be rendered into a string with a
+ * leading space (so that it can be directly appended to the tag name in a tag). If there is no attribute, an
+ * empty string will be returned.
+ *
+ * {@see addCssClass()}
+ */
+ public static function render(array $attributes): string
+ {
+ $html = '';
+ $sorted = [];
+
+ foreach (self::ORDER as $name) {
+ if (isset($attributes[$name])) {
+ /** @psalm-var string[] $sorted */
+ $sorted[$name] = $attributes[$name];
+ }
+ }
+
+ $attributes = array_merge($sorted, $attributes);
+
+ /**
+ * @var string $name
+ * @var mixed $values
+ */
+ foreach ($attributes as $name => $values) {
+ if ($name !== '' && $values !== '' && $values !== null) {
+ $html .= self::renderAttributes($name, $values);
+ }
+ }
+
+ return $html;
+ }
+
+ private static function renderAttribute(string $name, string $encodedValue = '', string $quote = '"'): string
+ {
+ if ($encodedValue === '') {
+ return ' ' . $name;
+ }
+
+ return ' ' . $name . '=' . $quote . $encodedValue . $quote;
+ }
+
+ private static function renderAttributes(string $name, mixed $values): string
+ {
+ return match (gettype($values)) {
+ 'array' => self::renderArrayAttributes($name, $values),
+ 'boolean' => self::renderBooleanAttributes($name, $values),
+ default => self::renderAttribute($name, Encode::value($values)),
+ };
+ }
+
+ private static function renderArrayAttributes(string $name, array $values): string
+ {
+ $attributes = self::renderAttribute($name, json_encode($values, self::JSON_FLAGS), '\'');
+
+ if (in_array($name, self::DATA, true)) {
+ $attributes = self::renderDataAttributes($name, $values);
+ }
+
+ if ($name === 'class') {
+ $attributes = self::renderClassAttributes($name, $values);
+ }
+
+ if ($name === 'style') {
+ $attributes = self::renderStyleAttributes($name, $values);
+ }
+
+ return $attributes;
+ }
+
+ private static function renderBooleanAttributes(string $name, bool $value): string
+ {
+ return $value === true ? self::renderAttribute($name) : '';
+ }
+
+ private static function renderClassAttributes(string $name, array $values): string
+ {
+ /** @psalm-var string[] $values */
+ return match ($values) {
+ [] => '',
+ default => " $name=\"" . Encode::content(implode(' ', $values)) . '"',
+ };
+ }
+
+ private static function renderDataAttributes(string $name, array $values): string
+ {
+ $result = '';
+
+ /** @psalm-var array $values */
+ foreach ($values as $n => $v) {
+ $result .= match (is_array($v)) {
+ true => self::renderAttribute($name . '-' . $n, json_encode($v, self::JSON_FLAGS), '\''),
+ false => self::renderAttribute($name . '-' . $n, Encode::value($v)),
+ };
+ }
+
+ return $result;
+ }
+
+ private static function renderStyleAttributes(string $name, array $values): string
+ {
+ $result = '';
+
+ /** @psalm-var string[] $values */
+ foreach ($values as $n => $v) {
+ $result .= "$n: $v; ";
+ }
+
+ return $result === '' ? '' : " $name=\"" . Encode::content(rtrim($result)) . '"';
+ }
+}
diff --git a/src/CssClass.php b/src/CssClass.php
new file mode 100644
index 0000000..56aef3b
--- /dev/null
+++ b/src/CssClass.php
@@ -0,0 +1,97 @@
+ $class) {
+ if (is_int($key) && !in_array($class, $existingClasses, true)) {
+ $existingClasses[] = $class;
+ } elseif (!isset($existingClasses[$key])) {
+ $existingClasses[$key] = $class;
+ }
+ }
+
+ return $existingClasses;
+ }
+}
diff --git a/src/Encode.php b/src/Encode.php
new file mode 100644
index 0000000..e223648
--- /dev/null
+++ b/src/Encode.php
@@ -0,0 +1,55 @@
+tag content`.
+ *
+ * Characters encoded are: &, <, >.
+ *
+ * @param string $content The content to be encoded.
+ * @param bool $doubleEncode If already encoded, entities should be encoded.
+ * @param string $charset The encoding to use, defaults to "UTF-8".
+ *
+ * @return string Encoded content.
+ *
+ * @link https://html.spec.whatwg.org/#data-state
+ */
+ public static function content(string $content, bool $doubleEncode = true, string $charset = 'UTF-8'): string
+ {
+ return htmlspecialchars($content, self::HTMLSPECIALCHARS_FLAGS, $charset, $doubleEncode);
+ }
+
+ /**
+ * Encodes special characters into HTML entities for use as HTML tag quoted attribute value
+ * i.e. ``.
+ * Characters encoded are: &, <, >, ", ', U+0000 (null).
+ *
+ * @param mixed $value The attribute value to be encoded.
+ * @param bool $doubleEncode If already encoded, entities should be encoded.
+ * @param string $charset The encoding to use, defaults to "UTF-8".
+ *
+ * @return string Encoded attribute value.
+ *
+ * @link https://html.spec.whatwg.org/#attribute-value-(single-quoted)-state
+ * @link https://html.spec.whatwg.org/#attribute-value-(double-quoted)-state
+ */
+ public static function value(mixed $value, bool $doubleEncode = true, string $charset = 'UTF-8'): string
+ {
+ $value = htmlspecialchars((string) $value, self::HTMLSPECIALCHARS_FLAGS, $charset, $doubleEncode);
+
+ return strtr($value, ['\u{0000}' => '']);
+ }
+}
diff --git a/src/Example.php b/src/Example.php
deleted file mode 100644
index 4dfa14e..0000000
--- a/src/Example.php
+++ /dev/null
@@ -1,13 +0,0 @@
-
+ */
+ private static array $removeEvilAttributes = [
+ 'form',
+ 'formaction',
+ 'style',
+ ];
+ /**
+ * @var array
+ */
+ private static array $removeEvilHtmlTags = [
+ 'button',
+ 'form',
+ 'input',
+ 'select',
+ 'svg',
+ 'textarea',
+ ];
+
+ /**
+ * Initialize the class with custom configuration.
+ *
+ * @psalm-param array $removeEvilAttributes
+ * @psalm-param array $removeEvilHtmlTags
+ */
+ public static function initialize(array $removeEvilAttributes = [], array $removeEvilHtmlTags = []): void
+ {
+ self::$removeEvilAttributes = $removeEvilAttributes;
+ self::$removeEvilHtmlTags = $removeEvilHtmlTags;
+ }
+
+ /**
+ * Sanitizes HTML content to prevent XSS attacks.
+ *
+ * @param RenderInterface|string ...$values The HTML content to sanitize.
+ *
+ * @return string The sanitized HTML content.
+ */
+ public static function html(string|RenderInterface ...$values): string
+ {
+ $cleanHtml = '';
+
+ foreach ($values as $value) {
+ if ($value instanceof RenderInterface) {
+ $value = $value->render();
+ }
+
+ /** @psalm-var string|string[] $cleanValue */
+ $cleanValue = self::cleanXSS($value);
+ $cleanValue = is_array($cleanValue) ? implode('', $cleanValue) : $cleanValue;
+
+ $cleanHtml .= $cleanValue;
+ }
+
+ return $cleanHtml;
+ }
+
+ private static function cleanXSS(string $content): string|array
+ {
+ $antiXss = new AntiXSS();
+
+ $antiXss->removeEvilAttributes(self::$removeEvilAttributes);
+ $antiXss->removeEvilHtmlTags(self::$removeEvilHtmlTags);
+
+ return $antiXss->xss_clean($content);
+ }
+}
diff --git a/src/Utils.php b/src/Utils.php
new file mode 100644
index 0000000..3bdcf37
--- /dev/null
+++ b/src/Utils.php
@@ -0,0 +1,209 @@
+ 'content', 'prefix' => '', 'suffix' => '[0]']`
+ *
+ * An property expression is an property name prefixed and/or suffixed with array indexes. It is mainly used in
+ * tabular data input and/or input of an array type. Below are some examples:
+ *
+ * - `[0]content` is used in tabular data input to represent the "content" property for the first model in tabular
+ * input;
+ * - `dates[0]` represents the first array element of the "dates" property;
+ * - `[0]dates[0]` represents the first array element of the "dates" property for the first model in tabular
+ * input.
+ *
+ * @param string $property The property name or expression
+ *
+ * @throws InvalidArgumentException If the property name contains non-word characters.
+ *
+ * @psalm-return string[]
+ */
+ private static function parseProperty(string $property): array
+ {
+ if (!preg_match('/(^|.*\])([\w\.\+\-_]+)(\[.*|$)/u', $property, $matches)) {
+ throw new InvalidArgumentException('Property name must contain word characters only.');
+ }
+
+ return [
+ 'name' => $matches[2],
+ 'prefix' => $matches[1],
+ 'suffix' => $matches[3],
+ ];
+ }
+}
diff --git a/src/Validator.php b/src/Validator.php
new file mode 100644
index 0000000..51ad5e8
--- /dev/null
+++ b/src/Validator.php
@@ -0,0 +1,101 @@
+assertSame($expected, Attributes::render($attributes));
+ }
+}
diff --git a/tests/CssClassTest.php b/tests/CssClassTest.php
new file mode 100644
index 0000000..5002ec5
--- /dev/null
+++ b/tests/CssClassTest.php
@@ -0,0 +1,108 @@
+assertSame([], $attributes);
+
+ CssClass::add($attributes, ['test-class']);
+ $this->assertSame(['class' => 'test-class'], $attributes);
+
+ CssClass::add($attributes, ['test-class']);
+ $this->assertSame(['class' => 'test-class'], $attributes);
+
+ CssClass::add($attributes, ['test-class-1']);
+ $this->assertSame(['class' => 'test-class test-class-1'], $attributes);
+
+ CssClass::add($attributes, ['test-class', 'test-class-1']);
+ $this->assertSame(['class' => 'test-class test-class-1'], $attributes);
+
+ CssClass::add($attributes, ['test-class-2']);
+ $this->assertSame(['class' => 'test-class test-class-1 test-class-2'], $attributes);
+
+ CssClass::add($attributes, ['test-override-class'], true);
+ $this->assertSame(['class' => 'test-override-class'], $attributes);
+ }
+
+ public function testAddMethodWithArrayClasses()
+ {
+ $attributes = ['class' => ['existing-class-1', 'existing-class-2']];
+
+ CssClass::add($attributes, 'new-class');
+ $this->assertSame('existing-class-1 existing-class-2 new-class', $attributes['class']);
+
+ $attributes = ['class' => 'existing-class-1 existing-class-2'];
+
+ CssClass::add($attributes, 'new-class');
+ $this->assertEquals('existing-class-1 existing-class-2 new-class', $attributes['class']);
+ }
+
+ public function testAddWithDefaultValueAttributesIsArray(): void
+ {
+ $attributes = [];
+
+ CssClass::add($attributes, 'test-class');
+ $this->assertSame('test-class', $attributes['class']);
+
+ $attributes = [];
+
+ CssClass::add($attributes, ['test-class-1', 'test-class-2']);
+ $this->assertSame('test-class-1 test-class-2', $attributes['class']);
+ }
+
+ public function testAddDefaultValueAttributeExistClass(): void
+ {
+ $attributes = ['class' => 'existing-class'];
+
+ CssClass::add($attributes, 'new-class');
+ $this->assertEquals('existing-class new-class', $attributes['class']);
+ }
+
+ public function testAddWithString(): void
+ {
+ $attributes = [];
+
+ CssClass::add($attributes, '');
+ $this->assertEmpty($attributes);
+
+ CssClass::add($attributes, 'test-class');
+ $this->assertSame(['class' => 'test-class'], $attributes);
+
+ CssClass::add($attributes, 'test-class');
+ $this->assertSame(['class' => 'test-class'], $attributes);
+
+ CssClass::add($attributes, 'test-class-1');
+ $this->assertSame(['class' => 'test-class test-class-1'], $attributes);
+
+ CssClass::add($attributes, 'test-class test-class-1');
+ $this->assertSame(['class' => 'test-class test-class-1'], $attributes);
+
+ CssClass::add($attributes, 'test-class-2');
+ $this->assertSame(['class' => 'test-class test-class-1 test-class-2'], $attributes);
+
+ CssClass::add($attributes, 'test-override-class', true);
+ $this->assertSame(['class' => 'test-override-class'], $attributes);
+ }
+
+ public function testMergeMethodAssignToKey()
+ {
+ $existingClasses = ['existing-class-1', 'existing-class-2'];
+ $additionalClasses = ['keyed-class' => 'new-class'];
+
+ $merged = Assert::invokeMethod(new CssClass(), 'merge', [$existingClasses, $additionalClasses]);
+
+ $this->assertArrayHasKey('keyed-class', $merged);
+ $this->assertEquals('new-class', $merged['keyed-class']);
+ }
+}
diff --git a/tests/EncodeTest.php b/tests/EncodeTest.php
new file mode 100644
index 0000000..5298857
--- /dev/null
+++ b/tests/EncodeTest.php
@@ -0,0 +1,36 @@
+assertSame($expected, Encode::content($value));
+ $this->assertSame($expected, Encode::content($value, $doubleEncode));
+ }
+
+ /**
+ * @dataProvider UIAwesome\Html\Helper\Tests\Provider\EncodeProvider::encodeValue
+ *
+ * @param string $value Value to encode.
+ * @param string $expected Expected result.
+ * @param bool $doubleEncode Whether to encode HTML entities in `$value`.
+ */
+ public function testValue(string $value, string $expected, bool $doubleEncode): void
+ {
+ $this->assertSame($expected, Encode::value($value));
+ $this->assertSame($expected, Encode::value($value, $doubleEncode));
+ }
+}
diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php
deleted file mode 100644
index 4e7a602..0000000
--- a/tests/ExampleTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-assertTrue($example->getExample());
- }
-}
diff --git a/tests/Provider/AttributesProvider.php b/tests/Provider/AttributesProvider.php
new file mode 100644
index 0000000..5565925
--- /dev/null
+++ b/tests/Provider/AttributesProvider.php
@@ -0,0 +1,136 @@
+ true,
+ 'disabled' => true,
+ 'hidden' => false,
+ 'required' => 'yes',
+ ],
+ ],
+ [
+ ' class="first second"', [
+ 'class' => ['first', 'second'],
+ ]],
+ [
+ '', [
+ 'class' => [],
+ ]],
+ [
+ ' style="width: 100px; height: 200px;"', [
+ 'style' => [
+ 'width' => '100px',
+ 'height' => '200px',
+ ],
+ ]],
+ [
+ ' name="position" value="42"', [
+ 'value' => 42,
+ 'name' => 'position',
+ ]],
+ [
+ ' class="a b" id="x" data-a="1" data-b="2" style="width: 100px;" any=\'[1,2]\'',
+ [
+ 'id' => 'x',
+ 'class' => ['a', 'b'],
+ 'data' => [
+ 'a' => 1,
+ 'b' => 2,
+ ],
+ 'style' => [
+ 'width' => '100px',
+ ],
+ 'any' => [1, 2],
+ ],
+ ],
+ [
+ ' data-a="0" data-b=\'[1,2]\' any="42"',
+ [
+ 'class' => [],
+ 'style' => [],
+ 'data' => [
+ 'a' => 0,
+ 'b' => [1, 2],
+ ],
+ 'any' => 42,
+ ],
+ ],
+ [
+ ' data-foo=\'[]\'',
+ [
+ 'data' => [
+ 'foo' => [],
+ ],
+ ],
+ ],
+ [
+ ' src="xyz" data-a="1" data-b="c"',
+ [
+ 'src' => 'xyz',
+ 'data' => [
+ 'a' => 1,
+ 'b' => 'c',
+ ],
+ ],
+ ],
+ [
+ ' src="xyz" ng-a="1" ng-b="c"',
+ [
+ 'src' => 'xyz',
+ 'ng' => [
+ 'a' => 1,
+ 'b' => 'c',
+ ],
+ ],
+ ],
+ [
+ ' src="xyz" data-ng-a="1" data-ng-b="c"',
+ [
+ 'src' => 'xyz',
+ 'data-ng' => [
+ 'a' => 1,
+ 'b' => 'c',
+ ],
+ ],
+ ],
+ [
+ ' src="xyz" aria-a="1" aria-b="c"',
+ [
+ 'src' => 'xyz',
+ 'aria' => [
+ 'a' => 1,
+ 'b' => 'c',
+ ],
+ ],
+ ],
+ [
+ '',
+ [
+ 'disabled' => null,
+ ],
+ ],
+ [
+ '',
+ [
+ 'value' => '',
+ ],
+ ],
+ [
+ '',
+ [
+ '' => 'test-class',
+ ],
+ ],
+ ];
+ }
+}
diff --git a/tests/Provider/EncodeProvider.php b/tests/Provider/EncodeProvider.php
new file mode 100644
index 0000000..42523c4
--- /dev/null
+++ b/tests/Provider/EncodeProvider.php
@@ -0,0 +1,32 @@
+&\"'\x80", 'a<>&"'�', false],
+ ["a<>&\"'\x80", 'a<>&"'�', true],
+ ['Sam & Dark', 'Sam & Dark', false],
+ ['Sam & Dark', 'Sam & Dark', true],
+ ['\u{0000}', '\u{0000}', false],
+ ['\u{0000}', '\u{0000}', true],
+ ];
+ }
+
+ public static function encodeValue(): array
+ {
+ return [
+ ["a<>&\"'\x80", 'a<>&"'�', false],
+ ["a<>&\"'\x80", 'a<>&"'�', true],
+ ['Sam & Dark', 'Sam & Dark', false],
+ ['Sam & Dark', 'Sam & Dark', true],
+ ['\u{0000}', '', false],
+ ['\u{0000}', '', true],
+ ];
+ }
+}
diff --git a/tests/Provider/UtilsProvider.php b/tests/Provider/UtilsProvider.php
new file mode 100644
index 0000000..4c7a02d
--- /dev/null
+++ b/tests/Provider/UtilsProvider.php
@@ -0,0 +1,54 @@
+assertSame(
+ ['form', 'style'],
+ Assert::inaccessibleProperty(new Sanitize(), 'removeEvilAttributes')
+ );
+ $this->assertSame(
+ ['button', 'form', 'input', 'select', 'svg', 'textarea'],
+ Assert::inaccessibleProperty(new Sanitize(), 'removeEvilHtmlTags')
+ );
+ }
+
+ public function testHtml(): void
+ {
+ $this->assertSame(
+ 'click',
+ Sanitize::html('click')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html('')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html('')
+ );
+ $this->assertSame(
+ '
',
+ Sanitize::html('
')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html('')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html('')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html('')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html('')
+ );
+ $this->assertSame(
+ '',
+ Sanitize::html(
+ '',
+ '',
+ '
'
+ )
+ );
+ }
+
+ public function testHtmlWithRenderInterface(): void
+ {
+ $this->assertSame(
+ '',
+ Sanitize::html(new InputWidget())
+ );
+ }
+}
diff --git a/tests/Support/InputWidget.php b/tests/Support/InputWidget.php
new file mode 100644
index 0000000..31c9ed3
--- /dev/null
+++ b/tests/Support/InputWidget.php
@@ -0,0 +1,20 @@
+render();
+ }
+
+ public function render(): string
+ {
+ return '';
+ }
+}
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
new file mode 100644
index 0000000..968fe1c
--- /dev/null
+++ b/tests/UtilsTest.php
@@ -0,0 +1,119 @@
+assertSame($expected, Utils::convertToPattern($regexp, $delimiter));
+ }
+
+ /**
+ * @dataProvider UIAwesome\Html\Helper\Tests\Provider\UtilsProvider::convertToPatternInvalid
+ *
+ * @param string $regexp The regexp pattern to normalize.
+ * @param string $message The expected exception message.
+ * @param string|null $delimiter The delimiter to use.
+ */
+ public function testConvertToPatterInvalid(string $regexp, string $message, ?string $delimiter = null): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage($message);
+
+ Utils::convertToPattern($regexp, $delimiter);
+ }
+
+ public function testGenerateArrayableName(): void
+ {
+ $this->assertSame('test.name[]', Utils::generateArrayableName('test.name'));
+ }
+
+ public function testGenerateId(): void
+ {
+ $this->assertMatchesRegularExpression('/^id-[0-9a-f]{13}$/', Utils::generateId());
+ }
+
+ public function testGenerateIdWithPrefix(): void
+ {
+ $this->assertMatchesRegularExpression('/^prefix-[0-9a-f]{13}$/', Utils::generateId('prefix-'));
+ }
+
+ public function testGenerateInputId(): void
+ {
+ $this->assertSame('utilstest-string', Utils::generateInputId('UtilsTest', 'string'));
+ }
+
+ public function testGenerateInputName(): void
+ {
+ $this->assertSame('TestForm[content][body]', Utils::generateInputName('TestForm', 'content[body]'));
+ }
+
+ /**
+ * @dataProvider UIAwesome\Html\Helper\Tests\Provider\UtilsProvider::dataGetInputName
+ */
+ public function testGetInputNameDataProvider(string $formName, string $attribute, bool $arrayable, string $expected): void
+ {
+ $this->assertSame($expected, Utils::generateInputName($formName, $attribute, $arrayable));
+ }
+
+ public function testGetInputNameWithArrayableTrue(): void
+ {
+ $this->assertSame('TestForm[content][body][]', Utils::generateInputName('TestForm', 'content[body]', true));
+ }
+
+ public function testGetInputNamewithOnlyCharacters(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Property name must contain word characters only.');
+
+ Utils::generateInputName('TestForm', 'content body');
+ }
+
+ public function testGetInputNameExceptionWithTabular(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The form model name cannot be empty for tabular inputs.');
+
+ Utils::generateInputName('', '[0]dates[0]');
+ }
+
+ public function testGetShortNameClass(): void
+ {
+ $this->assertSame('UtilsTest::class', Utils::getShortNameClass(self::class));
+ }
+
+ public function testGetShortNameClassWithLowercase(): void
+ {
+ $this->assertSame('utilstest', Utils::getShortNameClass(self::class, false, true));
+ }
+
+ public function testGetShortNameClassWithoutSuffix(): void
+ {
+ $this->assertSame('UtilsTest', Utils::getShortNameClass(self::class, false));
+ }
+
+ public function testMultibyteGenerateArrayableName(): void
+ {
+ $this->assertSame('登录[]', Utils::generateArrayableName('登录'));
+ $this->assertSame('登录[]', Utils::generateArrayableName('登录[]'));
+ $this->assertSame('登录[0][]', Utils::generateArrayableName('登录[0]'));
+ $this->assertSame('[0]登录[0][]', Utils::generateArrayableName('[0]登录[0]'));
+ }
+
+ public function testMultibyteGenerateInputId(): void
+ {
+ $this->assertSame('testform-mąka', Utils::generateInputId('TestForm', 'mĄkA'));
+ }
+}
diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php
new file mode 100644
index 0000000..6881d6a
--- /dev/null
+++ b/tests/ValidatorTest.php
@@ -0,0 +1,93 @@
+expectNotToPerformAssertions();
+
+ Validator::inList('a', '', 'a', 'b', 'c');
+ }
+
+ public function testInListException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The value is not in the list: "1".');
+
+ Validator::inList('1', 'The value is not in the list: "%s".', 'a', 'b', 'c');
+ }
+
+ public function testInListExceptionWithEmptyValue(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The value must not be empty. The valid values are: "a", "b", "c".');
+
+ Validator::inList('', '', 'a', 'b', 'c');
+ }
+
+ public function testIterable(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ Validator::isIterable([]);
+ }
+
+ public function testIsIterableException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The value must be an iterable or null value. The value is: string.');
+
+ Validator::isIterable('value');
+ }
+
+ public function testIsNumeric(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ Validator::isNumeric(1);
+ }
+
+ public function testIsNumericException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The value must be a numeric or null value. The value is: string.');
+
+ Validator::isNumeric('value');
+ }
+
+ public function testIsScalar(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ Validator::isScalar(1);
+ }
+
+ public function testIsScalarException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The value must be a scalar or null value. The value is: array.');
+
+ Validator::isScalar([]);
+ }
+
+ public function testIsString(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ Validator::isString('value');
+ }
+
+ public function testIsStringException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The value must be a string or null value. The value is: array.');
+
+ Validator::isString([]);
+ }
+}
From 04c09e3b3624d6d6239ee95f414dde63e7c67089 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 5 Mar 2024 06:15:35 -0300
Subject: [PATCH 2/2] Update Codecov badge.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 726059a..e4554e2 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+