feat: ColorValue prüft Farbwerte (Hex, rgb/hsl, Schlüsselwort) und die relativen Einstellungen von Farbverweisen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
9b7c3c1d3b
commit
fb46078d42
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Checks the value of a global color before it goes to Divi (ruling V5, D7 principle): hex, rgb()/rgba()/hsl()/hsla(),
|
||||
* or a color keyword. Placeholders ({{color:…}}) and $variable(…)$ references are no plain values – they are resolved
|
||||
* against the planned inventory by the caller; relative() checks the "relative" settings that belong to them.
|
||||
* Pure PHP.
|
||||
*/
|
||||
final class ColorValue
|
||||
{
|
||||
public const TYPES_NOTE = 'Erlaubt sind Hex-Werte (#rgb, #rgba, #rrggbb, #rrggbbaa), rgb()/rgba()/hsl()/hsla(), '
|
||||
. 'ein Farbschlüsselwort (z. B. red) oder ein Platzhalter {{color:<Name>}} zusammen mit "relative".';
|
||||
|
||||
/**
|
||||
* shortcut: these ranges are the sliders of the Visual Builder, not derived from Divi's PHP code; widen them if a
|
||||
* Divi release accepts more.
|
||||
*/
|
||||
private const RANGES = [
|
||||
'hue' => [-360, 360],
|
||||
'saturation' => [-100, 100],
|
||||
'lightness' => [-100, 100],
|
||||
'opacity' => [-100, 100],
|
||||
];
|
||||
|
||||
private const MAX_LENGTH = 100;
|
||||
|
||||
private const HEX = '/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/iD';
|
||||
|
||||
/** Only characters a color function needs; no "(", ")", ";" or quotes inside – nothing to break out of. */
|
||||
private const FUNCTION = '/^(?:rgb|rgba|hsl|hsla)\(\s*[0-9a-z.%+\-\/,\s]*\)$/iD';
|
||||
|
||||
private const KEYWORD = '/^[a-z]{3,20}$/iD';
|
||||
|
||||
/** @return string|null Null when $color is a valid plain color value, else a German message. */
|
||||
public static function check(string $color): ?string
|
||||
{
|
||||
if ($color === '') {
|
||||
return 'Farbwert fehlt. ' . self::TYPES_NOTE;
|
||||
}
|
||||
if (str_contains($color, '$variable(') || str_contains($color, '{{')) {
|
||||
return 'Verweise ($variable(…)$) und Platzhalter sind hier kein Farbwert; Platzhalter {{color:<Name>}} '
|
||||
. 'gehören allein in "color" und brauchen "relative".';
|
||||
}
|
||||
|
||||
$valid = strlen($color) <= self::MAX_LENGTH && (
|
||||
preg_match(self::HEX, $color) === 1
|
||||
|| preg_match(self::KEYWORD, $color) === 1
|
||||
|| (preg_match(self::FUNCTION, $color) === 1 && preg_match('/\d/', $color) === 1)
|
||||
);
|
||||
|
||||
return $valid ? null : sprintf('Ungültiger Farbwert "%s". %s', mb_substr($color, 0, 60), self::TYPES_NOTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings for VariableRef::serialize('color', …): only hue, saturation, lightness, opacity, numbers within
|
||||
* their ranges, in that order; whole floats become ints.
|
||||
*
|
||||
* @param array<string, mixed> $relative
|
||||
* @return array<string, int|float>|string The settings, or a German message.
|
||||
*/
|
||||
public static function relative(array $relative): array|string
|
||||
{
|
||||
if ($relative === []) {
|
||||
return 'relative: mindestens eine Einstellung nötig (' . implode(', ', array_keys(self::RANGES)) . ').';
|
||||
}
|
||||
|
||||
$unknown = array_diff(array_map('strval', array_keys($relative)), array_keys(self::RANGES));
|
||||
if ($unknown !== []) {
|
||||
return sprintf(
|
||||
'relative: unbekannte Einstellung "%s"; erlaubt: %s.',
|
||||
implode('", "', $unknown),
|
||||
implode(', ', array_keys(self::RANGES))
|
||||
);
|
||||
}
|
||||
|
||||
$settings = [];
|
||||
foreach (self::RANGES as $key => [$min, $max]) {
|
||||
if (! array_key_exists($key, $relative)) {
|
||||
continue;
|
||||
}
|
||||
$value = $relative[$key];
|
||||
$number = is_int($value) || is_float($value);
|
||||
if (! $number || ! is_finite((float) $value) || $value < $min || $value > $max) {
|
||||
return sprintf('relative.%s: Zahl zwischen %d und %d erwartet.', $key, $min, $max);
|
||||
}
|
||||
$settings[$key] = is_float($value) && floor($value) === $value ? (int) $value : $value;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\ColorValue;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ColorValueTest extends TestCase
|
||||
{
|
||||
/** @return array<string, array{string}> */
|
||||
public static function validColors(): array
|
||||
{
|
||||
return [
|
||||
'hex3' => ['#abc'], 'hex4' => ['#abcd'], 'hex6' => ['#5B3A29'], 'hex8' => ['#5b3a29cc'],
|
||||
'rgb' => ['rgb(10, 20, 30)'], 'rgba' => ['rgba(10,20,30,0.5)'], 'rgb modern' => ['rgb(10 20 30 / 50%)'],
|
||||
'hsl' => ['hsl(210, 50%, 40%)'], 'hsla' => ['hsla(210deg, 50%, 40%, .5)'],
|
||||
'keyword' => ['red'], 'keyword upper' => ['Transparent'], 'keyword long' => ['lightgoldenrodyellow'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider validColors */
|
||||
public function testValidColors(string $color): void
|
||||
{
|
||||
self::assertNull(ColorValue::check($color));
|
||||
}
|
||||
|
||||
/** @return array<string, array{string}> */
|
||||
public static function invalidColors(): array
|
||||
{
|
||||
return [
|
||||
'empty' => [''], 'hex5' => ['#abcde'], 'hex7' => ['#abcdef1'], 'hex without hash' => ['abcdef12'],
|
||||
'hex digits' => ['#ggg'], 'surrounding space' => [' #fff'], 'function without number' => ['rgb()'],
|
||||
'function with var' => ['rgb(var(--x))'], 'function with semicolon' => ['rgb(1;2;3)'],
|
||||
'unknown function' => ['url(1)'], 'keyword too short' => ['re'], 'keyword with digit' => ['red1'],
|
||||
'keyword too long' => ['abcdefghijklmnopqrstu'], 'css injection' => ['red;}body{x:y'],
|
||||
'too long' => ['rgb(' . str_repeat('1,', 60) . '1)'],
|
||||
'reference' => ['$variable({"type":"color","value":{"name":"gcid-a","settings":{}}})$'],
|
||||
'placeholder' => ['{{color:primary}}'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @dataProvider invalidColors */
|
||||
public function testInvalidColorsGetAGermanMessage(string $color): void
|
||||
{
|
||||
$message = ColorValue::check($color);
|
||||
|
||||
self::assertIsString($message);
|
||||
self::assertNotSame('', $message);
|
||||
}
|
||||
|
||||
public function testMessageNamesTheAllowedForms(): void
|
||||
{
|
||||
self::assertStringContainsString(ColorValue::TYPES_NOTE, (string) ColorValue::check('#abcde'));
|
||||
self::assertStringContainsString('Farbschlüsselwort', ColorValue::TYPES_NOTE);
|
||||
self::assertStringContainsString('Platzhalter', (string) ColorValue::check('{{color:primary}}'));
|
||||
}
|
||||
|
||||
public function testRelativeKeepsTheOrderAndNormalizesWholeFloats(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['hue' => 10, 'lightness' => -12.5, 'opacity' => 100],
|
||||
ColorValue::relative(['opacity' => 100.0, 'lightness' => -12.5, 'hue' => 10])
|
||||
);
|
||||
self::assertSame(['saturation' => 0], ColorValue::relative(['saturation' => 0]));
|
||||
}
|
||||
|
||||
public function testRelativeBoundsAreInclusive(): void
|
||||
{
|
||||
self::assertSame(['hue' => -360], ColorValue::relative(['hue' => -360]));
|
||||
self::assertSame(['hue' => 360], ColorValue::relative(['hue' => 360]));
|
||||
self::assertSame(['opacity' => 100], ColorValue::relative(['opacity' => 100]));
|
||||
self::assertSame(['opacity' => -100], ColorValue::relative(['opacity' => -100]));
|
||||
}
|
||||
|
||||
public function testRelativeRejectsRangesTypesAndUnknownKeys(): void
|
||||
{
|
||||
self::assertSame('relative.hue: Zahl zwischen -360 und 360 erwartet.', ColorValue::relative(['hue' => 361]));
|
||||
self::assertSame(
|
||||
'relative.lightness: Zahl zwischen -100 und 100 erwartet.',
|
||||
ColorValue::relative(['lightness' => -101])
|
||||
);
|
||||
self::assertSame(
|
||||
'relative.saturation: Zahl zwischen -100 und 100 erwartet.',
|
||||
ColorValue::relative(['saturation' => '10'])
|
||||
);
|
||||
self::assertSame(
|
||||
'relative.opacity: Zahl zwischen -100 und 100 erwartet.',
|
||||
ColorValue::relative(['opacity' => true])
|
||||
);
|
||||
self::assertSame(
|
||||
'relative.hue: Zahl zwischen -360 und 360 erwartet.',
|
||||
ColorValue::relative(['hue' => NAN])
|
||||
);
|
||||
self::assertSame(
|
||||
'relative: unbekannte Einstellung "alpha"; erlaubt: hue, saturation, lightness, opacity.',
|
||||
ColorValue::relative(['alpha' => 1])
|
||||
);
|
||||
self::assertSame(
|
||||
'relative: mindestens eine Einstellung nötig (hue, saturation, lightness, opacity).',
|
||||
ColorValue::relative([])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user