feat: lit-divi-insert prüft Schleifen-Inhalte im Kontext des Ziels; neue Schleife plus Inhalt in einem Aufruf fail closed (X3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 20:51:42 +02:00
co-authored by Claude Sonnet 5.5
parent 2f484b77ae
commit 249bc7b932
4 changed files with 254 additions and 4 deletions
@@ -6,6 +6,9 @@ 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\VariableRef;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use WP_Error;
@@ -17,6 +20,9 @@ defined('ABSPATH') || exit;
* validated first and every error is returned together; the blocks come only from Composer
* (client values pass its choke point) and enter the tree only through TreeEditor::insert(),
* which also wraps the first section of an empty page in divi/placeholder (ruling S9).
*
* Loop content (ruling X3): all new nodes share the loop scope of the target parent. A loop a new node of this
* call switches on counts only once it is saved – with loop content in the same call the insert fails closed.
*/
final class DiviInsertAbility extends DiviWriteAbility
{
@@ -97,7 +103,18 @@ final class DiviInsertAbility extends DiviWriteAbility
]);
}
$composed = $composition->composer()->compose($nodes, $position['parent'], 'nodes', $context);
$scope = self::loopAt($page->index, $position['parent']);
$composed = $composition->inLoop($scope)->composer()->compose($nodes, $position['parent'], 'nodes', $context);
if (self::startsLoop($composed['blocks']) && self::hasLoopContent($nodes)) {
return Errors::invalidInput(
'Schleifen-Inhalte unter einer neuen Divi-Schleife gehen nicht in einem Aufruf; nichts wurde '
. 'gespeichert.',
['nodes' => 'Die Schleife eines neuen Elements gilt erst nach dem Speichern: zuerst das Element mit '
. 'Schleife einfügen, dann die Schleifen-Inhalte mit lit-divi-insert (parent = Pfad-ID des '
. 'Elements) ergänzen']
);
}
if ($composed['errors'] !== []) {
return DiviErrors::composed($composed, 'Knoten sind ungültig; nichts wurde gespeichert.');
}
@@ -117,4 +134,38 @@ final class DiviInsertAbility extends DiviWriteAbility
return $change;
}
/**
* Whether one of $blocks or a descendant switches a loop on.
*
* @param list<Block> $blocks
*/
private static function startsLoop(array $blocks): bool
{
foreach ($blocks as $block) {
if (LoopContext::enabled($block->attrs()) || self::startsLoop($block->children())) {
return true;
}
}
return false;
}
/**
* Whether the input names loop content: a {{loop:…}} placeholder or a reference to a loop_* option.
*
* @param array<array-key, mixed> $nodes
*/
private static function hasLoopContent(array $nodes): bool
{
$found = false;
array_walk_recursive($nodes, static function (mixed $value) use (&$found): void {
$found = $found || (is_string($value) && (
preg_match('/^\s*\{\{\s*loop\s*:/', $value) === 1
|| str_starts_with((string) VariableRef::nameOf($value), 'loop_')
));
});
return $found;
}
}
@@ -6,6 +6,10 @@ namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\Compose\LoopContext;
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
@@ -114,4 +118,22 @@ abstract class DiviWriteAbility implements Ability, RequiresDiviFeatures
{
return null;
}
/**
* Ruling X3: the loop scope at $block – its nearest loop-enabled ancestor-or-self (LoopContext, X1); none at
* the top level. $newAttrs (node ID => attrs) replaces the stored attrs of nodes this call changes. The index
* is the tree loaded for this call, never a client value. Opaque blocks are never entered by NodeIndex, so one
* can only be $block itself – and every tool refuses to write into or onto it.
*
* @param array<string, array<string, mixed>> $newAttrs
*/
protected static function loopAt(NodeIndex $index, ?Block $block, array $newAttrs = []): LoopScope
{
$chain = [];
for ($node = $block; $node !== null; $node = $index->parentBlock($node)) {
array_unshift($chain, $newAttrs[(string) $index->idOf($node)] ?? $node->attrs());
}
return LoopContext::fromChain($chain);
}
}