feat: DiviComposition::inLoop() – VariableRef und Placeholders mit Schleifen-Kontext; S31 je Kontext (X3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6da2c0090f
commit
2f484b77ae
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
|||||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||||
|
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
|
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
||||||
@@ -27,10 +28,11 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wires the pure Compose classes with the live catalog, design system, Divi's dynamic content
|
* 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:
|
* registry and the WordPress sanitizer/media lookup – once per request, entry and loop scope. Ruling S31
|
||||||
* ONE VariableRef (carrying the live dynamic content map of the entry, S22), ONE Placeholders
|
* (as reworded by X3): ONE VariableRef per composition (carrying the live dynamic content map of the entry, S22,
|
||||||
* and ONE ValueCleaner instance are shared by AttrWriter, ShortformWriter and Composer, and the
|
* plus the loop options of its LoopScope), ONE Placeholders and ONE ValueCleaner instance are shared by
|
||||||
* same AttrWriter serves Composer and the abilities.
|
* AttrWriter, ShortformWriter and Composer, and the same AttrWriter serves Composer and the abilities. A tool
|
||||||
|
* whose nodes sit in several loop scopes uses one composition – and so one Composer – per scope (inLoop()).
|
||||||
*
|
*
|
||||||
* Presets (D7) reach blocks through ONE PresetAssignment per composition: its book is Divi's stored preset
|
* Presets (D7) reach blocks through ONE PresetAssignment per composition: its book is Divi's stored preset
|
||||||
* inventory (DesignSystemWriter::presets()), read lazily and at most once – only when a write names a preset or a
|
* inventory (DesignSystemWriter::presets()), read lazily and at most once – only when a write names a preset or a
|
||||||
@@ -38,8 +40,9 @@ defined('ABSPATH') || exit;
|
|||||||
* Without a preset source (null, unit tests) there are no presets.
|
* Without a preset source (null, unit tests) there are no presets.
|
||||||
*
|
*
|
||||||
* forPost() binds the composition to the entry being written (DiviWriter::composition() does it
|
* 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
|
* for the abilities); unbound, VariableRef's default map applies. inLoop() binds it to the loop scope of the
|
||||||
* tested with a mock.
|
* nodes it composes (LoopContext); without it the scope is none and every loop_* option is refused. Not final so
|
||||||
|
* abilities can be tested with a mock.
|
||||||
*/
|
*/
|
||||||
class DiviComposition
|
class DiviComposition
|
||||||
{
|
{
|
||||||
@@ -51,6 +54,10 @@ class DiviComposition
|
|||||||
private ?AttrWriter $attrWriter = null;
|
private ?AttrWriter $attrWriter = null;
|
||||||
private ?Composer $composer = null;
|
private ?Composer $composer = null;
|
||||||
private ?PresetAssignment $assignment = null;
|
private ?PresetAssignment $assignment = null;
|
||||||
|
private LoopScope $loop;
|
||||||
|
|
||||||
|
/** @var array<string, array<string, string>>|null Loop options of $loop for the entry (read once). */
|
||||||
|
private ?array $loopFields = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DiviModules $modules,
|
private DiviModules $modules,
|
||||||
@@ -61,35 +68,40 @@ class DiviComposition
|
|||||||
private LinkTargets $links = new WpLinkTargets(),
|
private LinkTargets $links = new WpLinkTargets(),
|
||||||
private ?DesignSystemWriter $presetSource = null
|
private ?DesignSystemWriter $presetSource = null
|
||||||
) {
|
) {
|
||||||
|
$this->loop = LoopScope::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A fresh composition for entry $postId (own VariableRef with its live map; TB area for StructureRules). */
|
/** A fresh composition for entry $postId (own VariableRef with its live map; TB area for StructureRules). */
|
||||||
public function forPost(int $postId, ?string $themeBuilderArea = null): static
|
public function forPost(int $postId, ?string $themeBuilderArea = null): static
|
||||||
{
|
{
|
||||||
$bound = clone $this;
|
$bound = $this->fresh();
|
||||||
$bound->postId = $postId;
|
$bound->postId = $postId;
|
||||||
$bound->themeBuilderArea = $themeBuilderArea;
|
$bound->themeBuilderArea = $themeBuilderArea;
|
||||||
$bound->references = null;
|
|
||||||
$bound->placeholders = null;
|
|
||||||
$bound->cleaner = null;
|
|
||||||
$bound->attrWriter = null;
|
|
||||||
$bound->composer = null;
|
|
||||||
$bound->assignment = null;
|
|
||||||
|
|
||||||
return $bound;
|
return $bound;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The one checker of $variable(…)$ values of this composition, with the entry's live map (S22, S31).
|
* A fresh composition of the same entry for nodes inside $scope (ruling X3): its VariableRef and Placeholders
|
||||||
|
* know the loop options of that kind; the abilities take $scope from LoopContext.
|
||||||
|
*/
|
||||||
|
public function inLoop(LoopScope $scope): static
|
||||||
|
{
|
||||||
|
$bound = $this->fresh();
|
||||||
|
$bound->loop = $scope;
|
||||||
|
|
||||||
|
return $bound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one checker of $variable(…)$ values of this composition: the entry's live map (S22, S31) plus the loop
|
||||||
|
* options of its scope, and the scope itself for the loop messages (X5).
|
||||||
*
|
*
|
||||||
* @internal Public for tests; code uses cleaner() / composer().
|
* @internal Public for tests; code uses cleaner() / composer().
|
||||||
*/
|
*/
|
||||||
public function references(): VariableRef
|
public function references(): VariableRef
|
||||||
{
|
{
|
||||||
return $this->references ??= new VariableRef(
|
return $this->references ??= new VariableRef($this->design->read(), $this->dynamicContentMap(), $this->loop);
|
||||||
$this->design->read(),
|
|
||||||
$this->postId === null ? null : $this->dynamicContent->forPost($this->postId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +111,7 @@ class DiviComposition
|
|||||||
*/
|
*/
|
||||||
public function placeholders(): Placeholders
|
public function placeholders(): Placeholders
|
||||||
{
|
{
|
||||||
return $this->placeholders ??= new Placeholders($this->design->read(), $this->links);
|
return $this->placeholders ??= new Placeholders($this->design->read(), $this->links, $this->loopFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The one choke point of this composition (ValueCleaner, rulings S1–S3, S7, S13, S28, S32). */
|
/** The one choke point of this composition (ValueCleaner, rulings S1–S3, S7, S13, S28, S32). */
|
||||||
@@ -161,4 +173,37 @@ class DiviComposition
|
|||||||
{
|
{
|
||||||
return new Warnings($this->design->read(), $this->presets());
|
return new Warnings($this->design->read(), $this->presets());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<string, string>>|null The entry's live map with the scope's loop options. */
|
||||||
|
private function dynamicContentMap(): ?array
|
||||||
|
{
|
||||||
|
$map = $this->postId === null ? null : $this->dynamicContent->forPost($this->postId);
|
||||||
|
|
||||||
|
return $map === null ? null : $map + $this->loopFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<string, string>> Loop options of the OWN scope's kind only ([] without). */
|
||||||
|
private function loopFields(): array
|
||||||
|
{
|
||||||
|
$kind = $this->loop->kind();
|
||||||
|
|
||||||
|
return $this->loopFields ??= $this->postId === null || $kind === null
|
||||||
|
? []
|
||||||
|
: $this->dynamicContent->loopFields($kind, $this->postId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A clone without any cached instance (they hang on the entry and the loop scope). */
|
||||||
|
private function fresh(): static
|
||||||
|
{
|
||||||
|
$bound = clone $this;
|
||||||
|
$bound->references = null;
|
||||||
|
$bound->placeholders = null;
|
||||||
|
$bound->cleaner = null;
|
||||||
|
$bound->attrWriter = null;
|
||||||
|
$bound->composer = null;
|
||||||
|
$bound->assignment = null;
|
||||||
|
$bound->loopFields = null;
|
||||||
|
|
||||||
|
return $bound;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
|||||||
use LIT\MCP\Integrations\Divi\BlockTree\Document;
|
use LIT\MCP\Integrations\Divi\BlockTree\Document;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||||
|
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\StoredNodeGuard;
|
use LIT\MCP\Integrations\Divi\Compose\StoredNodeGuard;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
|
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
|
||||||
@@ -265,4 +266,46 @@ final class DiviCompositionTest extends TestCase
|
|||||||
self::assertCount(1, $covering);
|
self::assertCount(1, $covering);
|
||||||
self::assertStringContainsString('„CTA primär“', $covering[0]);
|
self::assertStringContainsString('„CTA primär“', $covering[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruling X3: inLoop() gives a fresh composition bound to a loop scope – placeholders and references of the
|
||||||
|
* entry's map plus the loop options of that kind; outside a loop and in another kind loop content fails.
|
||||||
|
*/
|
||||||
|
public function testInLoopChecksLoopContentAgainstItsScope(): void
|
||||||
|
{
|
||||||
|
$post = $this->composition()->forPost(7);
|
||||||
|
$inPostLoop = $post->inLoop(LoopScope::ofQueryType('post_types'));
|
||||||
|
$compose = static fn (DiviComposition $composition, string $content = '{{loop:post_title}}'): array => $composition
|
||||||
|
->composer()->compose(
|
||||||
|
[['type' => 'text', 'content' => $content]],
|
||||||
|
Block::create('divi/column'),
|
||||||
|
'nodes'
|
||||||
|
);
|
||||||
|
|
||||||
|
$inside = $compose($inPostLoop);
|
||||||
|
self::assertSame([], $inside['errors']);
|
||||||
|
self::assertSame(
|
||||||
|
VariableRef::serialize('content', 'loop_post_title', ['before' => '', 'after' => '']),
|
||||||
|
AttrMap::desktop($inside['blocks'][0]->attrs(), 'content.innerContent')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Placeholders get the loop fields: the short form carries before/after; outside a loop it is refused.
|
||||||
|
$date = $compose($inPostLoop, '{{loop:post_date}}');
|
||||||
|
self::assertSame([], $date['errors']);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'loop_post_date',
|
||||||
|
(string) AttrMap::desktop($date['blocks'][0]->attrs(), 'content.innerContent')
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('braucht eine Divi-Schleife', implode(' ', $compose($post, '{{loop:post_date}}')['errors']));
|
||||||
|
|
||||||
|
self::assertStringContainsString('braucht eine Divi-Schleife', implode(' ', $compose($post)['errors']));
|
||||||
|
self::assertStringContainsString(
|
||||||
|
'passt nicht zur Schleife dieses Elements (Typ: Terme)',
|
||||||
|
implode(' ', $compose($post->inLoop(LoopScope::ofQueryType('post_taxonomies')))['errors'])
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNotSame($post->references(), $inPostLoop->references());
|
||||||
|
self::assertNotSame($post->placeholders(), $inPostLoop->placeholders());
|
||||||
|
self::assertSame($inPostLoop->references(), $inPostLoop->references(), 'one VariableRef per scope');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user