$input * @return array{container: Container, parent: ?Block, index: int}|string Position or German error. */ public function position(NodeIndex $index, array $input): array|string { $given = array_filter( [ 'parent' => $input['parent'] ?? null, 'before' => $input['before'] ?? null, 'after' => $input['after'] ?? null, ], static fn (mixed $value): bool => is_string($value) && $value !== '' ); if (count($given) > 1) { return 'nur eines von parent, before oder after angeben'; } $at = isset($input['index']) && is_int($input['index']) ? max(0, $input['index']) : PHP_INT_MAX; if ($given === []) { return ['container' => $index->root(), 'parent' => null, 'index' => $at]; } $key = (string) array_key_first($given); $node = $index->find((string) $given[$key]); if ($node === null) { return sprintf('Knoten "%s" gibt es nicht', (string) $given[$key]); } if ($key === 'parent') { return ['container' => $node, 'parent' => $node, 'index' => $at]; } $container = $index->parentOf($node); if ($container === null) { return sprintf('Knoten "%s" hat keinen Elternknoten', (string) $given[$key]); } $sibling = (int) $container->indexOf($node); return [ 'container' => $container, 'parent' => $container instanceof Block && ! $container->isPlaceholder() ? $container : null, 'index' => $key === 'before' ? $sibling : $sibling + 1, ]; } /** * @param array{container: Container, parent: ?Block, index: int} $position * @param list $blocks */ public function insert(array $position, array $blocks): ?string { foreach ($blocks as $block) { $violation = $this->rules->violation($position['parent'], GlobalInstance::effectiveName($block)) ?? $this->rules->nestedAreaViolation($block); if ($violation !== null) { return $violation; } } $container = $this->targetContainer($position['container']); $start = $position['index']; foreach (array_values($blocks) as $offset => $block) { $at = $start === PHP_INT_MAX ? PHP_INT_MAX : $start + $offset; $container->insertChild($at, $block); } return null; } /** @param array{container: Container, parent: ?Block, index: int} $position */ public function move(NodeIndex $index, Block $node, array $position): ?string { for ($ancestor = $position['parent']; $ancestor !== null; $ancestor = $index->parentBlock($ancestor)) { if ($ancestor === $node) { return 'ein Knoten kann nicht in sich selbst verschoben werden'; } } $violation = $this->rules->violation($position['parent'], GlobalInstance::effectiveName($node)); if ($violation !== null) { return $violation; } $from = $index->parentOf($node); if ($from === null) { return 'der Knoten hat keinen Elternknoten'; } $old = $from->removeChild($node); $target = $position['index']; if ($from === $position['container'] && $target !== PHP_INT_MAX && $old < $target) { --$target; } $position['container']->insertChild($target, $node); return null; } public function remove(NodeIndex $index, Block $node): void { $index->parentOf($node)?->removeChild($node); } /** Inserts a copy right after $node and returns it. Never touches $node or the copy afterwards. */ public function duplicate(NodeIndex $index, Block $node): Block { $copy = $node->copy(); $container = $index->parentOf($node); if ($container !== null) { $container->insertChild((int) $container->indexOf($node) + 1, $copy); } return $copy; } /** * Ruling S9: inserting at the document root of an empty page (no divi/placeholder yet) * first wraps it in one, the same way DiviContentComposer (Task 9) writes new content – * so a first-section page gets the structure the Visual Builder itself writes. */ private function targetContainer(Container $container): Container { if (! $container instanceof Document || $container->children() !== []) { return $container; } $placeholder = Block::create('divi/placeholder'); $container->insertChild(0, $placeholder); return $placeholder; } }