feat: Divi placeholders, strict $variable references and full-form attribute writer

VariableRef parses, validates and re-serializes every $variable(...)$ value
(S1/S2/S12); AttrWriter routes strings by catalog kind incl. per-subName
composite kinds and key-name URL fallback (S3/S3a), rejects odd value keys
(S13) and Divi HTML/attribute fields without unfiltered_html (S7);
ambiguous design-system labels fail with their ids (S13a); R2 finding
recorded (S5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 13:23:59 +02:00
co-authored by Claude Opus 5.5
parent 395e9644ac
commit 1ea2e16c49
10 changed files with 1417 additions and 0 deletions
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\Integrations\Divi\Schema\ModuleCatalogTest;
/**
* Compose classes wired with the real Divi 5.2.1 sample catalog, the sample design system and
* predictable fakes for sanitizing and media (no WordPress). $unfiltered switches the fake
* sanitizer between a user with and without unfiltered_html (ruling S7).
*/
final class ComposeFixture
{
public static function sanitizer(bool $unfiltered = false): ValueSanitizer
{
return new class ($unfiltered) implements ValueSanitizer {
public function __construct(private bool $unfiltered)
{
}
public function text(string $value): string
{
return trim(strip_tags($value));
}
public function html(string $value): string
{
return (string) preg_replace('#<script\b[^>]*>.*?</script>#is', '', $value);
}
public function code(string $value): string
{
return $value;
}
public function url(string $value): string
{
return str_starts_with(strtolower(trim($value)), 'javascript:') ? '' : trim($value);
}
public function allowsUnfilteredHtml(): bool
{
return $this->unfiltered;
}
};
}
public static function media(): MediaResolver
{
return new class implements MediaResolver {
public function image(int $id): ?array
{
return match ($id) {
33 => ['url' => 'https://kunde.test/wp-content/uploads/team.jpg', 'alt' => 'Unser Team'],
12 => ['url' => 'https://kunde.test/wp-content/uploads/halle.jpg', 'alt' => ''],
default => null,
};
}
};
}
public static function placeholders(): Placeholders
{
return new Placeholders(DesignSystemTest::sample());
}
public static function attrWriter(bool $unfiltered = false): AttrWriter
{
return new AttrWriter(ModuleCatalogTest::sampleCatalog(), self::placeholders(), self::sanitizer($unfiltered));
}
}