feat: lit-divi-update prüft jede Änderung im Schleifen-Kontext ihres Knotens – ein Composer je Kontext, neue loop-Attribute zählen (X3, S31)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
249bc7b932
commit
8888d246a9
@@ -7,7 +7,10 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
use LIT\MCP\Abilities\Annotations;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ColumnWidths;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
@@ -21,6 +24,9 @@ defined('ABSPATH') || exit;
|
||||
* come back together; only then are the attributes Composer::update() computed set. A node may
|
||||
* appear once per call (ruling S14); set and attrs of one change must not reach the same path
|
||||
* (ruling S38, checked by Composer::update()).
|
||||
*
|
||||
* Loop content (ruling X3): each change is checked in the loop scope of its node – the stored ancestors plus the
|
||||
* loop attrs this call gives the node or an ancestor – with one Composer per scope.
|
||||
*/
|
||||
final class DiviUpdateAbility extends DiviWriteAbility
|
||||
{
|
||||
@@ -123,14 +129,13 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
||||
]);
|
||||
}
|
||||
|
||||
$composer = $composition->composer();
|
||||
$errors = [];
|
||||
/** @var list<string> $invalidPaths */
|
||||
$invalidPaths = [];
|
||||
/** @var array<string, int> $seen id => position of its first change */
|
||||
$seen = [];
|
||||
/** @var list<array{0: Block, 1: array<string, mixed>, 2: string}> $updates */
|
||||
$updates = [];
|
||||
/** @var list<array{0: Block, 1: array<array-key, mixed>, 2: string}> $pending */
|
||||
$pending = [];
|
||||
foreach ($changes as $position => $change) {
|
||||
$field = sprintf('changes[%d]', $position);
|
||||
if (! is_array($change)) {
|
||||
@@ -159,10 +164,15 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $composer->update($block, $change, $field);
|
||||
$pending[] = [$block, $change, $field];
|
||||
}
|
||||
|
||||
/** @var list<array{0: Block, 1: array<string, mixed>, 2: string}> $updates */
|
||||
$updates = [];
|
||||
foreach (self::composeInLoops($page->index, $composition, $pending) as $position => $result) {
|
||||
$errors += $result['errors'];
|
||||
array_push($invalidPaths, ...$result['invalid_paths']);
|
||||
$updates[] = [$block, $result['attrs'], $field];
|
||||
$updates[] = [$pending[$position][0], $result['attrs'], $pending[$position][2]];
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
@@ -189,4 +199,46 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
||||
|
||||
return $change;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composer::update() of every pending change in its loop scope (ruling X3). First with the stored ancestors;
|
||||
* then changes whose scope the new attrs of this call move (a loop switched on, off or to another kind on the
|
||||
* node or an ancestor) are composed again in that scope – loop attrs are no client content, so the second
|
||||
* pass sees the same loops.
|
||||
*
|
||||
* @param list<array{0: Block, 1: array<array-key, mixed>, 2: string}> $pending
|
||||
* @return list<array{attrs: array<string, mixed>, errors: array<string, string>, invalid_paths: list<string>}>
|
||||
*/
|
||||
private static function composeInLoops(NodeIndex $index, DiviComposition $composition, array $pending): array
|
||||
{
|
||||
/** @var array<string, Composer> $composers query type ('' = no loop) => Composer of that scope */
|
||||
$composers = [];
|
||||
$scopes = [];
|
||||
$results = [];
|
||||
foreach ($pending as $position => [$block, $change, $field]) {
|
||||
$scopes[$position] = self::loopAt($index, $block);
|
||||
$results[$position] = self::composerFor($composition, $scopes[$position], $composers)
|
||||
->update($block, $change, $field);
|
||||
}
|
||||
|
||||
$newAttrs = [];
|
||||
foreach ($pending as $position => [$block]) {
|
||||
$newAttrs[(string) $index->idOf($block)] = $results[$position]['attrs'];
|
||||
}
|
||||
foreach ($pending as $position => [$block, $change, $field]) {
|
||||
$scope = self::loopAt($index, $block, $newAttrs);
|
||||
if ($scope->queryType() !== $scopes[$position]->queryType()) {
|
||||
$results[$position] = self::composerFor($composition, $scope, $composers)
|
||||
->update($block, $change, $field);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/** @param array<string, Composer> $composers One Composer per loop scope (S31 as reworded by X3). */
|
||||
private static function composerFor(DiviComposition $composition, LoopScope $scope, array &$composers): Composer
|
||||
{
|
||||
return $composers[$scope->queryType() ?? ''] ??= $composition->inLoop($scope)->composer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,4 +172,53 @@ final class DiviLoopWriteTest extends DiviWriteAbilityTestCase
|
||||
]]);
|
||||
self::assertTrue($withoutLoopContent['ok'], (string) json_encode($withoutLoopContent));
|
||||
}
|
||||
|
||||
/** Ruling X3: one Composer per loop scope – changes in a post loop and a term loop in one call. */
|
||||
public function testUpdateChecksEveryChangeInItsOwnLoop(): void
|
||||
{
|
||||
$result = $this->changeAll([
|
||||
['id' => 's0.r0.c0.m1', 'set' => ['content' => '{{loop:post_date}}']],
|
||||
['id' => 's0.r0.c1.m0', 'set' => ['content' => '{{loop:term_name}}']],
|
||||
['id' => 's0.r0.c2.m0', 'set' => ['content' => '<p>Neu</p>']],
|
||||
]);
|
||||
|
||||
self::assertTrue($result['ok'], (string) json_encode($result));
|
||||
self::assertStringContainsString('loop_term_name', $this->saved[0]);
|
||||
|
||||
$refused = $this->changeAll([
|
||||
['id' => 's0.r0.c0.m1', 'set' => ['content' => '{{loop:term_name}}']],
|
||||
['id' => 's0.r0.c2.m0', 'set' => ['content' => '{{loop:post_date}}']],
|
||||
]);
|
||||
$problems = self::errorFields($refused);
|
||||
self::assertStringContainsString('Typ: Beiträge', $problems['changes[0].set.content'] ?? '');
|
||||
self::assertStringContainsString('braucht eine Divi-Schleife', $problems['changes[1].set.content'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling X3: the scope of a change takes the loop attrs this call gives the node or an ancestor – a loop
|
||||
* switched on admits its content, a loop switched to another kind refuses the old kind's content.
|
||||
*/
|
||||
public function testUpdateUsesTheLoopsThisCallSets(): void
|
||||
{
|
||||
$switchedOn = $this->changeAll([
|
||||
['id' => 's0.r0.c2', 'attrs' => ['module.advanced.loop' => ['desktop' => self::TERM_LOOP]]],
|
||||
['id' => 's0.r0.c2.m0', 'set' => ['content' => '{{loop:term_name}}']],
|
||||
]);
|
||||
self::assertTrue($switchedOn['ok'], (string) json_encode($switchedOn));
|
||||
|
||||
$own = $this->changeAll([[
|
||||
'id' => 's0.r0.c0.m1',
|
||||
'attrs' => ['module.advanced.loop' => ['desktop' => self::TERM_LOOP]],
|
||||
'set' => ['content' => '{{loop:term_name}}'],
|
||||
]]);
|
||||
self::assertTrue($own['ok'], (string) json_encode($own));
|
||||
|
||||
$switchedKind = $this->changeAll([
|
||||
['id' => 's0.r0.c0', 'attrs' => ['module.advanced.loop' => ['desktop' => self::TERM_LOOP]]],
|
||||
['id' => 's0.r0.c0.m0', 'set' => ['content' => '{{loop:post_date}}']],
|
||||
]);
|
||||
$problems = self::errorFields($switchedKind);
|
||||
self::assertStringContainsString('Typ: Terme', $problems['changes[1].set.content'] ?? '');
|
||||
self::assertCount(2, $this->saved);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user