diff --git a/src/Integrations/Divi/Design/DetachRewrite.php b/src/Integrations/Divi/Design/DetachRewrite.php index 076bb10..da6d5c6 100644 --- a/src/Integrations/Divi/Design/DetachRewrite.php +++ b/src/Integrations/Divi/Design/DetachRewrite.php @@ -124,8 +124,11 @@ final class DetachRewrite implements NodeRewrite return array_values(array_filter($ids, fn (mixed $item): bool => $item !== $this->id)); } - /** Whether the attributes of $block or of any block below mention $id (fail closed on unencodable values). */ - private static function mentions(Block $block, string $id): bool + /** + * Whether the attributes of $block or of any block below mention $id (fail closed on unencodable values); also + * VariableDetachRewrite::concernsInstance(). + */ + public static function mentions(Block $block, string $id): bool { try { if (str_contains(Json::encode($block->attrs()), $id)) { diff --git a/src/Integrations/Divi/Design/VariableDetachRewrite.php b/src/Integrations/Divi/Design/VariableDetachRewrite.php new file mode 100644 index 0000000..9b988c0 --- /dev/null +++ b/src/Integrations/Divi/Design/VariableDetachRewrite.php @@ -0,0 +1,107 @@ +",…}})$ becomes the value – for a strings variable with the reference's own before/after + * around it. References embedded in a longer text stay (Divi resolves them inside the text; cutting them out would + * change the text) and are named in notes(). Other references – colors, other variables, dynamic content – stay + * untouched. Pure PHP. + */ +final class VariableDetachRewrite implements NodeRewrite +{ + private const EMBEDDED = '%s: Die Variable steht hier eingebettet in einem längeren Text; dieser Verweis bleibt ' + . 'stehen – im Divi-Builder ersetzen.'; + + public function __construct(private string $id, private string $value, private string $variableType) + { + } + + /** + * What one whole reference to a variable of $variableType becomes: its value, for strings with the reference's + * before/after settings around it. + */ + public static function replacement(string $reference, string $value, string $variableType): string + { + if ($variableType !== 'strings') { + return $value; + } + $settings = VariableRef::settingsOf($reference); + $before = is_string($settings['before'] ?? null) ? $settings['before'] : ''; + $after = is_string($settings['after'] ?? null) ? $settings['after'] : ''; + + return $before . $value . $after; + } + + public function rewrite(Block $block): ?array + { + if (! $block->isDivi()) { + return null; + } + + $changed = false; + $attrs = $this->resolved($block->attrs(), $changed); + + return $changed && is_array($attrs) ? $attrs : null; + } + + public function notes(Block $block, string $nodeId): array + { + return $this->embeds($block->attrs()) ? [sprintf(self::EMBEDDED, $nodeId)] : []; + } + + public function concernsInstance(Block $instance): bool + { + return DetachRewrite::mentions($instance, $this->id); + } + + /** $value with every whole reference to the variable replaced; $changed tells whether one was. */ + private function resolved(mixed $value, bool &$changed): mixed + { + if (is_string($value)) { + if (VariableRef::nameOf($value) !== $this->id) { + return $value; + } + $changed = true; + + return self::replacement($value, $this->value, $this->variableType); + } + if (! is_array($value)) { + return $value; + } + + foreach ($value as $key => $item) { + $value[$key] = $this->resolved($item, $changed); + } + + return $value; + } + + /** Whether a string below $value names the variable inside a longer text. */ + private function embeds(mixed $value): bool + { + if (is_string($value)) { + return VariableRef::contains($value) && str_contains($value, '"' . $this->id . '"'); + } + if (! is_array($value)) { + return false; + } + + foreach ($value as $item) { + if ($this->embeds($item)) { + return true; + } + } + + return false; + } +} diff --git a/tests/Unit/Integrations/Divi/Design/VariableDetachRewriteTest.php b/tests/Unit/Integrations/Divi/Design/VariableDetachRewriteTest.php new file mode 100644 index 0000000..5ed55e9 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/VariableDetachRewriteTest.php @@ -0,0 +1,177 @@ + ['value' => $value]]; + } + + /** + * @param list}> $blocks name and attributes, in the order of the column + * @return NodeIndex Blocks below s0.r0.c0 are m0, m1, … (g0, … for instances, x0, … for other blocks). + */ + private static function index(array $blocks): NodeIndex + { + $inner = ''; + foreach ($blocks as [$name, $attrs]) { + $inner .= sprintf('', $name, Json::encode($attrs)); + } + + return NodeIndex::build((new Parser())->parse( + '' . $inner + . '' + )); + } + + private static function node(NodeIndex $index, string $id): Block + { + $block = $index->find('s0.r0.c0.' . $id); + self::assertNotNull($block); + + return $block; + } + + public function testReplacesWholeNumberReferencesAtAnyDepth(): void + { + $index = self::index([['divi/button', [ + 'button' => ['decoration' => ['border' => self::desktop([ + 'radius' => ['topLeft' => self::ref(self::RADIUS), 'topRight' => self::ref(self::RADIUS)], + ])]], + 'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '10px']])]], + ]]]); + + $attrs = (new VariableDetachRewrite(self::RADIUS, '8px', 'numbers'))->rewrite(self::node($index, 'm0')); + + self::assertSame( + ['topLeft' => '8px', 'topRight' => '8px'], + $attrs['button']['decoration']['border']['desktop']['value']['radius'] ?? null + ); + self::assertSame('10px', $attrs['module']['decoration']['spacing']['desktop']['value']['padding']['top']); + } + + public function testAStringsVariableKeepsBeforeAndAfterOfTheReference(): void + { + $index = self::index([ + ['divi/text', ['content' => ['innerContent' => self::desktop( + self::ref(self::CLAIM, ['before' => 'Jetzt: ', 'after' => '!']) + )]]], + ['divi/text', ['content' => ['innerContent' => self::desktop(self::ref(self::CLAIM))]]], + ]); + $rewrite = new VariableDetachRewrite(self::CLAIM, 'Gut beraten', 'strings'); + + self::assertSame( + 'Jetzt: Gut beraten!', + $rewrite->rewrite(self::node($index, 'm0'))['content']['innerContent']['desktop']['value'] ?? null + ); + self::assertSame( + 'Gut beraten', + $rewrite->rewrite(self::node($index, 'm1'))['content']['innerContent']['desktop']['value'] ?? null + ); + } + + public function testOtherTypesIgnoreBeforeAndAfter(): void + { + self::assertSame( + 'Open Sans', + VariableDetachRewrite::replacement(self::ref('gvid-font000001', ['before' => 'x']), 'Open Sans', 'fonts') + ); + self::assertSame('a1b', VariableDetachRewrite::replacement(self::ref(self::CLAIM, [ + 'before' => 'a', 'after' => 'b', + ]), '1', 'strings')); + } + + public function testFontsAndOtherReferencesStayUntouched(): void + { + $color = VariableRef::serialize('color', 'gcid-primary-color', []); + $dynamic = VariableRef::serialize('content', 'post_title', []); + $index = self::index([ + ['divi/heading', [ + 'title' => ['decoration' => ['font' => ['font' => self::desktop([ + 'family' => self::ref('gvid-font000001'), + 'color' => $color, + ])]]], + 'content' => ['innerContent' => self::desktop($dynamic)], + ]], + ['divi/text', ['content' => ['innerContent' => self::desktop(self::ref(self::RADIUS))]]], + ['core/paragraph', ['content' => self::ref('gvid-font000001')]], + ]); + $rewrite = new VariableDetachRewrite('gvid-font000001', 'Open Sans', 'fonts'); + + $attrs = $rewrite->rewrite(self::node($index, 'm0')); + self::assertSame( + ['family' => 'Open Sans', 'color' => $color], + $attrs['title']['decoration']['font']['font']['desktop']['value'] ?? null + ); + self::assertSame($dynamic, $attrs['content']['innerContent']['desktop']['value']); + self::assertNull($rewrite->rewrite(self::node($index, 'm1')), 'another variable'); + self::assertNull($rewrite->rewrite(self::node($index, 'x0')), 'not a Divi block'); + } + + public function testEmbeddedReferencesStayAndAreNoted(): void + { + $embedded = 'Ab ' . self::ref(self::RADIUS) . ' Rundung'; + $index = self::index([ + ['divi/text', [ + 'content' => ['innerContent' => self::desktop($embedded)], + 'module' => ['decoration' => ['border' => self::desktop([ + 'radius' => ['topLeft' => self::ref(self::RADIUS)], + ])]], + ]], + ['divi/text', ['content' => ['innerContent' => self::desktop($embedded)]]], + ]); + $rewrite = new VariableDetachRewrite(self::RADIUS, '8px', 'numbers'); + $first = self::node($index, 'm0'); + + $attrs = $rewrite->rewrite($first); + self::assertSame($embedded, $attrs['content']['innerContent']['desktop']['value'] ?? null); + $first->setAttrs((array) $attrs); + self::assertSame( + ['s0.r0.c0.m0: Die Variable steht hier eingebettet in einem längeren Text; dieser Verweis bleibt stehen – ' + . 'im Divi-Builder ersetzen.'], + $rewrite->notes($first, 's0.r0.c0.m0') + ); + self::assertNull($rewrite->rewrite(self::node($index, 'm1')), 'only embedded: nothing to replace'); + self::assertSame( + [], + (new VariableDetachRewrite(self::CLAIM, 'x', 'strings'))->notes($first, 's0.r0.c0.m0'), + 'another variable is not noted' + ); + } + + public function testInstancesThatMentionTheIdConcernTheRewrite(): void + { + $index = self::index([ + ['divi/global-layout', ['globalModule' => '1058', 'blockName' => 'divi/text', 'localAttrs' => [ + 'content' => ['innerContent' => self::desktop(self::ref(self::RADIUS))], + ]]], + ['divi/global-layout', ['globalModule' => '1059', 'blockName' => 'divi/text']], + ]); + $rewrite = new VariableDetachRewrite(self::RADIUS, '8px', 'numbers'); + + self::assertTrue($rewrite->concernsInstance(self::node($index, 'g0'))); + self::assertFalse($rewrite->concernsInstance(self::node($index, 'g1'))); + } +}