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:
Jan-Niclas Loosen
2026-10-19 13:56:34 +02:00
co-authored by Claude Sonnet 5.5
parent 9b7c3c1d3b
commit fb46078d42
2 changed files with 202 additions and 0 deletions
@@ -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;
}
}