Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
168 lines
5.7 KiB
PHP
168 lines
5.7 KiB
PHP
<?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 reported through leftover() (ReportsLeftovers). Other references – colors, other
|
||
* variables, dynamic content – stay untouched. Pure PHP.
|
||
*/
|
||
final class VariableDetachRewrite implements NodeRewrite, ReportsLeftovers
|
||
{
|
||
private const EMBEDDED = 'Die Variable steht hier eingebettet in einem längeren Text; dieser Verweis bleibt '
|
||
. 'unverändert – im Divi-Builder prüfen.';
|
||
|
||
private const UNSAFE = 'Vor- oder Nachtext des Verweises enthält Markup oder einen Verweis; der Verweis bleibt '
|
||
. 'unverändert – im Divi-Builder prüfen.';
|
||
|
||
/** @param bool $markupAllowed Whether "<" and ">" may enter the content (unfiltered_html); markers never may. */
|
||
public function __construct(
|
||
private string $id,
|
||
private string $value,
|
||
private string $variableType,
|
||
private bool $markupAllowed = false
|
||
) {
|
||
}
|
||
|
||
/**
|
||
* Whether $text must not be inlined into content: it holds a reference marker (it never went through
|
||
* VariableRef::canonical()) or – without unfiltered_html – markup (D7, S1).
|
||
*/
|
||
public static function unsafe(string $text, bool $markupAllowed): bool
|
||
{
|
||
return str_contains($text, VariableRef::MARKER) || str_contains($text, VariableRef::END)
|
||
|| (! $markupAllowed && (str_contains($text, '<') || str_contains($text, '>')));
|
||
}
|
||
|
||
/**
|
||
* 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() || $this->hasUnsafeAffix($block->attrs())) {
|
||
return null; // fail closed: the block stays whole and leftover() reports it
|
||
}
|
||
|
||
$changed = false;
|
||
$attrs = $this->resolved($block->attrs(), $changed);
|
||
|
||
return $changed && is_array($attrs) ? $attrs : null;
|
||
}
|
||
|
||
public function notes(Block $block, string $nodeId): array
|
||
{
|
||
return []; // leftovers go through leftover(): they also matter for blocks rewrite() did not change
|
||
}
|
||
|
||
public function leftover(Block $block): ?string
|
||
{
|
||
if (! $block->isDivi()) {
|
||
return null;
|
||
}
|
||
if ($this->hasUnsafeAffix($block->attrs())) {
|
||
return self::UNSAFE;
|
||
}
|
||
|
||
return $this->embeds($block->attrs()) ? self::EMBEDDED : null;
|
||
}
|
||
|
||
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 whole reference below $value carries before/after text that must not be inlined. */
|
||
private function hasUnsafeAffix(mixed $value): bool
|
||
{
|
||
if (is_string($value)) {
|
||
if ($this->variableType !== 'strings' || VariableRef::nameOf($value) !== $this->id) {
|
||
return false;
|
||
}
|
||
$settings = VariableRef::settingsOf($value);
|
||
|
||
foreach (['before', 'after'] as $key) {
|
||
$text = is_string($settings[$key] ?? null) ? $settings[$key] : '';
|
||
if (self::unsafe($text, $this->markupAllowed)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
if (! is_array($value)) {
|
||
return false;
|
||
}
|
||
foreach ($value as $item) {
|
||
if ($this->hasUnsafeAffix($item)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/** 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;
|
||
}
|
||
}
|