From 653169a133e9c0135ce4b844f2ad87dbe6eec80c Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 14:03:40 +0200 Subject: [PATCH] feat: PresetBook nennt Presets mit Verweisen auf Farben/Variablen und ersetzt Variablen-Verweise durch ihren Wert Co-Authored-By: Claude Sonnet 5.5 --- src/Integrations/Divi/Design/PresetBook.php | 79 +++++++++++++++ .../Divi/Design/PresetBookTest.php | 96 +++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/src/Integrations/Divi/Design/PresetBook.php b/src/Integrations/Divi/Design/PresetBook.php index 4dc3d32..099c37f 100644 --- a/src/Integrations/Divi/Design/PresetBook.php +++ b/src/Integrations/Divi/Design/PresetBook.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace LIT\MCP\Integrations\Divi\Design; use InvalidArgumentException; +use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Abilities\Rev; defined('ABSPATH') || exit; @@ -21,6 +22,9 @@ final class PresetBook private const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; + /** The keys of a preset that hold its settings (formats §3). */ + private const SETTINGS = ['attrs', 'styleAttrs', 'renderAttrs']; + /** @param array{module: array>}>, * group: array>}>} $data */ private function __construct(private array $data) @@ -226,6 +230,59 @@ final class PresetBook return new self($data); } + /** + * Presets whose settings (attrs, styleAttrs, renderAttrs) name the global color or variable $id – as a whole + * reference or inside a longer value (ruling V8: warnings of lit/divi-colors-set and lit/divi-variables-set). + * + * @return list scope = module or group name + */ + public function referencing(string $id): array + { + $users = []; + foreach ($this->data as $records) { + foreach ($records as $scope => $record) { + foreach ($record['items'] as $presetId => $item) { + if (ReferenceUsageQuery::mentions(array_intersect_key($item, array_flip(self::SETTINGS)), [$id])) { + $users[] = [ + 'id' => (string) $presetId, + 'name' => is_string($item['name'] ?? null) ? $item['name'] : (string) $presetId, + 'scope' => (string) $scope, + ]; + } + } + } + } + + return $users; + } + + /** + * The inventory with every whole reference to global variable $id in the presets' settings replaced by $value + * (ruling V9, lit/divi-design-detach with variable). With $affixes the reference's own before/after settings frame + * the value (variables of type strings). References inside a longer value stay. Presets without a reference stay + * exactly as they are. + */ + public function withVariableResolved(string $id, string $value, bool $affixes = false): self + { + $data = $this->data; + foreach ($this->data as $kind => $records) { + foreach ($records as $name => $record) { + foreach ($record['items'] as $presetId => $item) { + foreach (self::SETTINGS as $key) { + if (! is_array($item[$key] ?? null)) { + continue; + } + $resolved = self::resolved($item[$key], $id, $value, $affixes); + if ($resolved !== $item[$key]) { + $data[$kind][$name]['items'][$presetId][$key] = $resolved; + } + } + } + } + } + + return new self($data); + } /** * IDs of the presets outside $touchedIds whose settings (attrs, styleAttrs, renderAttrs) hold a string with "<" * or ">" – HTML or CSS that Divi's prepare_data() strips when a user without unfiltered_html saves the inventory @@ -302,6 +359,28 @@ final class PresetBook return $nested; } + /** + * @param array $settings + * @return array + */ + private static function resolved(array $settings, string $id, string $value, bool $affixes): array + { + foreach ($settings as $key => $inner) { + if (is_array($inner)) { + $settings[$key] = self::resolved($inner, $id, $value, $affixes); + } elseif (is_string($inner) && VariableRef::nameOf($inner) === $id) { + $own = $affixes ? VariableRef::settingsOf($inner) : []; + $settings[$key] = self::text($own['before'] ?? '') . $value . self::text($own['after'] ?? ''); + } + } + + return $settings; + } + + private static function text(mixed $value): string + { + return is_string($value) ? $value : ''; + } private static function hasMarkup(mixed $value): bool { if (is_string($value)) { diff --git a/tests/Unit/Integrations/Divi/Design/PresetBookTest.php b/tests/Unit/Integrations/Divi/Design/PresetBookTest.php index 5b95466..1d2ab2a 100644 --- a/tests/Unit/Integrations/Divi/Design/PresetBookTest.php +++ b/tests/Unit/Integrations/Divi/Design/PresetBookTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design; use InvalidArgumentException; +use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Integrations\Divi\Design\PresetBook; use LIT\MCP\Integrations\Divi\Design\PresetDiff; use LIT\MCP\Integrations\Divi\Design\PresetScope; @@ -57,6 +58,101 @@ final class PresetBookTest extends TestCase return $stored; } + /** + * stored() with references: CTA primär uses color gcid-holz000001, Doppelt variable gvid-slogan0001 (whole with + * before/after, and embedded in a longer value), Lesetext variable gvid-radius0001 in its styleAttrs. + * + * @return array + */ + public static function storedWithReferences(): array + { + $stored = self::stored(); + $stored['module']['divi/button']['items']['aaaaaaaaaa']['attrs'] = [ + 'button' => ['decoration' => ['background' => ['desktop' => ['value' => [ + 'color' => VariableRef::serialize('color', 'gcid-holz000001', []), + ]]]]], + ]; + $stored['module']['divi/button']['items']['bbbbbbbbbb']['attrs'] = [ + 'button' => ['innerContent' => ['desktop' => ['value' => [ + 'text' => VariableRef::serialize('content', 'gvid-slogan0001', ['before' => 'Ab ', 'after' => '!']), + ]]]], + 'module' => ['meta' => ['adminLabel' => ['desktop' => [ + 'value' => 'Vorher ' . VariableRef::serialize('content', 'gvid-slogan0001', []), + ]]]], + ]; + $stored['group']['divi/font-body']['items']['gggggggggg']['styleAttrs'] = [ + 'font' => ['size' => VariableRef::serialize('content', 'gvid-radius0001', [])], + ]; + + return $stored; + } + + public function testReferencingListsThePresetsThatNameAColorOrVariable(): void + { + $book = PresetBook::fromDivi(self::storedWithReferences()); + + self::assertSame( + [['id' => 'aaaaaaaaaa', 'name' => 'CTA primär', 'scope' => 'divi/button']], + $book->referencing('gcid-holz000001') + ); + self::assertSame( + [['id' => 'bbbbbbbbbb', 'name' => 'Doppelt', 'scope' => 'divi/button']], + $book->referencing('gvid-slogan0001') + ); + self::assertSame( + [['id' => 'gggggggggg', 'name' => 'Lesetext', 'scope' => 'divi/font-body']], + $book->referencing('gvid-radius0001') + ); + self::assertSame([], $book->referencing('gvid-fehlt00001')); + } + + public function testWithVariableResolvedReplacesWholeReferencesOnly(): void + { + $stored = self::storedWithReferences(); + $button = new PresetScope('module', 'divi/button'); + $book = PresetBook::fromDivi($stored)->withVariableResolved('gvid-slogan0001', 'Holz mit Herz'); + $attrs = $book->items($button)['bbbbbbbbbb']['attrs']; + + self::assertSame('Holz mit Herz', $attrs['button']['innerContent']['desktop']['value']['text']); + self::assertSame( + $stored['module']['divi/button']['items']['bbbbbbbbbb']['attrs']['module'], + $attrs['module'], + 'an embedded reference stays' + ); + self::assertSame($stored['module']['divi/button']['items']['aaaaaaaaaa'], $book->items($button)['aaaaaaaaaa']); + } + + /** Ruling V9: a strings variable keeps the before/after of the reference. */ + public function testWithVariableResolvedFramesTheValueWithBeforeAndAfter(): void + { + $book = PresetBook::fromDivi(self::storedWithReferences()) + ->withVariableResolved('gvid-slogan0001', 'Holz mit Herz', true); + + self::assertSame( + 'Ab Holz mit Herz!', + $book->items(new PresetScope('module', 'divi/button'))['bbbbbbbbbb']['attrs']['button']['innerContent'] + ['desktop']['value']['text'] + ); + } + + public function testWithVariableResolvedReachesStyleAttrsAndLeavesOtherPresetsAlone(): void + { + $stored = self::storedWithReferences(); + $book = PresetBook::fromDivi($stored)->withVariableResolved('gvid-radius0001', '8px'); + + self::assertSame( + ['font' => ['size' => '8px']], + $book->items(new PresetScope('group', 'divi/font-body'))['gggggggggg']['styleAttrs'] + ); + self::assertSame( + PresetBook::fromDivi($stored)->items(new PresetScope('module', 'divi/button')), + $book->items(new PresetScope('module', 'divi/button')) + ); + self::assertSame( + PresetBook::fromDivi($stored)->data(), + PresetBook::fromDivi($stored)->withVariableResolved('gvid-fehlt00001', 'x')->data() + ); + } /** Final review I-2: presets whose HTML/CSS Divi's prepare_data() would strip for editors. */ public function testMarkupOutsideListsUntouchedPresetsWithHtmlOrCss(): void {