feat: VariableDetachRewrite ersetzt ganze Verweise auf eine Variable durch ihren Wert (V9)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
116d866c61
commit
6e9218a2bc
@@ -124,8 +124,11 @@ final class DetachRewrite implements NodeRewrite
|
|||||||
return array_values(array_filter($ids, fn (mixed $item): bool => $item !== $this->id));
|
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 {
|
try {
|
||||||
if (str_contains(Json::encode($block->attrs()), $id)) {
|
if (str_contains(Json::encode($block->attrs()), $id)) {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||||
|
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the references to global variable $id in the attributes of Divi blocks by its value (lit/divi-design-detach
|
||||||
|
* with variable, ruling V9): every attribute value that is exactly one reference $variable({"type":"content",
|
||||||
|
* "value":{"name":"<id>",…}})$ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Json;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||||
|
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\VariableDetachRewrite;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class VariableDetachRewriteTest extends TestCase
|
||||||
|
{
|
||||||
|
private const RADIUS = 'gvid-radius0001';
|
||||||
|
private const CLAIM = 'gvid-claim00001';
|
||||||
|
|
||||||
|
/** A whole reference to global variable $id, as the Divi Builder stores it. */
|
||||||
|
public static function ref(string $id, array $settings = []): string
|
||||||
|
{
|
||||||
|
return VariableRef::serialize('content', $id, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A breakpoint map with one desktop value. */
|
||||||
|
private static function desktop(mixed $value): array
|
||||||
|
{
|
||||||
|
return ['desktop' => ['value' => $value]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{0: string, 1: array<string, mixed>}> $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('<!-- wp:%s %s /-->', $name, Json::encode($attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NodeIndex::build((new Parser())->parse(
|
||||||
|
'<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->' . $inner
|
||||||
|
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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')));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user