From b36371c06ee7b4b604f85926b6df7ca13b914b48 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Fri, 12 Feb 2021 16:27:40 +0100 Subject: [PATCH] Added new CssColor constraint --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Validator/Constraints/CssColor.php | 106 +++++ .../Constraints/CssColorValidator.php | 86 ++++ .../Resources/translations/validators.en.xlf | 4 + .../Resources/translations/validators.fr.xlf | 4 + .../Resources/translations/validators.it.xlf | 4 + .../Tests/Constraints/CssColorTest.php | 56 +++ .../Constraints/CssColorValidatorTest.php | 395 ++++++++++++++++++ 8 files changed, 656 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/CssColor.php create mode 100644 src/Symfony/Component/Validator/Constraints/CssColorValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/CssColorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index f73923bc1f1f4..51d3aa41576f9 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 5.4 --- + * Add a `CssColor` constraint to validate CSS colors * Add support for `ConstraintViolationList::createFromMessage()` 5.3 diff --git a/src/Symfony/Component/Validator/Constraints/CssColor.php b/src/Symfony/Component/Validator/Constraints/CssColor.php new file mode 100644 index 0000000000000..3888ab72adae1 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/CssColor.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\InvalidArgumentException; + +/** + * @Annotation + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * + * @author Mathieu Santostefano + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class CssColor extends Constraint +{ + public const HEX_LONG = 'hex_long'; + public const HEX_LONG_WITH_ALPHA = 'hex_long_with_alpha'; + public const HEX_SHORT = 'hex_short'; + public const HEX_SHORT_WITH_ALPHA = 'hex_short_with_alpha'; + public const BASIC_NAMED_COLORS = 'basic_named_colors'; + public const EXTENDED_NAMED_COLORS = 'extended_named_colors'; + public const SYSTEM_COLORS = 'system_colors'; + public const KEYWORDS = 'keywords'; + public const RGB = 'rgb'; + public const RGBA = 'rgba'; + public const HSL = 'hsl'; + public const HSLA = 'hsla'; + public const INVALID_FORMAT_ERROR = '454ab47b-aacf-4059-8f26-184b2dc9d48d'; + + protected static $errorNames = [ + self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', + ]; + + /** + * @var string[] + */ + private static $validationModes = [ + self::HEX_LONG, + self::HEX_LONG_WITH_ALPHA, + self::HEX_SHORT, + self::HEX_SHORT_WITH_ALPHA, + self::BASIC_NAMED_COLORS, + self::EXTENDED_NAMED_COLORS, + self::SYSTEM_COLORS, + self::KEYWORDS, + self::RGB, + self::RGBA, + self::HSL, + self::HSLA, + ]; + + public $message = 'This value is not a valid CSS color.'; + public $formats; + + /** + * @param array|string $formats The types of CSS colors allowed (e.g. hexadecimal only, RGB and HSL only, etc.). + */ + public function __construct($formats, string $message = null, array $groups = null, $payload = null, array $options = null) + { + $validationModesAsString = array_reduce(self::$validationModes, function ($carry, $value) { + return $carry ? $carry.', '.$value : $value; + }, ''); + + if (\is_array($formats) && \is_string(key($formats))) { + $options = array_merge($formats, $options); + } elseif (\is_array($formats)) { + if ([] === array_intersect(static::$validationModes, $formats)) { + throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString)); + } + + $options['value'] = $formats; + } elseif (\is_string($formats)) { + if (!\in_array($formats, static::$validationModes)) { + throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString)); + } + + $options['value'] = [$formats]; + } else { + throw new InvalidArgumentException('The "formats" parameter type is not valid. It should be a string or an array.'); + } + + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + } + + public function getDefaultOption(): string + { + return 'formats'; + } + + public function getRequiredOptions(): array + { + return ['formats']; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/CssColorValidator.php b/src/Symfony/Component/Validator/Constraints/CssColorValidator.php new file mode 100644 index 0000000000000..3574151f940e2 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/CssColorValidator.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Mathieu Santostefano + */ +class CssColorValidator extends ConstraintValidator +{ + private const PATTERN_HEX_LONG = '/^#[0-9a-f]{6}$/i'; + private const PATTERN_HEX_LONG_WITH_ALPHA = '/^#[0-9a-f]{8}$/i'; + private const PATTERN_HEX_SHORT = '/^#[0-9a-f]{3}$/i'; + private const PATTERN_HEX_SHORT_WITH_ALPHA = '/^#[0-9a-f]{4}$/i'; + // List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors + private const PATTERN_BASIC_NAMED_COLORS = '/^(black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua)$/i'; + // List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors + private const PATTERN_EXTENDED_NAMED_COLORS = '/^(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)$/i'; + // List comes from https://drafts.csswg.org/css-color/#css-system-colors + private const PATTERN_SYSTEM_COLORS = '/^(Canvas|CanvasText|LinkText|VisitedText|ActiveText|ButtonFace|ButtonText|ButtonBorder|Field|FieldText|Highlight|HighlightText|SelectedItem|SelectedItemText|Mark|MarkText|GrayText)$/i'; + private const PATTERN_KEYWORDS = '/^(transparent|currentColor)$/i'; + private const PATTERN_RGB = '/^rgb\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\)$/i'; + private const PATTERN_RGBA = '/^rgba\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|0?\.\d|1(\.0)?)\)$/i'; + private const PATTERN_HSL = '/^hsl\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s?(0|100|\d{1,2})%,\s?(0|100|\d{1,2})%\)$/i'; + private const PATTERN_HSLA = '/^hsla\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s?(0|100|\d{1,2})%,\s?(0|100|\d{1,2})%,\s?(0?\.\d|1(\.0)?)\)$/i'; + + private const COLOR_PATTERNS = [ + CssColor::HEX_LONG => self::PATTERN_HEX_LONG, + CssColor::HEX_LONG_WITH_ALPHA => self::PATTERN_HEX_LONG_WITH_ALPHA, + CssColor::HEX_SHORT => self::PATTERN_HEX_SHORT, + CssColor::HEX_SHORT_WITH_ALPHA => self::PATTERN_HEX_SHORT_WITH_ALPHA, + CssColor::BASIC_NAMED_COLORS => self::PATTERN_BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS => self::PATTERN_EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS => self::PATTERN_SYSTEM_COLORS, + CssColor::KEYWORDS => self::PATTERN_KEYWORDS, + CssColor::RGB => self::PATTERN_RGB, + CssColor::RGBA => self::PATTERN_RGBA, + CssColor::HSL => self::PATTERN_HSL, + CssColor::HSLA => self::PATTERN_HSLA, + ]; + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof CssColor) { + throw new UnexpectedTypeException($constraint, CssColor::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_string($value)) { + throw new UnexpectedValueException($value, 'string'); + } + + $formats = array_flip((array) $constraint->formats); + $formatRegexes = array_intersect_key(self::COLOR_PATTERNS, $formats); + + foreach ($formatRegexes as $regex) { + if (preg_match($regex, (string) $value)) { + return; + } + } + + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->addViolation(); + } +} diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf index 84cd9b9dcd9c2..3ba8d874da3ec 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf @@ -390,6 +390,10 @@ This value should be a valid expression. This value should be a valid expression. + + This value is not a valid CSS color. + This value is not a valid CSS color. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index c61ff92c6d473..39126b312b2e3 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -390,6 +390,10 @@ This value should be a valid expression. Cette valeur doit être une expression valide. + + This value is not a valid CSS color. + Cette valeur n'est pas une couleur CSS valide. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf index bca112204ddc8..99a40cc30b920 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf @@ -390,6 +390,10 @@ This value should be a valid expression. Questo valore dovrebbe essere un'espressione valida. + + This value is not a valid CSS color. + Questo valore non è un colore CSS valido. + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CssColorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CssColorTest.php new file mode 100644 index 0000000000000..fcf58b85b33c4 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/CssColorTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\CssColor; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; + +/** + * @author Mathieu Santostefano + * @requires PHP 8 + */ +final class CssColorTest extends TestCase +{ + public function testAttributes() + { + $metadata = new ClassMetadata(CssColorDummy::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + self::assertSame([CssColor::HEX_LONG, CssColor::HEX_SHORT], $aConstraint->formats); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + self::assertSame([CssColor::HEX_LONG], $bConstraint->formats); + self::assertSame('myMessage', $bConstraint->message); + self::assertSame(['Default', 'CssColorDummy'], $bConstraint->groups); + + [$cConstraint] = $metadata->properties['c']->getConstraints(); + self::assertSame([CssColor::HEX_SHORT], $cConstraint->formats); + self::assertSame(['my_group'], $cConstraint->groups); + self::assertSame('some attached data', $cConstraint->payload); + } +} + +class CssColorDummy +{ + #[CssColor([CssColor::HEX_LONG, CssColor::HEX_SHORT])] + private $a; + + #[CssColor(formats: CssColor::HEX_LONG, message: 'myMessage')] + private $b; + + #[CssColor(formats: [CssColor::HEX_SHORT], groups: ['my_group'], payload: 'some attached data')] + private $c; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php new file mode 100644 index 0000000000000..79b80fba8db6d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/CssColorValidatorTest.php @@ -0,0 +1,395 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\CssColor; +use Symfony\Component\Validator\Constraints\CssColorValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +final class CssColorValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): CssColorValidator + { + return new CssColorValidator(); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new CssColor(CssColor::HEX_LONG)); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new CssColor(CssColor::HEX_LONG)); + + $this->assertNoViolation(); + } + + public function testExpectsStringCompatibleType() + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new \stdClass(), new CssColor(CssColor::HEX_LONG)); + } + + /** + * @dataProvider getValidHexLongColors + */ + public function testValidHexLongColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::HEX_LONG)); + $this->assertNoViolation(); + } + + public function getValidHexLongColors(): array + { + return [['#ABCDEF'], ['#abcdef'], ['#C0FFEE'], ['#c0ffee'], ['#501311']]; + } + + /** + * @dataProvider getValidHexLongColorsWithAlpha + */ + public function testValidHexLongColorsWithAlpha($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::HEX_LONG_WITH_ALPHA)); + $this->assertNoViolation(); + } + + public function getValidHexLongColorsWithAlpha(): array + { + return [['#ABCDEF00'], ['#abcdef01'], ['#C0FFEE02'], ['#c0ffee03'], ['#501311FF']]; + } + + /** + * @dataProvider getValidHexShortColors + */ + public function testValidHexShortColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::HEX_SHORT)); + $this->assertNoViolation(); + } + + public function getValidHexShortColors(): array + { + return [['#F4B'], ['#FAB'], ['#f4b'], ['#fab']]; + } + + /** + * @dataProvider getValidHexShortColorsWithAlpha + */ + public function testValidHexShortColorsWithAlpha($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::HEX_SHORT_WITH_ALPHA)); + $this->assertNoViolation(); + } + + public function getValidHexShortColorsWithAlpha(): array + { + return [['#F4B1'], ['#FAB1'], ['#f4b1'], ['#fab1']]; + } + + /** + * @dataProvider getValidBasicNamedColors + */ + public function testValidBasicNamedColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::BASIC_NAMED_COLORS)); + $this->assertNoViolation(); + } + + public function getValidBasicNamedColors(): array + { + return [ + ['black'], ['silver'], ['gray'], ['white'], ['maroon'], ['red'], ['purple'], ['fuchsia'], ['green'], ['lime'], ['olive'], ['yellow'], ['navy'], ['blue'], ['teal'], ['aqua'], + ['BLACK'], ['SILVER'], ['GRAY'], ['WHITE'], ['MAROON'], ['RED'], ['PURPLE'], ['FUCHSIA'], ['GREEN'], ['LIME'], ['OLIVE'], ['YELLOW'], ['NAVY'], ['BLUE'], ['TEAL'], ['AQUA'], + ]; + } + + /** + * @dataProvider getValidExtendedNamedColors + */ + public function testValidExtendedNamedColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::EXTENDED_NAMED_COLORS)); + $this->assertNoViolation(); + } + + public function getValidExtendedNamedColors(): array + { + return [ + ['aliceblue'], ['antiquewhite'], ['aqua'], ['aquamarine'], ['azure'], ['beige'], ['bisque'], ['black'], ['blanchedalmond'], ['blue'], ['blueviolet'], ['brown'], ['burlywood'], ['cadetblue'], ['chartreuse'], ['chocolate'], ['coral'], ['cornflowerblue'], ['cornsilk'], ['crimson'], ['cyan'], ['darkblue'], ['darkcyan'], ['darkgoldenrod'], ['darkgray'], ['darkgreen'], ['darkgrey'], ['darkkhaki'], ['darkmagenta'], ['darkolivegreen'], ['darkorange'], ['darkorchid'], ['darkred'], ['darksalmon'], ['darkseagreen'], ['darkslateblue'], ['darkslategray'], ['darkslategrey'], ['darkturquoise'], ['darkviolet'], ['deeppink'], ['deepskyblue'], ['dimgray'], ['dimgrey'], ['dodgerblue'], ['firebrick'], ['floralwhite'], ['forestgreen'], ['fuchsia'], ['gainsboro'], ['ghostwhite'], ['gold'], ['goldenrod'], ['gray'], ['green'], ['greenyellow'], ['grey'], ['honeydew'], ['hotpink'], ['indianred'], ['indigo'], ['ivory'], ['khaki'], ['lavender'], ['lavenderblush'], ['lawngreen'], ['lemonchiffon'], ['lightblue'], ['lightcoral'], ['lightcyan'], ['lightgoldenrodyellow'], ['lightgray'], ['lightgreen'], ['lightgrey'], ['lightpink'], ['lightsalmon'], ['lightseagreen'], ['lightskyblue'], ['lightslategray'], ['lightslategrey'], ['lightsteelblue'], ['lightyellow'], ['lime'], ['limegreen'], ['linen'], ['magenta'], ['maroon'], ['mediumaquamarine'], ['mediumblue'], ['mediumorchid'], ['mediumpurple'], ['mediumseagreen'], ['mediumslateblue'], ['mediumspringgreen'], ['mediumturquoise'], ['mediumvioletred'], ['midnightblue'], ['mintcream'], ['mistyrose'], ['moccasin'], ['navajowhite'], ['navy'], ['oldlace'], ['olive'], ['olivedrab'], ['orange'], ['orangered'], ['orchid'], ['palegoldenrod'], ['palegreen'], ['paleturquoise'], ['palevioletred'], ['papayawhip'], ['peachpuff'], ['peru'], ['pink'], ['plum'], ['powderblue'], ['purple'], ['red'], ['rosybrown'], ['royalblue'], ['saddlebrown'], ['salmon'], ['sandybrown'], ['seagreen'], ['seashell'], ['sienna'], ['silver'], ['skyblue'], ['slateblue'], ['slategray'], ['slategrey'], ['snow'], ['springgreen'], ['steelblue'], ['tan'], ['teal'], ['thistle'], ['tomato'], ['turquoise'], ['violet'], ['wheat'], ['white'], ['whitesmoke'], ['yellow'], ['yellowgreen'], + ['ALICEBLUE'], ['ANTIQUEWHITE'], ['AQUA'], ['AQUAMARINE'], ['AZURE'], ['BEIGE'], ['BISQUE'], ['BLACK'], ['BLANCHEDALMOND'], ['BLUE'], ['BLUEVIOLET'], ['BROWN'], ['BURLYWOOD'], ['CADETBLUE'], ['CHARTREUSE'], ['CHOCOLATE'], ['CORAL'], ['CORNFLOWERBLUE'], ['CORNSILK'], ['CRIMSON'], ['CYAN'], ['DARKBLUE'], ['DARKCYAN'], ['DARKGOLDENROD'], ['DARKGRAY'], ['DARKGREEN'], ['DARKGREY'], ['DARKKHAKI'], ['DARKMAGENTA'], ['DARKOLIVEGREEN'], ['DARKORANGE'], ['DARKORCHID'], ['DARKRED'], ['DARKSALMON'], ['DARKSEAGREEN'], ['DARKSLATEBLUE'], ['DARKSLATEGRAY'], ['DARKSLATEGREY'], ['DARKTURQUOISE'], ['DARKVIOLET'], ['DEEPPINK'], ['DEEPSKYBLUE'], ['DIMGRAY'], ['DIMGREY'], ['DODGERBLUE'], ['FIREBRICK'], ['FLORALWHITE'], ['FORESTGREEN'], ['FUCHSIA'], ['GAINSBORO'], ['GHOSTWHITE'], ['GOLD'], ['GOLDENROD'], ['GRAY'], ['GREEN'], ['GREENYELLOW'], ['GREY'], ['HONEYDEW'], ['HOTPINK'], ['INDIANRED'], ['INDIGO'], ['IVORY'], ['KHAKI'], ['LAVENDER'], ['LAVENDERBLUSH'], ['LAWNGREEN'], ['LEMONCHIFFON'], ['LIGHTBLUE'], ['LIGHTCORAL'], ['LIGHTCYAN'], ['LIGHTGOLDENRODYELLOW'], ['LIGHTGRAY'], ['LIGHTGREEN'], ['LIGHTGREY'], ['LIGHTPINK'], ['LIGHTSALMON'], ['LIGHTSEAGREEN'], ['LIGHTSKYBLUE'], ['LIGHTSLATEGRAY'], ['LIGHTSLATEGREY'], ['LIGHTSTEELBLUE'], ['LIGHTYELLOW'], ['LIME'], ['LIMEGREEN'], ['LINEN'], ['MAGENTA'], ['MAROON'], ['MEDIUMAQUAMARINE'], ['MEDIUMBLUE'], ['MEDIUMORCHID'], ['MEDIUMPURPLE'], ['MEDIUMSEAGREEN'], ['MEDIUMSLATEBLUE'], ['MEDIUMSPRINGGREEN'], ['MEDIUMTURQUOISE'], ['MEDIUMVIOLETRED'], ['MIDNIGHTBLUE'], ['MINTCREAM'], ['MISTYROSE'], ['MOCCASIN'], ['NAVAJOWHITE'], ['NAVY'], ['OLDLACE'], ['OLIVE'], ['OLIVEDRAB'], ['ORANGE'], ['ORANGERED'], ['ORCHID'], ['PALEGOLDENROD'], ['PALEGREEN'], ['PALETURQUOISE'], ['PALEVIOLETRED'], ['PAPAYAWHIP'], ['PEACHPUFF'], ['PERU'], ['PINK'], ['PLUM'], ['POWDERBLUE'], ['PURPLE'], ['RED'], ['ROSYBROWN'], ['ROYALBLUE'], ['SADDLEBROWN'], ['SALMON'], ['SANDYBROWN'], ['SEAGREEN'], ['SEASHELL'], ['SIENNA'], ['SILVER'], ['SKYBLUE'], ['SLATEBLUE'], ['SLATEGRAY'], ['SLATEGREY'], ['SNOW'], ['SPRINGGREEN'], ['STEELBLUE'], ['TAN'], ['TEAL'], ['THISTLE'], ['TOMATO'], ['TURQUOISE'], ['VIOLET'], ['WHEAT'], ['WHITE'], ['WHITESMOKE'], ['YELLOW'], ['YELLOWGREEN'], + ]; + } + + /** + * @dataProvider getValidSystemColors + */ + public function testValidSystemColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::SYSTEM_COLORS)); + $this->assertNoViolation(); + } + + public function getValidSystemColors(): array + { + return [ + ['Canvas'], ['CanvasText'], ['LinkText'], ['VisitedText'], ['ActiveText'], ['ButtonFace'], ['ButtonText'], ['ButtonBorder'], ['Field'], ['FieldText'], ['Highlight'], ['HighlightText'], ['SelectedItem'], ['SelectedItemText'], ['Mark'], ['MarkText'], ['GrayText'], + ['canvas'], ['canvastext'], ['linktext'], ['visitedtext'], ['activetext'], ['buttonface'], ['buttontext'], ['buttonborder'], ['field'], ['fieldtext'], ['highlight'], ['highlighttext'], ['selecteditem'], ['selecteditemtext'], ['mark'], ['marktext'], ['graytext'], + ['CANVAS'], ['CANVASTEXT'], ['LINKTEXT'], ['VISITEDTEXT'], ['ACTIVETEXT'], ['BUTTONFACE'], ['BUTTONTEXT'], ['BUTTONBORDER'], ['FIELD'], ['FIELDTEXT'], ['HIGHLIGHT'], ['HIGHLIGHTTEXT'], ['SELECTEDITEM'], ['SELECTEDITEMTEXT'], ['MARK'], ['MARKTEXT'], ['GRAYTEXT'], + ]; + } + + /** + * @dataProvider getValidKeywords + */ + public function testValidKeywords($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::KEYWORDS)); + $this->assertNoViolation(); + } + + public function getValidKeywords(): array + { + return [['transparent'], ['currentColor']]; + } + + /** + * @dataProvider getValidRGB + */ + public function testValidRGB($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::RGB)); + $this->assertNoViolation(); + } + + public function getValidRGB(): array + { + return [ + ['rgb(255, 255, 255)'], ['rgb(0, 0, 0)'], ['rgb(0, 0, 255)'], ['rgb(255, 0, 0)'], ['rgb(122, 122, 122)'], ['rgb(66, 66, 66)'], + ['rgb(255,255,255)'], ['rgb(0,0,0)'], ['rgb(0,0,255)'], ['rgb(255,0,0)'], ['rgb(122,122,122)'], ['rgb(66,66,66)'], + ]; + } + + /** + * @dataProvider getValidRGBA + */ + public function testValidRGBA($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::RGBA)); + $this->assertNoViolation(); + } + + public function getValidRGBA(): array + { + return [ + ['rgba(255, 255, 255, 0.3)'], ['rgba(0, 0, 0, 0.3)'], ['rgba(0, 0, 255, 0.3)'], ['rgba(255, 0, 0, 0.3)'], ['rgba(122, 122, 122, 0.3)'], ['rgba(66, 66, 66, 0.3)'], + ['rgba(255,255,255,0.3)'], ['rgba(0,0,0,0.3)'], ['rgba(0,0,255,0.3)'], ['rgba(255,0,0,0.3)'], ['rgba(122,122,122,0.3)'], ['rgba(66,66,66,0.3)'], + ]; + } + + /** + * @dataProvider getValidHSL + */ + public function testValidHSL($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::HSL)); + $this->assertNoViolation(); + } + + public function getValidHSL(): array + { + return [ + ['hsl(0, 0%, 20%)'], ['hsl(0, 100%, 50%)'], ['hsl(147, 50%, 47%)'], ['hsl(46, 100%, 0%)'], + ['hsl(0,0%,20%)'], ['hsl(0,100%,50%)'], ['hsl(147,50%,47%)'], ['hsl(46,100%,0%)'], + ]; + } + + /** + * @dataProvider getValidHSLA + */ + public function testValidHSLA($cssColor) + { + $this->validator->validate($cssColor, new CssColor(CssColor::HSLA)); + $this->assertNoViolation(); + } + + public function getValidHSLA(): array + { + return [ + ['hsla(0, 0%, 20%, 0.4)'], ['hsla(0, 100%, 50%, 0.4)'], ['hsla(147, 50%, 47%, 0.4)'], ['hsla(46, 100%, 0%, 0.4)'], + ['hsla(0,0%,20%,0.4)'], ['hsla(0,100%,50%,0.4)'], ['hsla(147,50%,47%,0.4)'], ['hsla(46,100%,0%,0.4)'], + ]; + } + + /** + * @dataProvider getInvalidHexColors + */ + public function testInvalidHexColors($cssColor) + { + $constraint = new CssColor([CssColor::HEX_LONG, CssColor::HEX_LONG_WITH_ALPHA], 'myMessage'); + $this->validator->validate($cssColor, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidHexColors(): array + { + return [['ABCDEF'], ['abcdef'], ['#K0FFEE'], ['#k0ffee'], ['#_501311'], ['ABCDEF00'], ['abcdefcc'], ['#K0FFEE33'], ['#k0ffeecc'], ['#_50131100'], ['#FAℬ'], ['#Ⅎab'], ['#F4️⃣B'], ['#f(4)b'], ['#907;']]; + } + + /** + * @dataProvider getInvalidShortHexColors + */ + public function testInvalidShortHexColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor([CssColor::HEX_SHORT, CssColor::HEX_SHORT_WITH_ALPHA], 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidShortHexColors(): array + { + return [['ABC'], ['ABCD'], ['abc'], ['abcd'], ['#K0F'], ['#K0FF'], ['#k0f'], ['#k0ff'], ['#_50'], ['#_501']]; + } + + /** + * @dataProvider getInvalidNamedColors + */ + public function testInvalidNamedColors($cssColor) + { + $this->validator->validate($cssColor, new CssColor([ + CssColor::BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS, + CssColor::KEYWORDS, + ], 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidNamedColors(): array + { + return [['fabpot'], ['ngrekas'], ['symfony'], ['FABPOT'], ['NGREKAS'], ['SYMFONY']]; + } + + /** + * @dataProvider getInvalidRGB + */ + public function testInvalidRGB($cssColor) + { + $this->validator->validate($cssColor, new CssColor([ + CssColor::BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS, + CssColor::KEYWORDS, + ], 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidRGB(): array + { + return [['rgb(999,999,999)'], ['rgb(-99,-99,-99)'], ['rgb(a,b,c)']]; + } + + /** + * @dataProvider getInvalidRGBA + */ + public function testInvalidRGBA($cssColor) + { + $this->validator->validate($cssColor, new CssColor([ + CssColor::BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS, + CssColor::KEYWORDS, + ], 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidRGBA(): array + { + return [['rgba(999,999,999,999)'], ['rgba(-99,-99,-99,-99)'], ['rgba(a,b,c,d)']]; + } + + /** + * @dataProvider getInvalidHSL + */ + public function testInvalidHSL($cssColor) + { + $this->validator->validate($cssColor, new CssColor([ + CssColor::BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS, + CssColor::KEYWORDS, + ], 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidHSL(): array + { + return [['hsl(1000, 1000%, 20000%)'], ['hsl(-100, -10%, -2%)'], ['hsl(a, b, c)'], ['hsl(a, b%, c%)']]; + } + + /** + * @dataProvider getInvalidHSL + */ + public function testInvalidHSLA($cssColor) + { + $this->validator->validate($cssColor, new CssColor([ + CssColor::BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS, + CssColor::KEYWORDS, + ], 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$cssColor.'"') + ->setCode(CssColor::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidHSLA(): array + { + return [['hsla(1000, 1000%, 20000%, 999)'], ['hsla(-100, -10%, -2%, 999)'], ['hsla(a, b, c, d)'], ['hsla(a, b%, c%, d)']]; + } + + public function testUnknownFormatsOnValidateTriggerException() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "formats" parameter value is not valid. It must contain one or more of the following values: "hex_long, hex_long_with_alpha, hex_short, hex_short_with_alpha, basic_named_colors, extended_named_colors, system_colors, keywords, rgb, rgba, hsl, hsla".'); + $constraint = new CssColor('Unknown Format'); + $this->validator->validate('#F4B907', $constraint); + } +}