feat: lit/divi-variables-set prüft Schrift-Variablen gegen Divis Schriftliste (mit Vorschlägen)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 17:56:58 +02:00
co-authored by Claude Sonnet 5.5
parent dab4a9ba2e
commit 449ec5c33f
4 changed files with 71 additions and 8 deletions
@@ -22,8 +22,9 @@ defined('ABSPATH') || exit;
* pipeline (VariableArea). An item names an existing variable in id (ID or name) or creates one from type, label and * pipeline (VariableArea). An item names an existing variable in id (ID or name) or creates one from type, label and
* value. Variables are never deleted – Divi refuses an inventory that lacks a stored ID (V1) –, only archived (order * value. Variables are never deleted – Divi refuses an inventory that lacks a stored ID (V1) –, only archived (order
* "") and activated again (appended at the end of their type); the type of a variable stays, gradients are left to the * "") and activated again (appended at the end of their type); the type of a variable stays, gradients are left to the
* builder. Values are checked per type (VariableValue); an active variable needs one. The dry run lists the posts that * builder. Values are checked per type (VariableValue), font variables also against Divi's font list (FontCheck, ruling
* reference a changed, archived or activated variable (ReferenceUsageQuery) and warns about presets that use it. * W12); an active variable needs one. The dry run lists the posts that reference a changed, archived or activated
* variable (ReferenceUsageQuery) and warns about presets that use it.
*/ */
final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
{ {
@@ -52,6 +53,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
private GlobalVariablesAdapter $variables, private GlobalVariablesAdapter $variables,
private DesignChange $change, private DesignChange $change,
private GlobalUsages $usages, private GlobalUsages $usages,
private FontCatalog $fonts,
private ?Closure $random = null private ?Closure $random = null
) { ) {
} }
@@ -229,7 +231,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
try { try {
$entry[$key] = match ($key) { $entry[$key] = match ($key) {
'label' => self::cleanLabel($book, $item['label'], $id), 'label' => self::cleanLabel($book, $item['label'], $id),
'value' => self::value($type, $item['value']), 'value' => $this->value($type, $item['value']),
default => self::status($item['status']), default => self::status($item['status']),
}; };
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
@@ -349,7 +351,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
} }
/** @throws InvalidArgumentException */ /** @throws InvalidArgumentException */
private static function value(string $type, mixed $value): string private function value(string $type, mixed $value): string
{ {
if (is_int($value)) { if (is_int($value)) {
$value = (string) $value; // an attachment ID for images $value = (string) $value; // an attachment ID for images
@@ -358,6 +360,9 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures
throw new InvalidArgumentException('Wert als Text erwartet'); throw new InvalidArgumentException('Wert als Text erwartet');
} }
$problem = VariableValue::check($type, $value); $problem = VariableValue::check($type, $value);
if ($problem === null && $type === 'fonts') {
$problem = FontCheck::problem($this->fonts, $value);
}
if ($problem !== null) { if ($problem !== null) {
throw new InvalidArgumentException($problem); throw new InvalidArgumentException($problem);
} }
@@ -8,8 +8,9 @@ defined('ABSPATH') || exit;
/** /**
* Checks the value of a global variable before it goes to Divi (ruling V6, D7 principle). The type "gradients" * Checks the value of a global variable before it goes to Divi (ruling V6, D7 principle). The type "gradients"
* (new in Divi 5.15) is passed through by VariableBook, but the tools never create or change it. The check of fonts * (new in Divi 5.15) is passed through by VariableBook, but the tools never create or change it. Font names are only
* against Divi's font list follows with 4d. Pure PHP. * checked for their form here; DiviVariablesSetAbility checks them against Divi's font list (FontCheck, ruling W12).
* Pure PHP.
*/ */
final class VariableValue final class VariableValue
{ {
+3 -1
View File
@@ -30,6 +30,7 @@ use LIT\MCP\Integrations\Divi\Design\DiviPresetsAssignAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility; use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility; use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviVariablesSetAbility; use LIT\MCP\Integrations\Divi\Design\DiviVariablesSetAbility;
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
use LIT\MCP\Integrations\Divi\Design\PageRewriter; use LIT\MCP\Integrations\Divi\Design\PageRewriter;
use LIT\MCP\Integrations\Divi\Design\PresetAssignment; use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
use LIT\MCP\Integrations\Divi\Design\PresetComposer; use LIT\MCP\Integrations\Divi\Design\PresetComposer;
@@ -304,7 +305,8 @@ final class DiviIntegration implements Integration, Bootable
$this->designWriter, $this->designWriter,
$this->variablesAdapter, $this->variablesAdapter,
$this->designChange(), $this->designChange(),
$this->globalUsages() $this->globalUsages(),
new FontCatalog()
), ),
]; ];
} }
@@ -10,6 +10,7 @@ use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Design\DesignChange; use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter; use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviVariablesSetAbility; use LIT\MCP\Integrations\Divi\Design\DiviVariablesSetAbility;
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter; use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
use LIT\MCP\Integrations\Divi\Design\ReferenceUsageQuery; use LIT\MCP\Integrations\Divi\Design\ReferenceUsageQuery;
use LIT\MCP\Integrations\Divi\Design\SnapshotStore; use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
@@ -48,7 +49,8 @@ final class DiviVariablesSetAbilityTest extends TestCase
?SnapshotStore $snapshots = null, ?SnapshotStore $snapshots = null,
array $presets = [], array $presets = [],
?GlobalVariablesAdapter $variables = null, ?GlobalVariablesAdapter $variables = null,
bool $manage = true bool $manage = true,
?FontCatalog $fonts = null
): DiviVariablesSetAbility { ): DiviVariablesSetAbility {
$writer = Mockery::mock(DesignSystemWriter::class); $writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('canUseVisualBuilder')->andReturn(true); $writer->allows('canUseVisualBuilder')->andReturn(true);
@@ -60,6 +62,7 @@ final class DiviVariablesSetAbilityTest extends TestCase
$variables ?? VariableAreaTest::adapter($this->stored, $this->saves, $manage), $variables ?? VariableAreaTest::adapter($this->stored, $this->saves, $manage),
new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), new FakeChangeLog(3)), new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), new FakeChangeLog(3)),
$usages ?? self::usages(self::unused()), $usages ?? self::usages(self::unused()),
$fonts ?? FontCheckTest::catalog(),
static function (int $length) use (&$counter): string { static function (int $length) use (&$counter): string {
return str_pad((string) ++$counter, $length, '0', STR_PAD_LEFT); return str_pad((string) ++$counter, $length, '0', STR_PAD_LEFT);
} }
@@ -377,4 +380,56 @@ final class DiviVariablesSetAbilityTest extends TestCase
self::assertSame([], $dry['warnings']); self::assertSame([], $dry['warnings']);
} }
/** Ruling W12: a font variable names a font Divi knows exactly; unknown names get suggestions. */
public function testFontVariablesNeedAFontDiviKnows(): void
{
$error = $this->ability()
->execute(['items' => [['type' => 'fonts', 'label' => 'Marke', 'value' => 'robotto']]]);
self::assertToolError($error, 'validation_failed');
$fields = self::errorFields($error);
self::assertSame(['items[0].value'], array_keys($fields));
self::assertStringStartsWith('unbekannte Schrift „robotto“; ähnlich: ', $fields['items[0].value']);
self::assertStringContainsString('Roboto', $fields['items[0].value']);
self::assertSame([], $this->saves);
}
public function testAFontVariableWithAKnownFontIsPlanned(): void
{
$dry = self::assertToolError(
$this->ability()->execute(['items' => [['type' => 'fonts', 'label' => 'Marke', 'value' => 'Roboto Slab']]]),
'confirm_required'
);
self::assertSame(['added'], array_column($dry['diff'], 'action'));
}
/** The font list is loaded only for font variables (it costs 1.3 MB, ruling W5). */
public function testOtherTypesDoNotLoadTheFontList(): void
{
$fonts = Mockery::mock(FontCatalog::class);
$fonts->shouldNotReceive('has', 'names');
self::assertToolError(
$this->ability(fonts: $fonts)->execute(['items' => [
['type' => 'strings', 'label' => 'Gruß', 'value' => 'Hallo'],
['id' => 'Eckenradius', 'value' => '12px'],
]]),
'confirm_required'
);
}
/** VariableValue checks first: a malformed name never reaches the font list. */
public function testAMalformedFontNameFailsBeforeTheFontList(): void
{
$fonts = Mockery::mock(FontCatalog::class);
$fonts->shouldNotReceive('has', 'names');
$error = $this->ability(fonts: $fonts)
->execute(['items' => [['type' => 'fonts', 'label' => 'Marke', 'value' => 'Roboto; x']]]);
self::assertToolError($error, 'validation_failed');
self::assertStringStartsWith('Schrift:', self::errorFields($error)['items[0].value']);
}
} }