Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Create CssColor rule #68

Merged
merged 5 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions .ddev/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,22 @@ omit_containers: [db]
use_dns_when_possible: true
composer_version: "2"
web_environment: []
corepack_enable: false
disable_upload_dirs_warning: true

# Key features of DDEV's config.yaml:

# name: <projectname> # Name of the project, automatically provides
# http://projectname.ddev.site and https://projectname.ddev.site

# type: <projecttype> # backdrop, craftcms, django4, drupal6/7/8/9/10, laravel, magento, magento2, php, python, shopware6, silverstripe, typo3, wordpress
# See https://ddev.readthedocs.io/en/latest/users/quickstart/ for more
# type: <projecttype> # backdrop, craftcms, django4, drupal, drupal6, drupal7, laravel, magento, magento2, php, python, shopware6, silverstripe, typo3, wordpress
# See https://ddev.readthedocs.io/en/stable/users/quickstart/ for more
# information on the different project types
# "drupal" covers recent Drupal 8+

# docroot: <relative_path> # Relative path to the directory containing index.php.

# php_version: "8.1" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"
# php_version: "8.2" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"

# You can explicitly specify the webimage but this
# is not recommended, as the images are often closely tied to DDEV's' behavior,
Expand All @@ -36,8 +38,9 @@ disable_upload_dirs_warning: true

# database:
# type: <dbtype> # mysql, mariadb, postgres
# version: <version> # database version, like "10.4" or "8.0"
# MariaDB versions can be 5.5-10.8 and 10.11, MySQL versions can be 5.5-8.0
# version: <version> # database version, like "10.11" or "8.0"
# MariaDB versions can be 5.5-10.8, 10.11, and 11.4.
# MySQL versions can be 5.5-8.0.
# PostgreSQL versions can be 9-16.

# router_http_port: <port> # Port to be used for http (defaults to global configuration, usually 80)
Expand Down Expand Up @@ -77,7 +80,7 @@ disable_upload_dirs_warning: true
# Alternatively, an explicit Composer version may be specified, for example "2.2.18".
# To reinstall Composer after the image was built, run "ddev debug refresh".

# nodejs_version: "18"
# nodejs_version: "20"
# change from the default system Node.js version to any other version.
# Numeric version numbers can be complete (i.e. 18.15.0) or
# incomplete (18, 17.2, 16). 'lts' and 'latest' can be used as well along with
Expand All @@ -86,6 +89,9 @@ disable_upload_dirs_warning: true
# Note that you can continue using 'ddev nvm' or nvm inside the web container
# to change the project's installed node version if you need to.

# corepack_enable: false
# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm

# additional_hostnames:
# - somename
# - someothername
Expand Down Expand Up @@ -143,8 +149,8 @@ disable_upload_dirs_warning: true
# - "mutagen": enables Mutagen for this project.
# - "nfs": enables NFS for this project.
#
# See https://ddev.readthedocs.io/en/latest/users/install/performance/#nfs
# See https://ddev.readthedocs.io/en/latest/users/install/performance/#mutagen
# See https://ddev.readthedocs.io/en/stable/users/install/performance/#nfs
# See https://ddev.readthedocs.io/en/stable/users/install/performance/#mutagen

# fail_on_hook_fail: False
# Decide whether 'ddev start' should be interrupted by a failing hook
Expand Down Expand Up @@ -197,7 +203,7 @@ disable_upload_dirs_warning: true

# disable_settings_management: false
# If true, DDEV will not create CMS-specific settings files like
# Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php
# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php
# In this case the user must provide all such settings.

# You can inject environment variables into the web container with:
Expand Down
1 change: 1 addition & 0 deletions docs/03-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

## String Rules

- [CssColor](03-rules_css-color.md)
- [Email](03-rules_email.md)
- [Length](03-rules_length.md)
- [PasswordStrength](03-rules_password-strength.md)
Expand Down
126 changes: 126 additions & 0 deletions docs/03-rules_css-color.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# CssColor

Validates that a value is a valid CSS color.

```php
CssColor(
?array $formats = null,
?string $message = null
);
```

## Basic Usage

```php
// by default, all possible ways to define a CSS color are considered valid
Validator::cssColor()->validate('#0f0f0f'); // true
Validator::cssColor()->validate('black'); // true
Validator::cssColor()->validate('rgb(0, 255, 0)'); // true
// ...

// restrict allowed formats
Validator::cssColor(formats: ['hex-long'])->validate('#0f0f0f'); // true
Validator::cssColor(formats: ['hex-long'])->validate('rgb(0, 255, 0)'); // false
Validator::cssColor(formats: ['hex-long', 'rgb'])->validate('rgb(0, 255, 0)'); // true
```

> [!NOTE]
> An `UnexpectedValueException` will be thrown when a `formats` option is invalid.

## Options

### `formats`

type: `?array` default: `null`

By default, all possible ways to define a CSS color are considered valid.
Use this options to restrict the allowed CSS formats.

Available options are:

- [`hex-long`](#hex-long)
- [`hex-long-with-alpha`](#hex-long-with-alpha)
- [`hex-short`](#hex-short)
- [`hex-short-with-alpha`](#hex-short-with-alpha)
- [`basic-named-colors`](#basic-named-colors)
- [`extended-named-colors`](#extended-named-colors)
- [`system-colors`](#system-colors)
- [`keywords`](#keywords)
- [`rgb`](#rgb)
- [`rgba`](#rgba)
- [`hsl`](#hsl)
- [`hsla`](#hsla)

#### `hex-long`

Examples: `#0f0f0f`, `#0F0F0F`

#### `hex-long-with-alpha`

Examples: `#0f0f0f50`, `#0F0F0F50`

#### `hex-short`

Examples: `#0f0`, `#0F0`

#### `hex-short-with-alpha`

Examples: `#0f05`, `#0F05`

#### `basic-named-colors`

Colors names defined in the [W3C list of basic names colors](https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors).

Examples: `black`, `green`

#### `extended-named-colors`

Colors names defined in the [W3C list of extended names colors](https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors).

Examples: `black`, `aqua`, `darkgoldenrod`, `green`

#### `system-colors`

Colors names defined in the [CSS WG list of system colors](https://drafts.csswg.org/css-color/#css-system-colors).

Examples: `AccentColor`, `VisitedText`

#### `keywords`

Colors names defined in the [CSS WG list of keywords](https://drafts.csswg.org/css-color/#transparent-color).

Examples: `transparent`, `currentColor`

#### `rgb`

Examples: `rgb(0, 255, 0)`, `rgb(0,255,0)`

#### `rgba`

Examples: `rgba(0, 255, 0, 50)`, `rgba(0,255,0,50)`

#### `hsl`

Examples: `hsl(0, 50%, 50%)`, `hsl(0,50%,50%)`

#### `hsla`

Examples: `hsla(0, 50%, 50%, 0.5)`, `hsla(0,50%,50%,0.5)`

### `message`

type: `?string` default: `The {{ name }} value is not a valid CSS color.`

Message that will be shown if the input value is not a valid CSS color.

The following parameters are available:

| Parameter | Description |
|-----------------|---------------------------|
| `{{ value }}` | The current invalid value |
| `{{ name }}` | Name of the invalid value |
| `{{ formats }}` | Selected formats |

## Changelog

- `1.2.0` Created
5 changes: 5 additions & 0 deletions src/ChainedValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public function country(
?string $message = null
): ChainedValidatorInterface&Validator;

public function cssColor(
?array $formats = null,
?string $message = null
): ChainedValidatorInterface&Validator;

public function dateTime(
string $format = 'Y-m-d H:i:s',
?string $message = null
Expand Down
5 changes: 5 additions & 0 deletions src/Exception/CssColorException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace ProgrammatorDev\Validator\Exception;

class CssColorException extends ValidationException {}
114 changes: 114 additions & 0 deletions src/Rule/CssColor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace ProgrammatorDev\Validator\Rule;

use ProgrammatorDev\Validator\Exception\CssColorException;
use ProgrammatorDev\Validator\Exception\UnexpectedOptionException;
use ProgrammatorDev\Validator\Exception\UnexpectedTypeException;

class CssColor extends AbstractRule implements RuleInterface
{
public const FORMAT_HEX_LONG = 'hex-long';
public const FORMAT_HEX_LONG_WITH_ALPHA = 'hex-long-with-alpha';
public const FORMAT_HEX_SHORT = 'hex-short';
public const FORMAT_HEX_SHORT_WITH_ALPHA = 'hex-short-with-alpha';
public const FORMAT_BASIC_NAMED_COLORS = 'basic-named-colors';
public const FORMAT_EXTENDED_NAMED_COLORS = 'extended-named-colors';
public const FORMAT_SYSTEM_COLORS = 'system-colors';
public const FORMAT_KEYWORDS = 'keywords';
public const FORMAT_RGB = 'rgb';
public const FORMAT_RGBA = 'rgba';
public const FORMAT_HSL = 'hsl';
public const FORMAT_HSLA = 'hsla';

private const COLOR_FORMATS = [
self::FORMAT_HEX_LONG,
self::FORMAT_HEX_LONG_WITH_ALPHA,
self::FORMAT_HEX_SHORT,
self::FORMAT_HEX_SHORT_WITH_ALPHA,
self::FORMAT_BASIC_NAMED_COLORS,
self::FORMAT_EXTENDED_NAMED_COLORS,
self::FORMAT_SYSTEM_COLORS,
self::FORMAT_KEYWORDS,
self::FORMAT_RGB,
self::FORMAT_RGBA,
self::FORMAT_HSL,
self::FORMAT_HSLA
];

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';
// 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';
// 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';
// https://drafts.csswg.org/css-color/#css-system-colors
private const PATTERN_SYSTEM_COLORS = '/^(AccentColor|AccentColorText|ActiveText|ButtonBorder|ButtonFace|ButtonText|Canvas|CanvasText|Field|FieldText|GrayText|Highlight|HighlightText|LinkText|Mark|MarkText|SelectedItem|SelectedItemText|VisitedText)$/i';
// https://drafts.csswg.org/css-color/#transparent-color
// https://drafts.csswg.org/css-color/#currentcolor-color
private const PATTERN_KEYWORDS = '/^(transparent|currentColor)$/i';
private const PATTERN_RGB = '/^rgb\(\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|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s*\)$/i';
private const PATTERN_RGBA = '/^rgba\(\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|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|0?\.\d+|1(\.0)?)\s*\)$/i';
private const PATTERN_HSL = '/^hsl\(\s*(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*\)$/i';
private const PATTERN_HSLA = '/^hsla\(\s*(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|0?\.\d+|1(\.0)?)\s*\)$/i';

private const COLOR_PATTERNS = [
self::FORMAT_HEX_LONG => self::PATTERN_HEX_LONG,
self::FORMAT_HEX_LONG_WITH_ALPHA => self::PATTERN_HEX_LONG_WITH_ALPHA,
self::FORMAT_HEX_SHORT => self::PATTERN_HEX_SHORT,
self::FORMAT_HEX_SHORT_WITH_ALPHA => self::PATTERN_HEX_SHORT_WITH_ALPHA,
self::FORMAT_BASIC_NAMED_COLORS => self::PATTERN_BASIC_NAMED_COLORS,
self::FORMAT_EXTENDED_NAMED_COLORS => self::PATTERN_EXTENDED_NAMED_COLORS,
self::FORMAT_SYSTEM_COLORS => self::PATTERN_SYSTEM_COLORS,
self::FORMAT_KEYWORDS => self::PATTERN_KEYWORDS,
self::FORMAT_RGB => self::PATTERN_RGB,
self::FORMAT_RGBA => self::PATTERN_RGBA,
self::FORMAT_HSL => self::PATTERN_HSL,
self::FORMAT_HSLA => self::PATTERN_HSLA
];

private array $formats = self::COLOR_FORMATS;
private string $message = 'The {{ name }} value is not a valid CSS color.';

public function __construct(
?array $formats = null,
?string $message = null
)
{
$this->formats = $formats ?? $this->formats;
$this->message = $message ?? $this->message;
}

public function assert(mixed $value, ?string $name = null): void
{
foreach ($this->formats as $format) {
if (!\in_array($format, self::COLOR_FORMATS, true)) {
throw new UnexpectedOptionException('format', self::COLOR_FORMATS, $format);
}
}

if (!\is_string($value)) {
throw new UnexpectedTypeException('string', get_debug_type($value));
}

foreach ($this->formats as $format) {
$pattern = self::COLOR_PATTERNS[$format];

// it is valid if at least one pattern matches
if (\preg_match($pattern, $value)) {
return;
}
}

throw new CssColorException(
message: $this->message,
parameters: [
'value' => $value,
'name' => $name,
'formats' => $this->formats
]
);
}
}
5 changes: 5 additions & 0 deletions src/StaticValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public static function country(
?string $message = null
): ChainedValidatorInterface&Validator;

public static function cssColor(
?array $formats = null,
?string $message = null
): ChainedValidatorInterface&Validator;

public static function dateTime(
string $format = 'Y-m-d H:i:s',
?string $message = null
Expand Down
Loading
Loading