diff --git a/bin/create-mixin b/bin/create-mixin index 5828c81f8..e401c72f2 100755 --- a/bin/create-mixin +++ b/bin/create-mixin @@ -8,34 +8,31 @@ require __DIR__ . '/../vendor/autoload.php'; use Nette\PhpGenerator\InterfaceType; use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Printer; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Mixins\KeyChain; -use Respect\Validation\Mixins\LengthChain; -use Respect\Validation\Mixins\MaxChain; -use Respect\Validation\Mixins\MinChain; -use Respect\Validation\Mixins\NotChain; -use Respect\Validation\Mixins\NullOrChain; -use Respect\Validation\Mixins\PropertyChain; -use Respect\Validation\Mixins\UndefOrChain; use Respect\Validation\Mixins\Chain; use Respect\Validation\Mixins\KeyBuilder; +use Respect\Validation\Mixins\KeyChain; use Respect\Validation\Mixins\LengthBuilder; +use Respect\Validation\Mixins\LengthChain; use Respect\Validation\Mixins\MaxBuilder; +use Respect\Validation\Mixins\MaxChain; use Respect\Validation\Mixins\MinBuilder; +use Respect\Validation\Mixins\MinChain; use Respect\Validation\Mixins\NotBuilder; +use Respect\Validation\Mixins\NotChain; use Respect\Validation\Mixins\NullOrBuilder; +use Respect\Validation\Mixins\NullOrChain; use Respect\Validation\Mixins\PropertyBuilder; +use Respect\Validation\Mixins\PropertyChain; use Respect\Validation\Mixins\UndefOrBuilder; -use Respect\Validation\Rules\Undef; -use Respect\Validation\Rules\NullOr; -use Respect\Validation\Rules\UndefOr; +use Respect\Validation\Mixins\UndefOrChain; use Respect\Validation\Rule; +use Respect\Validation\Validator; function addMethodToInterface( string $originalName, InterfaceType $interfaceType, ReflectionClass $reflection, - ?string $prefix, + string|null $prefix, array $allowList, array $denyList, ): void { @@ -65,6 +62,7 @@ function addMethodToInterface( if ($reflrectionConstructor === null) { return; } + $commend = $reflrectionConstructor->getDocComment(); if ($commend) { $method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $commend)); @@ -83,13 +81,15 @@ function addMethodToInterface( } } elseif ($type instanceof ReflectionNamedType) { $types[] = $type->getName(); - if ( str_starts_with($type->getName(), 'Sokil') + if ( + str_starts_with($type->getName(), 'Sokil') || str_starts_with($type->getName(), 'Egulias') || $type->getName() === 'finfo' ) { continue; } } + $parameter = $method->addParameter($reflectionParameter->getName()); $parameter->setType(implode('|', $types)); @@ -121,7 +121,7 @@ function overwriteFile(string $content, string $basename): void { file_put_contents(sprintf('%s/../library/Mixins/%s.php', __DIR__, $basename), implode(PHP_EOL . PHP_EOL, [ 'isAbstract()) { continue; } + $names[$reflection->getShortName()] = $reflection; } + ksort($names); foreach ($mixins as [$name, $prefix, $allowList, $denyList]) { @@ -213,7 +215,7 @@ function overwriteFile(string $content, string $basename): void $chainedInterface->addExtend(NullOrChain::class); $chainedInterface->addExtend(PropertyChain::class); $chainedInterface->addExtend(UndefOrChain::class); - $chainedInterface->addComment('@mixin \\' . \Respect\Validation\Validator::class); + $chainedInterface->addComment('@mixin \\' . Validator::class); $staticInterface->addExtend(KeyBuilder::class); $staticInterface->addExtend(LengthBuilder::class); @@ -237,5 +239,5 @@ function overwriteFile(string $content, string $basename): void overwriteFile($printer->printNamespace($chainedNamespace), $chainedInterface->getName()); } - shell_exec(__DIR__.'/../vendor/bin/phpcbf'); + shell_exec(__DIR__ . '/../vendor/bin/phpcbf ' . __DIR__ . '/../library/Mixins'); })(); diff --git a/docs/09-list-of-rules-by-category.md b/docs/09-list-of-rules-by-category.md index 92c5176e6..ac3854bed 100644 --- a/docs/09-list-of-rules-by-category.md +++ b/docs/09-list-of-rules-by-category.md @@ -229,13 +229,13 @@ - [ContainsAny](rules/ContainsAny.md) - [Control](rules/Control.md) - [Digit](rules/Digit.md) +- [Emoji](rules/Emoji.md) - [EndsWith](rules/EndsWith.md) - [Graph](rules/Graph.md) - [HexRgbColor](rules/HexRgbColor.md) - [In](rules/In.md) - [Json](rules/Json.md) - [Lowercase](rules/Lowercase.md) -- [NotEmoji](rules/NotEmoji.md) - [Phone](rules/Phone.md) - [PhpLabel](rules/PhpLabel.md) - [PostalCode](rules/PostalCode.md) @@ -344,6 +344,7 @@ - [Domain](rules/Domain.md) - [Each](rules/Each.md) - [Email](rules/Email.md) +- [Emoji](rules/Emoji.md) - [EndsWith](rules/EndsWith.md) - [Equals](rules/Equals.md) - [Equivalent](rules/Equivalent.md) @@ -404,7 +405,6 @@ - [No](rules/No.md) - [NoneOf](rules/NoneOf.md) - [Not](rules/Not.md) -- [NotEmoji](rules/NotEmoji.md) - [NotEmpty](rules/NotEmpty.md) - [NullOr](rules/NullOr.md) - [NullType](rules/NullType.md) diff --git a/docs/rules/Alnum.md b/docs/rules/Alnum.md index a27894d9a..d68f4a738 100644 --- a/docs/rules/Alnum.md +++ b/docs/rules/Alnum.md @@ -70,8 +70,8 @@ See also: - [Control](Control.md) - [Decimal](Decimal.md) - [Digit](Digit.md) +- [Emoji](Emoji.md) - [Lowercase](Lowercase.md) -- [NotEmoji](NotEmoji.md) - [Regex](Regex.md) - [Spaced](Spaced.md) - [StringType](StringType.md) diff --git a/docs/rules/Alpha.md b/docs/rules/Alpha.md index 7211711d6..0de2187d4 100644 --- a/docs/rules/Alpha.md +++ b/docs/rules/Alpha.md @@ -64,8 +64,8 @@ See also: - [Consonant](Consonant.md) - [Decimal](Decimal.md) - [Digit](Digit.md) +- [Emoji](Emoji.md) - [Lowercase](Lowercase.md) -- [NotEmoji](NotEmoji.md) - [Regex](Regex.md) - [Spaced](Spaced.md) - [Uppercase](Uppercase.md) diff --git a/docs/rules/Decimal.md b/docs/rules/Decimal.md index 46ea45cbf..c7188bbac 100644 --- a/docs/rules/Decimal.md +++ b/docs/rules/Decimal.md @@ -55,12 +55,12 @@ See also: - [Alpha](Alpha.md) - [Consonant](Consonant.md) - [CreditCard](CreditCard.md) +- [Emoji](Emoji.md) - [Factor](Factor.md) - [Finite](Finite.md) - [Infinite](Infinite.md) - [IntType](IntType.md) - [IntVal](IntVal.md) -- [NotEmoji](NotEmoji.md) - [NumericVal](NumericVal.md) - [Regex](Regex.md) - [Uuid](Uuid.md) diff --git a/docs/rules/Digit.md b/docs/rules/Digit.md index 4635a0343..5568bea59 100644 --- a/docs/rules/Digit.md +++ b/docs/rules/Digit.md @@ -56,12 +56,12 @@ See also: - [Alpha](Alpha.md) - [Consonant](Consonant.md) - [CreditCard](CreditCard.md) +- [Emoji](Emoji.md) - [Factor](Factor.md) - [Finite](Finite.md) - [Infinite](Infinite.md) - [IntType](IntType.md) - [IntVal](IntVal.md) -- [NotEmoji](NotEmoji.md) - [NumericVal](NumericVal.md) - [Regex](Regex.md) - [Uuid](Uuid.md) diff --git a/docs/rules/Emoji.md b/docs/rules/Emoji.md new file mode 100644 index 000000000..99a34853a --- /dev/null +++ b/docs/rules/Emoji.md @@ -0,0 +1,63 @@ +# Emoji + +- `v::emoji()` + +Validates if the input is an emoji or a sequence of emojis. + +```php +v::emoji()->isValid('๐Ÿ•'); // true +v::emoji()->isValid('๐ŸŽˆ'); // true +v::emoji()->isValid('โšก'); // true +v::emoji()->isValid('๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐Ÿ„๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐Ÿ–๐ŸŒด'); // true +v::emoji()->isValid('๐Ÿ‡ง๐Ÿ‡ท'); // true (country flag) +v::emoji()->isValid('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'); // true (ZWJ sequence) +v::emoji()->isValid('๐Ÿ‘ฉ๐Ÿฝ'); // true (skin tone modifier) +v::emoji()->isValid('1๏ธโƒฃ'); // true (keycap sequence) +v::emoji()->isValid('Hello World'); // false +v::emoji()->isValid('this is a spark โšก'); // false (mixed content) +``` + +This validator supports: + +- Basic emojis and pictographs +- Skin tone modifiers (Fitzpatrick scale) +- Country flags (regional indicator sequences) +- Subdivision flags (tag sequences like ๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ) +- Keycap sequences (0๏ธโƒฃ-9๏ธโƒฃ, #๏ธโƒฃ, \*๏ธโƒฃ) +- ZWJ (Zero Width Joiner) sequences for families, professions, and combined emojis +- Emojis up to Unicode 17.0 / Emoji 16.0 + +## Templates + +### `Emoji::TEMPLATE_STANDARD` + +| Mode | Template | +| ---------- | -------------------------------- | +| `default` | {{subject}} must be an emoji | +| `inverted` | {{subject}} must not be an emoji | + +## Template placeholders + +| Placeholder | Description | +| ----------- | ---------------------------------------------------------------- | +| `subject` | The validated input or the custom validator name (if specified). | + +## Categorization + +- Strings + +## Changelog + +| Version | Description | +| ------: | --------------------------------------------------------------------------- | +| 3.0.0 | Renamed to `Emoji`, changed the behavior, and added support for more emojis | +| 2.0.0 | Created as `NotEmoji` | + +--- + +See also: + +- [Alnum](Alnum.md) +- [Alpha](Alpha.md) +- [Decimal](Decimal.md) +- [Digit](Digit.md) diff --git a/docs/rules/NotEmoji.md b/docs/rules/NotEmoji.md deleted file mode 100644 index 004a19370..000000000 --- a/docs/rules/NotEmoji.md +++ /dev/null @@ -1,57 +0,0 @@ -# NotEmoji - -- `v::notEmoji()` - -Validates if the input does not contain an emoji. - -```php -v::notEmoji()->isValid('Hello World, without emoji'); // true -v::notEmoji()->isValid('๐Ÿ•'); // false -v::notEmoji()->isValid('๐ŸŽˆ'); // false -v::notEmoji()->isValid('โšก'); // false -v::notEmoji()->isValid('this is a spark โšก'); // false -v::notEmoji()->isValid('๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐Ÿ„๐ŸŒŠ๐ŸŒŠ๐ŸŒŠ๐Ÿ–๐ŸŒด'); // false -``` - -Please consider that the performance of this validator is linear which -means the longer the text the longer it takes to perform the check. -However, the validator will break the execution as soon as it finds the -first emoji or until it checks the whole text. - -_Note: this validator will check the Emoji as they are defined in -Unicode V11 check the following link for more details -[Unicode v11](https://unicode.org/emoji/charts/full-emoji-list.html)_ - -## Templates - -### `NotEmoji::TEMPLATE_STANDARD` - -| Mode | Template | -| ---------- | ------------------------------------- | -| `default` | {{subject}} must not contain an emoji | -| `inverted` | {{subject}} must contain an emoji | - -## Template placeholders - -| Placeholder | Description | -| ----------- | ---------------------------------------------------------------- | -| `subject` | The validated input or the custom validator name (if specified). | - -## Categorization - -- Strings - -## Changelog - -| Version | Description | -| ------: | ----------- | -| 2.0.0 | Created | - ---- - -See also: - -- [Alnum](Alnum.md) -- [Alpha](Alpha.md) -- [Decimal](Decimal.md) -- [Digit](Digit.md) diff --git a/library/Mixins/Builder.php b/library/Mixins/Builder.php index a82b54477..6d51c4c34 100644 --- a/library/Mixins/Builder.php +++ b/library/Mixins/Builder.php @@ -242,7 +242,7 @@ public static function noneOf(Rule $rule1, Rule $rule2, Rule ...$rules): Chain; public static function not(Rule $rule): Chain; - public static function notEmoji(): Chain; + public static function emoji(): Chain; public static function notEmpty(): Chain; diff --git a/library/Mixins/Chain.php b/library/Mixins/Chain.php index 3d46de119..2e8793ef6 100644 --- a/library/Mixins/Chain.php +++ b/library/Mixins/Chain.php @@ -245,7 +245,7 @@ public function noneOf(Rule $rule1, Rule $rule2, Rule ...$rules): Chain; public function not(Rule $rule): Chain; - public function notEmoji(): Chain; + public function emoji(): Chain; public function notEmpty(): Chain; diff --git a/library/Mixins/KeyBuilder.php b/library/Mixins/KeyBuilder.php index 8bbf58482..2900c3183 100644 --- a/library/Mixins/KeyBuilder.php +++ b/library/Mixins/KeyBuilder.php @@ -221,7 +221,7 @@ public static function keyNoneOf(int|string $key, Rule $rule1, Rule $rule2, Rule public static function keyNot(int|string $key, Rule $rule): Chain; - public static function keyNotEmoji(int|string $key): Chain; + public static function keyEmoji(int|string $key): Chain; public static function keyNotEmpty(int|string $key): Chain; diff --git a/library/Mixins/KeyChain.php b/library/Mixins/KeyChain.php index 0dfeb63d3..89dae962f 100644 --- a/library/Mixins/KeyChain.php +++ b/library/Mixins/KeyChain.php @@ -221,7 +221,7 @@ public function keyNoneOf(int|string $key, Rule $rule1, Rule $rule2, Rule ...$ru public function keyNot(int|string $key, Rule $rule): Chain; - public function keyNotEmoji(int|string $key): Chain; + public function keyEmoji(int|string $key): Chain; public function keyNotEmpty(int|string $key): Chain; diff --git a/library/Mixins/NullOrBuilder.php b/library/Mixins/NullOrBuilder.php index 5fbc94e36..b5452cf84 100644 --- a/library/Mixins/NullOrBuilder.php +++ b/library/Mixins/NullOrBuilder.php @@ -229,7 +229,7 @@ public static function nullOrNoneOf(Rule $rule1, Rule $rule2, Rule ...$rules): C public static function nullOrNot(Rule $rule): Chain; - public static function nullOrNotEmoji(): Chain; + public static function nullOrEmoji(): Chain; public static function nullOrNotEmpty(): Chain; diff --git a/library/Mixins/NullOrChain.php b/library/Mixins/NullOrChain.php index 6af09ad42..e9e956c15 100644 --- a/library/Mixins/NullOrChain.php +++ b/library/Mixins/NullOrChain.php @@ -229,7 +229,7 @@ public function nullOrNoneOf(Rule $rule1, Rule $rule2, Rule ...$rules): Chain; public function nullOrNot(Rule $rule): Chain; - public function nullOrNotEmoji(): Chain; + public function nullOrEmoji(): Chain; public function nullOrNotEmpty(): Chain; diff --git a/library/Mixins/PropertyBuilder.php b/library/Mixins/PropertyBuilder.php index d9e84b33a..61f806a0c 100644 --- a/library/Mixins/PropertyBuilder.php +++ b/library/Mixins/PropertyBuilder.php @@ -225,7 +225,7 @@ public static function propertyNoneOf(string $propertyName, Rule $rule1, Rule $r public static function propertyNot(string $propertyName, Rule $rule): Chain; - public static function propertyNotEmoji(string $propertyName): Chain; + public static function propertyEmoji(string $propertyName): Chain; public static function propertyNotEmpty(string $propertyName): Chain; diff --git a/library/Mixins/PropertyChain.php b/library/Mixins/PropertyChain.php index 573bb4000..99e350009 100644 --- a/library/Mixins/PropertyChain.php +++ b/library/Mixins/PropertyChain.php @@ -221,7 +221,7 @@ public function propertyNoneOf(string $propertyName, Rule $rule1, Rule $rule2, R public function propertyNot(string $propertyName, Rule $rule): Chain; - public function propertyNotEmoji(string $propertyName): Chain; + public function propertyEmoji(string $propertyName): Chain; public function propertyNotEmpty(string $propertyName): Chain; diff --git a/library/Mixins/UndefOrBuilder.php b/library/Mixins/UndefOrBuilder.php index 9be6f64e9..3975116ca 100644 --- a/library/Mixins/UndefOrBuilder.php +++ b/library/Mixins/UndefOrBuilder.php @@ -227,7 +227,7 @@ public static function undefOrNoneOf(Rule $rule1, Rule $rule2, Rule ...$rules): public static function undefOrNot(Rule $rule): Chain; - public static function undefOrNotEmoji(): Chain; + public static function undefOrEmoji(): Chain; public static function undefOrNotEmpty(): Chain; diff --git a/library/Mixins/UndefOrChain.php b/library/Mixins/UndefOrChain.php index f93a300a8..0c15cc7db 100644 --- a/library/Mixins/UndefOrChain.php +++ b/library/Mixins/UndefOrChain.php @@ -227,7 +227,7 @@ public function undefOrNoneOf(Rule $rule1, Rule $rule2, Rule ...$rules): Chain; public function undefOrNot(Rule $rule): Chain; - public function undefOrNotEmoji(): Chain; + public function undefOrEmoji(): Chain; public function undefOrNotEmpty(): Chain; diff --git a/library/Rules/Emoji.php b/library/Rules/Emoji.php new file mode 100644 index 000000000..db837c4bc --- /dev/null +++ b/library/Rules/Emoji.php @@ -0,0 +1,62 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules; + +use Attribute; +use Respect\Validation\Message\Template; +use Respect\Validation\Rules\Core\Simple; + +use function is_string; +use function preg_match; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +#[Template( + '{{subject}} must be an emoji', + '{{subject}} must not be an emoji', +)] +final class Emoji extends Simple +{ + private const string REGEX = <<<'REGEX' + (?: + # Tag sequence for country flags + [\x{1F1E6}-\x{1F1FF}]{2} + | + # Tag sequence for subdivision flags + \x{1F3F4}[\x{E0020}-\x{E007E}]+\x{E007F} + | + # Keycap sequences + [0-9#*](?:\x{FE0F})?\x{20E3} + | + # Standard emoji cluster: + \p{Extended_Pictographic} # base emoji + (?:\x{FE0F})? # optional emoji variant selector + (?:[\x{1F3FB}-\x{1F3FF}])? # optional skin tone modifier + (?: # optionally repeat ZWJ sequences: + \x{200D} # ZWJ + (?: # joined element: + \p{Extended_Pictographic} # another emoji + | + [\x{2640}\x{2642}\x{26A7}] # or gender symbol + ) + (?:\x{FE0F})? # optional variant selector + (?:[\x{1F3FB}-\x{1F3FF}])? # optional skin tone modifier + )* # ...zero or more times + ) + REGEX; + + public function isValid(mixed $input): bool + { + if (!is_string($input)) { + return false; + } + + return preg_match('/^' . self::REGEX . '+$/ux', $input) === 1; + } +} diff --git a/library/Rules/NotEmoji.php b/library/Rules/NotEmoji.php deleted file mode 100644 index fa5a2e65d..000000000 --- a/library/Rules/NotEmoji.php +++ /dev/null @@ -1,207 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Rules; - -use Attribute; -use Respect\Validation\Message\Template; -use Respect\Validation\Rules\Core\Simple; - -use function implode; -use function is_string; -use function preg_match; - -#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -#[Template( - '{{subject}} must not contain an emoji', - '{{subject}} must contain an emoji', -)] -final class NotEmoji extends Simple -{ - private const array RANGES = [ - '\x{0023}\x{FE0F}\x{20E3}', - '\x{0023}\x{20E3}', - '\x{002A}\x{FE0F}\x{20E3}', - '\x{002A}\x{20E3}', - '\x{0030}\x{FE0F}\x{20E3}', - '\x{0030}\x{20E3}', - '\x{0031}\x{FE0F}\x{20E3}', - '\x{0031}\x{20E3}', - '\x{0032}\x{FE0F}\x{20E3}', - '\x{0032}\x{20E3}', - '\x{0033}\x{FE0F}\x{20E3}', - '\x{0033}\x{20E3}', - '\x{0034}\x{FE0F}\x{20E3}', - '\x{0034}\x{20E3}', - '\x{0035}\x{FE0F}\x{20E3}', - '\x{0035}\x{20E3}', - '\x{0036}\x{FE0F}\x{20E3}', - '\x{0036}\x{20E3}', - '\x{0037}\x{FE0F}\x{20E3}', - '\x{0037}\x{20E3}', - '\x{0038}\x{FE0F}\x{20E3}', - '\x{0038}\x{20E3}', - '\x{0039}\x{FE0F}\x{20E3}', - '\x{0039}\x{20E3}', - '\x{1F004}', - '\x{1F0CF}', - '[\x{1F170}-\x{1F171}]', - '[\x{1F17E}-\x{1F17F}]', - '\x{1F18E}', - '[\x{1F191}-\x{1F19A}]', - '[\x{1F1E6}-\x{1F1FF}]', - '[\x{1F201}-\x{1F202}]', - '\x{1F21A}', - '\x{1F22F}', - '[\x{1F232}-\x{1F23A}]', - '[\x{1F250}-\x{1F251}]', - '[\x{1F300}-\x{1F321}]', - '[\x{1F324}-\x{1F393}]', - '[\x{1F396}-\x{1F397}]', - '[\x{1F399}-\x{1F39B}]', - '[\x{1F39E}-\x{1F3F0}]', - '[\x{1F3F3}-\x{1F3F5}]', - '[\x{1F3F7}-\x{1F4FD}]', - '[\x{1F4FF}-\x{1F53D}]', - '[\x{1F549}-\x{1F54E}]', - '[\x{1F550}-\x{1F567}]', - '[\x{1F56F}-\x{1F570}]', - '[\x{1F573}-\x{1F57A}]', - '\x{1F587}', - '[\x{1F58A}-\x{1F58D}]', - '\x{1F590}', - '[\x{1F595}-\x{1F596}]', - '[\x{1F5A4}-\x{1F5A5}]', - '\x{1F5A8}', - '[\x{1F5B1}-\x{1F5B2}]', - '\x{1F5BC}', - '[\x{1F5C2}-\x{1F5C4}]', - '[\x{1F5D1}-\x{1F5D3}]', - '[\x{1F5DC}-\x{1F5DE}]', - '\x{1F5E1}', - '\x{1F5E3}', - '\x{1F5E8}', - '\x{1F5EF}', - '\x{1F5F3}', - '[\x{1F5FA}-\x{1F64F}]', - '[\x{1F680}-\x{1F6C5}]', - '[\x{1F6CB}-\x{1F6D2}]', - '[\x{1F6E0}-\x{1F6E5}]', - '\x{1F6E9}', - '[\x{1F6EB}-\x{1F6EC}]', - '\x{1F6F0}', - '[\x{1F6F3}-\x{1F6F9}]', - '[\x{1F910}-\x{1F93A}]', - '[\x{1F93C}-\x{1F93E}]', - '[\x{1F940}-\x{1F945}]', - '[\x{1F947}-\x{1F970}]', - '[\x{1F973}-\x{1F976}]', - '\x{1F97A}', - '[\x{1F97C}-\x{1F9A2}]', - '[\x{1F9B0}-\x{1F9B9}]', - '[\x{1F9C0}-\x{1F9C2}]', - '[\x{1F9D0}-\x{1F9FF}]', - '\x{00A9}', - '\x{00AE}', - '\x{203C}', - '\x{2049}', - '\x{2122}', - '\x{2139}', - '[\x{2194}-\x{2199}]', - '[\x{21A9}-\x{21AA}]', - '[\x{231A}-\x{231B}]', - '\x{2328}', - '\x{23CF}', - '[\x{23E9}-\x{23F3}]', - '[\x{23F8}-\x{23FA}]', - '\x{24C2}', - '[\x{25AA}-\x{25AB}]', - '\x{25B6}', - '\x{25C0}', - '[\x{25FB}-\x{25FE}]', - '[\x{2600}-\x{2604}]', - '\x{260E}', - '\x{2611}', - '[\x{2614}-\x{2615}]', - '\x{2618}', - '\x{261D}', - '\x{2620}', - '[\x{2622}-\x{2623}]', - '\x{2626}', - '\x{262A}', - '[\x{262E}-\x{262F}]', - '[\x{2638}-\x{263A}]', - '\x{2640}', - '\x{2642}', - '[\x{2648}-\x{2653}]', - '[\x{265F}-\x{2660}]', - '\x{2663}', - '[\x{2665}-\x{2666}]', - '\x{2668}', - '\x{267B}', - '[\x{267E}-\x{267F}]', - '[\x{2692}-\x{2697}]', - '\x{2699}', - '[\x{269B}-\x{269C}]', - '[\x{26A0}-\x{26A1}]', - '[\x{26AA}-\x{26AB}]', - '[\x{26B0}-\x{26B1}]', - '[\x{26BD}-\x{26BE}]', - '[\x{26C4}-\x{26C5}]', - '\x{26C8}', - '[\x{26CE}-\x{26CF}]', - '\x{26D1}', - '[\x{26D3}-\x{26D4}]', - '\x{26EA}', - '[\x{26F0}-\x{26F5}]', - '[\x{26F7}-\x{26FA}]', - '\x{26FD}', - '\x{2702}', - '\x{2705}', - '[\x{2708}-\x{270D}]', - '\x{270F}', - '\x{2712}', - '\x{2714}', - '\x{2716}', - '\x{271D}', - '\x{2721}', - '\x{2728}', - '[\x{2733}-\x{2734}]', - '\x{2744}', - '\x{2747}', - '\x{26E9}', - '\x{274C}', - '\x{274E}', - '[\x{2753}-\x{2755}]', - '\x{2757}', - '[\x{2763}-\x{2764}]', - '[\x{2795}-\x{2797}]', - '\x{27A1}', - '\x{27B0}', - '\x{27BF}', - '[\x{2934}-\x{2935}]', - '[\x{2B05}-\x{2B07}]', - '[\x{2B1B}-\x{2B1C}]', - '\x{2B50}', - '\x{2B55}', - '\x{3030}', - '\x{303D}', - '\x{3297}', - '\x{3299}', - ]; - - public function isValid(mixed $input): bool - { - if (!is_string($input)) { - return false; - } - - return preg_match('/' . implode('|', self::RANGES) . '/mu', $input) === 0; - } -} diff --git a/library/Transformers/Prefix.php b/library/Transformers/Prefix.php index c22c8b1c7..2f32c8e7c 100644 --- a/library/Transformers/Prefix.php +++ b/library/Transformers/Prefix.php @@ -27,7 +27,7 @@ final class Prefix implements Transformer 'min', 'minAge', 'not', - 'notEmoji', + 'emoji', 'notEmpty', 'undef', 'nullOr', diff --git a/tests/feature/Rules/EmojiTest.php b/tests/feature/Rules/EmojiTest.php new file mode 100644 index 000000000..6cabb6498 --- /dev/null +++ b/tests/feature/Rules/EmojiTest.php @@ -0,0 +1,24 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +test('default template', catchAll( + fn() => v::emoji()->assert('โ˜Ž๏ธŽ'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"โ˜Ž๏ธŽ" must be an emoji') + ->and($fullMessage)->toBe('- "โ˜Ž๏ธŽ" must be an emoji') + ->and($messages)->toBe(['emoji' => '"โ˜Ž๏ธŽ" must be an emoji']), +)); + +test('inverted template', catchAll( + fn() => v::not(v::emoji())->assert('๐Ÿผ'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"๐Ÿผ" must not be an emoji') + ->and($fullMessage)->toBe('- "๐Ÿผ" must not be an emoji') + ->and($messages)->toBe(['notEmoji' => '"๐Ÿผ" must not be an emoji']), +)); diff --git a/tests/feature/Rules/NotEmojiTest.php b/tests/feature/Rules/NotEmojiTest.php deleted file mode 100644 index b314e6b71..000000000 --- a/tests/feature/Rules/NotEmojiTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -test('Scenario #1', catchMessage( - fn() => v::notEmoji()->assert('๐Ÿ•'), - fn(string $message) => expect($message)->toBe('"๐Ÿ•" must not contain an emoji'), -)); - -test('Scenario #2', catchMessage( - fn() => v::not(v::notEmoji())->assert('AB'), - fn(string $message) => expect($message)->toBe('"AB" must contain an emoji'), -)); - -test('Scenario #3', catchFullMessage( - fn() => v::notEmoji()->assert('๐Ÿ„'), - fn(string $fullMessage) => expect($fullMessage)->toBe('- "๐Ÿ„" must not contain an emoji'), -)); - -test('Scenario #4', catchFullMessage( - fn() => v::not(v::notEmoji())->assert('YZ'), - fn(string $fullMessage) => expect($fullMessage)->toBe('- "YZ" must contain an emoji'), -)); diff --git a/tests/unit/Rules/EmojiTest.php b/tests/unit/Rules/EmojiTest.php new file mode 100644 index 000000000..fdb98535a --- /dev/null +++ b/tests/unit/Rules/EmojiTest.php @@ -0,0 +1,157 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use Respect\Validation\Test\RuleTestCase; +use stdClass; + +#[Group('rule')] +#[CoversClass(Emoji::class)] +final class EmojiTest extends RuleTestCase +{ + /** @return iterable */ + public static function providerForInvalidInput(): iterable + { + $sut = new Emoji(); + + return [ + 'Numbers' => [$sut, '0123456789'], + 'Alpha' => [$sut, 'ABCDEFGHIKLMNOPQRSTVXYZabcdefghiklmnopqrstvxyz'], + 'Symbols' => [$sut, '&"\'(-_)@-*/+.'], + 'Unicode symbols' => [$sut, 'รงร รฉรจโŠวทรžรร†'], + 'Arabic' => [$sut, 'ุถุตุซู‚ูุบุนู‡ุฎุญุฌุดุณูŠุจู„ุงุชู†ู…ูƒุทุฆุกุคุฑู„ุงู‰ุฉูˆุฒุธุฐ'], + 'Russian' => [$sut, 'ั€ัƒััะบะธะน'], + 'Japanese' => [$sut, 'ใ‚ใฌใตใ‚ใ†ใˆใŠใ‚„ใ‚†ใ‚ˆใ‚ใ‚›ใธใกใคใ„ใ™ใ‹ใ‚“ใชใซใ‚‰ใ›ใ‚Œใ›ใ‚œใŸqใจใ—ใฏใใใพใฎใ‚Šใ‚‚ใ‚ใ‚€ใฆใ•ใใฒใ“ใฟใญใ‚‹ใ‚!'], + 'Mixed with text' => [$sut, 'this is a pizza ๐Ÿ•'], + 'Array' => [$sut, []], + 'Bool' => [$sut, true], + 'Object' => [$sut, new stdClass()], + ]; + } + + /** @return iterable */ + public static function providerForValidInput(): iterable + { + $sut = new Emoji(); + + return [ + // Basic categories + 'Smileys & People' => [$sut, '๐Ÿคฃ'], + 'Animals & Nature' => [$sut, '๐Ÿต'], + 'Food & Drink' => [$sut, '๐ŸŽ'], + 'Travel & Places' => [$sut, 'โ›ฐ๏ธ'], + 'Activities' => [$sut, '๐ŸŽˆ'], + 'Objects' => [$sut, '๐Ÿ“ข'], + + // Skin tone modifiers + 'Backhand Index Pointing Right with modifier' => [$sut, '๐Ÿ‘‰๐Ÿฟ'], + 'Santa Claus with modifier' => [$sut, '๐ŸŽ…๐Ÿพ'], + 'Man Frowning with modifier' => [$sut, '๐Ÿ™๐Ÿปโ€โ™‚๏ธ'], + 'Woman with skin tone' => [$sut, '๐Ÿ‘ฉ๐Ÿฝ'], + + // Symbols from various Unicode versions + 'Symbols from Unicode 4.0' => [$sut, 'โš ๏ธ'], + 'Symbols from Unicode 6.0' => [$sut, 'โœ…'], + 'Symbols from Unicode 7.0' => [$sut, 'โบ๏ธ'], + + // Flags + 'Flags Emoji 1.0' => [$sut, '๐Ÿ‡น๐Ÿ‡ณ'], + 'Flags Emoji 4.0' => [$sut, '๐Ÿณ๏ธโ€๐ŸŒˆ'], + 'Flags Emoji 5.0' => [$sut, '๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ'], + 'Flags Emoji 11.0' => [$sut, '๐Ÿดโ€โ˜ ๏ธ'], + 'Country flag USA' => [$sut, '๐Ÿ‡บ๐Ÿ‡ธ'], + 'Country flag Japan' => [$sut, '๐Ÿ‡ฏ๐Ÿ‡ต'], + 'Scotland subdivision flag' => [$sut, '๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ'], + 'Wales subdivision flag' => [$sut, '๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ'], + + // Keycap sequences + 'Keycap digit one' => [$sut, '1๏ธโƒฃ'], + 'Keycap digit zero' => [$sut, '0๏ธโƒฃ'], + 'Keycap hash' => [$sut, '#๏ธโƒฃ'], + 'Keycap asterisk' => [$sut, '*๏ธโƒฃ'], + + // ZWJ sequences - families + 'Family man woman girl boy' => [$sut, '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'], + 'Couple with heart' => [$sut, '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ'], + 'Kiss' => [$sut, '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ'], + + // ZWJ sequences - professions + 'Woman health worker' => [$sut, '๐Ÿ‘ฉโ€โš•๏ธ'], + 'Man technologist' => [$sut, '๐Ÿ‘จโ€๐Ÿ’ป'], + 'Woman firefighter' => [$sut, '๐Ÿ‘ฉโ€๐Ÿš’'], + + // Emoji 13.0 (2020) + 'Emoji 13.0 Smiling Face with Tear' => [$sut, '๐Ÿฅฒ'], + 'Emoji 13.0 Ninja' => [$sut, '๐Ÿฅท'], + 'Emoji 13.0 Anatomical Heart' => [$sut, '๐Ÿซ€'], + 'Emoji 13.0 Lungs' => [$sut, '๐Ÿซ'], + 'Emoji 13.0 Pinched Fingers' => [$sut, '๐ŸคŒ'], + 'Emoji 13.0 Beaver' => [$sut, '๐Ÿฆซ'], + 'Emoji 13.0 Polar Bear' => [$sut, '๐Ÿปโ€โ„๏ธ'], + + // Emoji 13.1 (2021) + 'Emoji 13.1 Heart on Fire' => [$sut, 'โค๏ธโ€๐Ÿ”ฅ'], + 'Emoji 13.1 Mending Heart' => [$sut, 'โค๏ธโ€๐Ÿฉน'], + 'Emoji 13.1 Face Exhaling' => [$sut, '๐Ÿ˜ฎโ€๐Ÿ’จ'], + 'Emoji 13.1 Face in Clouds' => [$sut, '๐Ÿ˜ถโ€๐ŸŒซ๏ธ'], + 'Emoji 13.1 Woman with Beard' => [$sut, '๐Ÿง”โ€โ™€๏ธ'], + + // Emoji 14.0 (2021) + 'Emoji 14.0 Melting Face' => [$sut, '๐Ÿซ '], + 'Emoji 14.0 Saluting Face' => [$sut, '๐Ÿซก'], + 'Emoji 14.0 Face with Open Eyes and Hand Over Mouth' => [$sut, '๐Ÿซข'], + 'Emoji 14.0 Face with Peeking Eye' => [$sut, '๐Ÿซฃ'], + 'Emoji 14.0 Dotted Line Face' => [$sut, '๐Ÿซฅ'], + 'Emoji 14.0 Biting Lip' => [$sut, '๐Ÿซฆ'], + 'Emoji 14.0 Coral' => [$sut, '๐Ÿชธ'], + 'Emoji 14.0 Lotus' => [$sut, '๐Ÿชท'], + + // Emoji 15.0 (2022) + 'Emoji 15.0 Shaking Face' => [$sut, '๐Ÿซจ'], + 'Emoji 15.0 Pink Heart' => [$sut, '๐Ÿฉท'], + 'Emoji 15.0 Light Blue Heart' => [$sut, '๐Ÿฉต'], + 'Emoji 15.0 Grey Heart' => [$sut, '๐Ÿฉถ'], + 'Emoji 15.0 Moose' => [$sut, '๐ŸซŽ'], + 'Emoji 15.0 Donkey' => [$sut, '๐Ÿซ'], + 'Emoji 15.0 Wing' => [$sut, '๐Ÿชฝ'], + 'Emoji 15.0 Goose' => [$sut, '๐Ÿชฟ'], + 'Emoji 15.0 Jellyfish' => [$sut, '๐Ÿชผ'], + 'Emoji 15.0 Hyacinth' => [$sut, '๐Ÿชป'], + 'Emoji 15.0 Pea Pod' => [$sut, '๐Ÿซ›'], + 'Emoji 15.0 Folding Hand Fan' => [$sut, '๐Ÿชญ'], + 'Emoji 15.0 Hair Pick' => [$sut, '๐Ÿชฎ'], + 'Emoji 15.0 Maracas' => [$sut, '๐Ÿช‡'], + 'Emoji 15.0 Flute' => [$sut, '๐Ÿชˆ'], + 'Emoji 15.0 Khanda' => [$sut, '๐Ÿชฏ'], + + // Emoji 15.1 (2023) + 'Emoji 15.1 Head Shaking Horizontally' => [$sut, '๐Ÿ™‚โ€โ†”๏ธ'], + 'Emoji 15.1 Head Shaking Vertically' => [$sut, '๐Ÿ™‚โ€โ†•๏ธ'], + 'Emoji 15.1 Phoenix' => [$sut, '๐Ÿฆโ€๐Ÿ”ฅ'], + 'Emoji 15.1 Lime' => [$sut, '๐Ÿ‹โ€๐ŸŸฉ'], + 'Emoji 15.1 Brown Mushroom' => [$sut, '๐Ÿ„โ€๐ŸŸซ'], + + // Emoji 16.0 (2024) + 'Emoji 16.0 Face with Bags Under Eyes' => [$sut, '๐ŸซŸ'], + 'Emoji 16.0 Fingerprint' => [$sut, '๐Ÿชฌ'], + 'Emoji 16.0 Leafless Tree' => [$sut, '๐Ÿชพ'], + 'Emoji 16.0 Root Vegetable' => [$sut, '๐Ÿซœ'], + 'Emoji 16.0 Harp' => [$sut, '๐Ÿช‰'], + 'Emoji 16.0 Shovel' => [$sut, '๐Ÿช'], + + // Multiple emojis in sequence + 'Multiple basic emojis' => [$sut, '๐Ÿ˜€๐Ÿ˜ƒ๐Ÿ˜„'], + 'Multiple flags' => [$sut, '๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡ฌ๐Ÿ‡ง๐Ÿ‡ฏ๐Ÿ‡ต'], + 'Multiple keycaps' => [$sut, '1๏ธโƒฃ2๏ธโƒฃ3๏ธโƒฃ'], + ]; + } +} diff --git a/tests/unit/Rules/NotEmojiTest.php b/tests/unit/Rules/NotEmojiTest.php deleted file mode 100644 index 3cb501c80..000000000 --- a/tests/unit/Rules/NotEmojiTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Rules; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Group; -use Respect\Validation\Test\RuleTestCase; -use stdClass; - -#[Group('rule')] -#[CoversClass(NotEmoji::class)] -final class NotEmojiTest extends RuleTestCase -{ - /** @return iterable */ - public static function providerForValidInput(): iterable - { - $sut = new NotEmoji(); - - return [ - 'Numbers' => [$sut, '0123456789'], - 'Alpha' => [$sut, 'ABCDEFGHIKLMNOPQRSTVXYZabcdefghiklmnopqrstvxyz'], - 'Symbols' => [$sut, '&"\'(-_)@-*/+.'], - 'Unicode symbols' => [$sut, 'รงร รฉรจโŠวทรžรร†'], - 'Arabic' => [$sut, 'ุถุตุซู‚ูุบุนู‡ุฎุญุฌุดุณูŠุจู„ุงุชู†ู…ูƒุทุฆุกุคุฑู„ุงู‰ุฉูˆุฒุธุฐ'], - 'Russian' => [$sut, 'ั€ัƒััะบะธะน'], - 'Japanese' => [$sut, 'ใ‚ใฌใตใ‚ใ†ใˆใŠใ‚„ใ‚†ใ‚ˆใ‚ใ‚›ใธใกใคใ„ใ™ใ‹ใ‚“ใชใซใ‚‰ใ›ใ‚Œใ›ใ‚œใŸqใจใ—ใฏใใใพใฎใ‚Šใ‚‚ใ‚ใ‚€ใฆใ•ใใฒใ“ใฟใญใ‚‹ใ‚!'], - ]; - } - - /** @return iterable */ - public static function providerForInvalidInput(): iterable - { - $sut = new NotEmoji(); - - return [ - 'Smileys & People' => [$sut, '๐Ÿคฃ'], - 'Backhand Index Pointing Right with modifier' => [$sut, '๐Ÿ‘‰๐Ÿฟ'], - 'Santa Claus with modifier' => [$sut, '๐ŸŽ…๐Ÿพ'], - 'Man Frowning with modifier' => [$sut, '๐Ÿ™๐Ÿปโ€โ™‚๏ธ'], - 'Animals & Nature' => [$sut, '๐Ÿต'], - 'Food & Drink' => [$sut, '๐ŸŽ'], - 'Travel & Places' => [$sut, 'โ›ฐ๏ธ'], - 'Activities' => [$sut, '๐ŸŽˆ'], - 'Objects' => [$sut, '๐Ÿ“ข'], - 'Symbols from Unicode 4.0' => [$sut, 'โš ๏ธ'], - 'Symbols from Unicode 7.0' => [$sut, 'โบ๏ธ'], - 'Symbols from Unicode 6.0' => [$sut, 'โœ…'], - 'Flags Emoji 1.0' => [$sut, '๐Ÿ‡น๐Ÿ‡ณ'], - 'Flags Emoji 4.0' => [$sut, '๐Ÿณ๏ธโ€๐ŸŒˆ'], - 'Flags Emoji 5.0' => [$sut, '๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ'], - 'Flags Emoji 11.0' => [$sut, '๐Ÿดโ€โ˜ ๏ธ'], - 'Flags' => [$sut, '๐Ÿ‡น๐Ÿ‡ณ'], - 'Mixed with text' => [$sut, 'this is a pizza ๐Ÿ•'], - 'Array' => [$sut, []], - 'Bool' => [$sut, true], - 'Object' => [$sut, new stdClass()], - ]; - } -} diff --git a/tests/unit/Transformers/PrefixTest.php b/tests/unit/Transformers/PrefixTest.php index dcf7f0bab..b5a0605ec 100644 --- a/tests/unit/Transformers/PrefixTest.php +++ b/tests/unit/Transformers/PrefixTest.php @@ -93,7 +93,6 @@ public static function providerForUntransformedRuleNames(): array 'min' => ['min'], 'minAge' => ['minAge'], 'not' => ['not'], - 'notEmoji' => ['notEmoji'], 'notEmpty' => ['notEmpty'], 'undef' => ['undef'], 'property' => ['property'],