From ed6fe12ada89a725b3eecde253f0e5fd794b9be6 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 20:52:53 +0200 Subject: [PATCH] feat: lit-divi-move warnt, wenn Schleifen-Inhalte ihre Schleife verlassen oder in eine andere Art kommen (X3, A11) Co-Authored-By: Claude Sonnet 5.5 --- .../Divi/Pages/DiviMoveAbility.php | 64 ++++++++++++++++++- .../Divi/Pages/DiviLoopWriteTest.php | 37 +++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/Integrations/Divi/Pages/DiviMoveAbility.php b/src/Integrations/Divi/Pages/DiviMoveAbility.php index 2e2d253..1e79c56 100644 --- a/src/Integrations/Divi/Pages/DiviMoveAbility.php +++ b/src/Integrations/Divi/Pages/DiviMoveAbility.php @@ -6,6 +6,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\Compose\LoopContext; +use LIT\MCP\Integrations\Divi\Compose\LoopScope; +use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Integrations\Divi\Release\DiviFeature; use WP_Error; @@ -14,7 +18,8 @@ defined('ABSPATH') || exit; /** * lit/divi-move: moves a node (with everything inside) to parent / before / after – also into * another row or section, as long as the structure rules allow it (spec §5.7). Read-only - * nodes (global modules, foreign blocks) move as a whole, unchanged. + * nodes (global modules, foreign blocks) move as a whole, unchanged. Loop content that leaves its loop scope (out + * of a loop, into one of another kind) is moved with a warning – Divi allows it (rulings X3, A11). */ final class DiviMoveAbility extends DiviWriteAbility { @@ -76,6 +81,9 @@ final class DiviMoveAbility extends DiviWriteAbility return DiviErrors::position($position); } + // Both scopes from the tree before the move: the new parent never lies inside $node (TreeEditor::move()). + $from = self::loopAt($page->index, $page->index->parentBlock($node)); + $to = self::loopAt($page->index, $position['parent']); $problem = $editor->move($page->index, $node, $position); if ($problem !== null) { return Errors::invalidInput( @@ -84,6 +92,58 @@ final class DiviMoveAbility extends DiviWriteAbility ); } - return (new DiviChange(sprintf('Knoten %s verschoben', $id)))->record($node, 'moved'); + $change = (new DiviChange(sprintf('Knoten %s verschoben', $id)))->record($node, 'moved'); + $names = self::loopContent($node); + if ($names !== [] && $from->kind() !== $to->kind()) { + $change->warning(self::loopWarning($names, $from, $to)); + } + + return $change; + } + + /** + * The loop_* options $block and its descendants show in the scope around $block; a block that runs its own + * loop keeps its content's scope, read-only blocks are not entered. + * + * @return list + */ + private static function loopContent(Block $block): array + { + if ($block->isOpaque() || LoopContext::enabled($block->attrs())) { + return []; + } + + $names = []; + $attrs = $block->attrs(); + array_walk_recursive($attrs, static function (mixed $value) use (&$names): void { + $name = is_string($value) ? VariableRef::nameOf($value) : null; + if ($name !== null && str_starts_with($name, 'loop_')) { + $names[] = $name; + } + }); + foreach ($block->children() as $child) { + array_push($names, ...self::loopContent($child)); + } + + return array_values(array_unique($names)); + } + + /** @param list $names */ + private static function loopWarning(array $names, LoopScope $from, LoopScope $to): string + { + $where = $to->active() + ? sprintf( + 'jetzt in einer Divi-Schleife vom Typ: %s (vorher: %s); passen sie nicht zu dieser Art, zeigt Divi ' + . 'sie leer oder mit Daten eines anderen Objekts mit derselben ID', + $to->label(), + $from->active() ? $from->label() : 'keine Schleife' + ) + : 'jetzt außerhalb jeder Divi-Schleife; Divi zeigt sie dort leer'; + + return sprintf( + 'Schleifen-Inhalte (%s) liegen %s. Zurückverschieben oder die Inhalte anpassen.', + implode(', ', $names), + $where + ); } } diff --git a/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php b/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php index beb92a2..0c183d8 100644 --- a/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php +++ b/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php @@ -221,4 +221,41 @@ final class DiviLoopWriteTest extends DiviWriteAbilityTestCase self::assertStringContainsString('Typ: Terme', $problems['changes[1].set.content'] ?? ''); self::assertCount(2, $this->saved); } + + public function testMovingLoopContentOutOfItsLoopWarns(): void + { + $out = $this->moveNode([ + 'id' => 's0.r0.c0.m0', 'parent' => 's0.r0.c2', + ]); + self::assertTrue($out['ok'], 'Divi allows the move (A11): a warning, no refusal'); + $warnings = implode(' | ', $out['warnings']); + self::assertStringContainsString('loop_post_title', $warnings); + self::assertStringContainsString('außerhalb jeder Divi-Schleife', $warnings); + + $other = $this->moveNode([ + 'id' => 's0.r0.c2.m1', 'parent' => 's0.r0.c1', + ]); + self::assertStringContainsString('Typ: Terme (vorher: keine Schleife)', implode(' | ', $other['warnings'])); + } + + public function testMovesThatKeepTheLoopDoNotWarn(): void + { + // Path IDs are positions: after this move c0 holds [Fest, title, own loop]. + $withinLoop = $this->moveNode([ + 'id' => 's0.r0.c0.m0', 'after' => 's0.r0.c0.m1', + ]); + // m0 is the plain text now: nothing to warn about when it leaves the loop. + $plain = $this->moveNode([ + 'id' => 's0.r0.c0.m0', 'parent' => 's0.r0.c2', + ]); + // m1 is the module with its own term loop now: it takes the loop along. + $loopItself = $this->moveNode([ + 'id' => 's0.r0.c0.m1', 'parent' => 's0.r0.c2', + ]); + + foreach ([$withinLoop, $plain, $loopItself] as $result) { + self::assertTrue($result['ok'], (string) json_encode($result)); + self::assertStringNotContainsString('Schleife', implode(' | ', $result['warnings'])); + } + } }