feat: Divi save pipeline – tree_rev lock, kses-safe save, revision, flags, cache, ToolResult

DiviSaver/DiviWriter/DiviComposition/DiviChange, WpValueSanitizer (S4),
WpMediaResolver, DiviFlags (S11), DynamicContentMap from Divi's live
registry (S22), S31 wiring, S35 control strip at apply() entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 14:56:47 +02:00
co-authored by Claude Opus 5.5
parent ea1fd607b2
commit 7762b76305
20 changed files with 1716 additions and 2 deletions
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
use LIT\MCP\Integrations\Divi\Compose\Composer;
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
use LIT\MCP\Integrations\Divi\Compose\StructureRules;
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
use LIT\MCP\Integrations\Divi\Compose\Warnings;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
/**
* Wires the pure Compose classes with the live catalog, design system, Divi's dynamic content
* registry and the WordPress sanitizer/media lookup – once per request and entry. Ruling S31:
* ONE Placeholders instance (carrying the live dynamic content map of the entry, S22) is shared
* by AttrWriter and ShortformWriter, and the same AttrWriter serves ShortformWriter and Composer.
*
* forPost() binds the composition to the entry being written (DiviWriter::composition() does it
* for the abilities); unbound, VariableRef's default map applies. Not final so abilities can be
* tested with a mock.
*/
class DiviComposition
{
private ?int $postId = null;
private ?Placeholders $placeholders = null;
private ?AttrWriter $attrWriter = null;
private ?Composer $composer = null;
public function __construct(
private DiviModules $modules,
private DesignSystemReader $design,
private ValueSanitizer $sanitizer = new WpValueSanitizer(),
private MediaResolver $media = new WpMediaResolver(),
private DynamicContentMap $dynamicContent = new DynamicContentMap()
) {
}
/** A fresh composition for entry $postId (own Placeholders with that entry's live map). */
public function forPost(int $postId): static
{
$bound = clone $this;
$bound->postId = $postId;
$bound->placeholders = null;
$bound->attrWriter = null;
$bound->composer = null;
return $bound;
}
/** The one Placeholders instance of this composition (ruling S31). */
public function placeholders(): Placeholders
{
return $this->placeholders ??= new Placeholders(
$this->design->read(),
$this->postId === null ? null : $this->dynamicContent->forPost($this->postId)
);
}
public function attrWriter(): AttrWriter
{
return $this->attrWriter ??= new AttrWriter($this->modules->catalog(), $this->placeholders(), $this->sanitizer);
}
public function composer(): Composer
{
return $this->composer ??= new Composer(
$this->modules->catalog(),
$this->rules(),
new ShortformWriter($this->placeholders(), $this->media, $this->attrWriter()),
$this->attrWriter(),
$this->design->read(),
$this->modules->builderVersion()
);
}
public function rules(): StructureRules
{
return new StructureRules($this->modules->catalog());
}
public function editor(): TreeEditor
{
return new TreeEditor($this->rules());
}
public function warnings(): Warnings
{
return new Warnings($this->design->read());
}
}