diff --git a/src/Integrations/Divi/Design/DiviVariablesSetAbility.php b/src/Integrations/Divi/Design/DiviVariablesSetAbility.php index eab2c51..728e2f2 100644 --- a/src/Integrations/Divi/Design/DiviVariablesSetAbility.php +++ b/src/Integrations/Divi/Design/DiviVariablesSetAbility.php @@ -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 * 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 - * builder. Values are checked per type (VariableValue); 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. + * builder. Values are checked per type (VariableValue), font variables also against Divi's font list (FontCheck, ruling + * 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 { @@ -52,6 +53,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures private GlobalVariablesAdapter $variables, private DesignChange $change, private GlobalUsages $usages, + private FontCatalog $fonts, private ?Closure $random = null ) { } @@ -229,7 +231,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures try { $entry[$key] = match ($key) { 'label' => self::cleanLabel($book, $item['label'], $id), - 'value' => self::value($type, $item['value']), + 'value' => $this->value($type, $item['value']), default => self::status($item['status']), }; } catch (InvalidArgumentException $e) { @@ -349,7 +351,7 @@ final class DiviVariablesSetAbility implements Ability, RequiresDiviFeatures } /** @throws InvalidArgumentException */ - private static function value(string $type, mixed $value): string + private function value(string $type, mixed $value): string { if (is_int($value)) { $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'); } $problem = VariableValue::check($type, $value); + if ($problem === null && $type === 'fonts') { + $problem = FontCheck::problem($this->fonts, $value); + } if ($problem !== null) { throw new InvalidArgumentException($problem); } diff --git a/src/Integrations/Divi/Design/VariableValue.php b/src/Integrations/Divi/Design/VariableValue.php index 625f884..9b0cfd6 100644 --- a/src/Integrations/Divi/Design/VariableValue.php +++ b/src/Integrations/Divi/Design/VariableValue.php @@ -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" - * (new in Divi 5.15) is passed through by VariableBook, but the tools never create or change it. The check of fonts - * against Divi's font list follows with 4d. Pure PHP. + * (new in Divi 5.15) is passed through by VariableBook, but the tools never create or change it. Font names are only + * checked for their form here; DiviVariablesSetAbility checks them against Divi's font list (FontCheck, ruling W12). + * Pure PHP. */ final class VariableValue { diff --git a/src/Integrations/Divi/DiviIntegration.php b/src/Integrations/Divi/DiviIntegration.php index e66f980..12ca023 100644 --- a/src/Integrations/Divi/DiviIntegration.php +++ b/src/Integrations/Divi/DiviIntegration.php @@ -30,6 +30,7 @@ use LIT\MCP\Integrations\Divi\Design\DiviPresetsAssignAbility; use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility; use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility; 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\PresetAssignment; use LIT\MCP\Integrations\Divi\Design\PresetComposer; @@ -304,7 +305,8 @@ final class DiviIntegration implements Integration, Bootable $this->designWriter, $this->variablesAdapter, $this->designChange(), - $this->globalUsages() + $this->globalUsages(), + new FontCatalog() ), ]; } diff --git a/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php b/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php index 6bbf9d0..bed656a 100644 --- a/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Design/DiviVariablesSetAbilityTest.php @@ -10,6 +10,7 @@ use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Integrations\Divi\Design\DesignChange; use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter; 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\ReferenceUsageQuery; use LIT\MCP\Integrations\Divi\Design\SnapshotStore; @@ -48,7 +49,8 @@ final class DiviVariablesSetAbilityTest extends TestCase ?SnapshotStore $snapshots = null, array $presets = [], ?GlobalVariablesAdapter $variables = null, - bool $manage = true + bool $manage = true, + ?FontCatalog $fonts = null ): DiviVariablesSetAbility { $writer = Mockery::mock(DesignSystemWriter::class); $writer->allows('canUseVisualBuilder')->andReturn(true); @@ -60,6 +62,7 @@ final class DiviVariablesSetAbilityTest extends TestCase $variables ?? VariableAreaTest::adapter($this->stored, $this->saves, $manage), new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), new FakeChangeLog(3)), $usages ?? self::usages(self::unused()), + $fonts ?? FontCheckTest::catalog(), static function (int $length) use (&$counter): string { return str_pad((string) ++$counter, $length, '0', STR_PAD_LEFT); } @@ -377,4 +380,56 @@ final class DiviVariablesSetAbilityTest extends TestCase 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']); + } }