refactor: ValueCleaner is the named choke point (H1)
The choke point moves out of AttrWriter into ValueCleaner (catalog, the composition's VariableRef, sanitizer): clean() (was AttrWriter::cleanValue), kindOf(), guardPath() and the private cleanNode/kind/unknownKind/tightens with VALUE_KEY, URL_KEY, URL_SEGMENT, STRICTNESS, BUILDER_ONLY (its docblock now sits on it; STRUCTURE stays in AttrWriter). Code of the pipeline moved unchanged. - ValueCleaner's docblock is the one canonical description: steps 0-8 with their rulings in code order, invariants I1-I3; the step comments in cleanNode carry the same numbers. AttrWriter/ShortformWriter/Composer/TextReplacer docblocks point there and keep only their own rules. - ValueCleaner::textReplacementFor($module) holds the D10 rule that was a closure in DiviReplaceTextAbility::replace (text/html only, clean() by kind). - AttrWriter, ShortformWriter and Composer take the ValueCleaner; DiviComposition builds one per composition (cleaner()). Tests: the eight AttrWriterTest methods that only exercised cleanValue/kindOf moved verbatim to ValueCleanerTest (clean/kindOf, ComposeFixture::cleaner()); mixed tests call the cleaner in place; +1 test for textReplacementFor. Every provider row and per-test assertion count unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -11,6 +11,7 @@ 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\ValueCleaner;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Warnings;
|
||||
@@ -20,9 +21,9 @@ 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 VariableRef (carrying the live dynamic content map of the entry, S22) and ONE Placeholders
|
||||
* instance are shared by AttrWriter and ShortformWriter, and the same AttrWriter serves
|
||||
* ShortformWriter and Composer.
|
||||
* ONE VariableRef (carrying the live dynamic content map of the entry, S22), ONE Placeholders
|
||||
* and ONE ValueCleaner instance are shared by AttrWriter, ShortformWriter and Composer, and the
|
||||
* same AttrWriter serves Composer and the abilities.
|
||||
*
|
||||
* 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
|
||||
@@ -33,6 +34,7 @@ class DiviComposition
|
||||
private ?int $postId = null;
|
||||
private ?VariableRef $references = null;
|
||||
private ?Placeholders $placeholders = null;
|
||||
private ?ValueCleaner $cleaner = null;
|
||||
private ?AttrWriter $attrWriter = null;
|
||||
private ?Composer $composer = null;
|
||||
|
||||
@@ -52,6 +54,7 @@ class DiviComposition
|
||||
$bound->postId = $postId;
|
||||
$bound->references = null;
|
||||
$bound->placeholders = null;
|
||||
$bound->cleaner = null;
|
||||
$bound->attrWriter = null;
|
||||
$bound->composer = null;
|
||||
|
||||
@@ -73,14 +76,15 @@ class DiviComposition
|
||||
return $this->placeholders ??= new Placeholders($this->design->read());
|
||||
}
|
||||
|
||||
/** The one choke point of this composition (ValueCleaner, rulings S1–S3, S7, S13, S28, S32). */
|
||||
public function cleaner(): ValueCleaner
|
||||
{
|
||||
return $this->cleaner ??= new ValueCleaner($this->modules->catalog(), $this->references(), $this->sanitizer);
|
||||
}
|
||||
|
||||
public function attrWriter(): AttrWriter
|
||||
{
|
||||
return $this->attrWriter ??= new AttrWriter(
|
||||
$this->modules->catalog(),
|
||||
$this->placeholders(),
|
||||
$this->references(),
|
||||
$this->sanitizer
|
||||
);
|
||||
return $this->attrWriter ??= new AttrWriter($this->modules->catalog(), $this->placeholders(), $this->cleaner());
|
||||
}
|
||||
|
||||
public function composer(): Composer
|
||||
@@ -88,8 +92,9 @@ class DiviComposition
|
||||
return $this->composer ??= new Composer(
|
||||
$this->modules->catalog(),
|
||||
$this->rules(),
|
||||
new ShortformWriter($this->placeholders(), $this->media, $this->attrWriter()),
|
||||
new ShortformWriter($this->placeholders(), $this->media, $this->cleaner()),
|
||||
$this->attrWriter(),
|
||||
$this->cleaner(),
|
||||
$this->design->read(),
|
||||
$this->modules->builderVersion()
|
||||
);
|
||||
|
||||
@@ -29,8 +29,8 @@ use WP_Error;
|
||||
* (DiviWriter::composition). Should the entry's map refuse something (an option Divi offers
|
||||
* only for other types), write() returns the errors and lit/content-create deletes the draft.
|
||||
*
|
||||
* Every client string goes through Composer (AttrWriter / ShortformWriter) – the single choke
|
||||
* point that makes the kses bypass safe (spec D7).
|
||||
* Every client string goes through Composer (AttrWriter / ShortformWriter → ValueCleaner) – the
|
||||
* single choke point that makes the kses bypass safe (spec D7).
|
||||
*/
|
||||
final class DiviContentComposer implements ContentComposer
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ use WP_Error;
|
||||
* it is written; DiviWriter::save() stores it with its own restore-point revision and change
|
||||
* log. The batch reports every page (spec §8.2, ruling S10): any Throwable of one page is logged
|
||||
* and reported there, the others go on. Inserted text is cleaned by the field kind through
|
||||
* AttrWriter::cleanValue() (S2/S28); URL and code fields are never touched.
|
||||
* ValueCleaner::textReplacementFor() (S2/S28); URL and code fields are never touched.
|
||||
*/
|
||||
final class DiviReplaceTextAbility implements Ability
|
||||
{
|
||||
@@ -379,7 +379,7 @@ final class DiviReplaceTextAbility implements Ability
|
||||
|
||||
/**
|
||||
* Every hit of $page, computed without changing it. The replacement is cleaned per place by
|
||||
* the entry's AttrWriter (field kind; markers rejected); URL and code fields are skipped.
|
||||
* the entry's ValueCleaner (field kind; markers rejected); URL and code fields are skipped.
|
||||
*
|
||||
* @return array{count: int, samples: list<array<string, string>>,
|
||||
* blocks: list<array{Block, array<string, mixed>}>, skipped_nodes: list<array{id: string, reason: string}>}
|
||||
@@ -387,7 +387,7 @@ final class DiviReplaceTextAbility implements Ability
|
||||
*/
|
||||
private function replace(DiviPage $page, TextReplacer $replacer): array
|
||||
{
|
||||
$attrWriter = $this->writer->composition($page)->attrWriter();
|
||||
$cleaner = $this->writer->composition($page)->cleaner();
|
||||
$count = 0;
|
||||
$samples = [];
|
||||
$blocks = [];
|
||||
@@ -404,16 +404,8 @@ final class DiviReplaceTextAbility implements Ability
|
||||
continue;
|
||||
}
|
||||
|
||||
$module = $block->name();
|
||||
$clean = static function (string $path, ?string $key, string $value) use ($attrWriter, $module): ?string {
|
||||
$kind = $attrWriter->kindOf($module, $path, $key);
|
||||
|
||||
return $kind === 'text' || $kind === 'html'
|
||||
? (string) $attrWriter->cleanValue($module, $path, $value, $key)
|
||||
: null;
|
||||
};
|
||||
try {
|
||||
$result = $replacer->apply($block->attrs(), $clean);
|
||||
$result = $replacer->apply($block->attrs(), $cleaner->textReplacementFor($block->name()));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new InvalidArgumentException(sprintf('Knoten %s: %s', $entry['id'], $e->getMessage()), 0, $e);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use LIT\MCP\Integrations\Divi\Compose\ValueSanitizer;
|
||||
/**
|
||||
* WordPress sanitizing of Divi values (spec §5.6/§10). Because DiviSaver bypasses kses for the
|
||||
* page as a whole (kses would re-serialize every untouched block, D7), every value this plugin
|
||||
* writes goes through here instead – always via AttrWriter::cleanValue(), which also checks
|
||||
* writes goes through here instead – always via ValueCleaner::clean(), which also checks
|
||||
* $variable(…)$ references before and after these sanitizers.
|
||||
*
|
||||
* Rich text and code depend on unfiltered_html like in core (ruling S4): administrators keep
|
||||
|
||||
Reference in New Issue
Block a user