Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
201 lines
7.6 KiB
PHP
201 lines
7.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||
|
||
use LIT\MCP\Abilities\Ability;
|
||
use LIT\MCP\Abilities\Annotations;
|
||
use LIT\MCP\Abilities\Errors;
|
||
use LIT\MCP\Abilities\Rev;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||
use WP_Error;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* lit/divi-outline: the page tree with path IDs, types, admin labels, text excerpts and
|
||
* preset names plus tree_rev (spec §5.3, §5.7). Every Divi write needs this tree_rev.
|
||
*/
|
||
final class DiviOutlineAbility implements Ability
|
||
{
|
||
public function __construct(private DiviPageLoader $loader, private DesignSystemReader $design)
|
||
{
|
||
}
|
||
|
||
public function name(): string
|
||
{
|
||
return 'lit/divi-outline';
|
||
}
|
||
|
||
public function label(): string
|
||
{
|
||
return 'Divi-Seitenbaum';
|
||
}
|
||
|
||
public function description(): string
|
||
{
|
||
return 'Liest den Aufbau einer Divi-Seite: Knoten mit Pfad-ID (s0, s0.r1, s0.r1.c0.m2), Modultyp, '
|
||
. 'Admin-Label, Textauszug und Preset. Liefert tree_rev – Pflicht für jede Divi-Änderung. '
|
||
. 'Optional nur ein Teilbaum (id), begrenzte Tiefe (depth) oder begrenzte Breite je Ebene (limit); '
|
||
. 'abgeschnittene Kinder zeigt "truncated".';
|
||
}
|
||
|
||
public function inputSchema(): array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'post_id' => ['type' => 'integer', 'minimum' => 1],
|
||
'id' => ['type' => 'string', 'description' => 'Nur diesen Teilbaum, z. B. "s1".'],
|
||
'depth' => [
|
||
'type' => 'integer',
|
||
'minimum' => 0,
|
||
'maximum' => 10,
|
||
'description' => 'Maximale Tiefe unterhalb des Startknotens (0 = nur der Knoten).',
|
||
],
|
||
'limit' => [
|
||
'type' => 'integer',
|
||
'minimum' => 1,
|
||
'maximum' => 500,
|
||
'description' => 'Höchstzahl Kind-Knoten je Elternknoten (Standard: '
|
||
. Outline::DEFAULT_LIMIT . '). Überzählige Kinder werden abgeschnitten (truncated).',
|
||
],
|
||
],
|
||
'required' => ['post_id'],
|
||
'additionalProperties' => false,
|
||
];
|
||
}
|
||
|
||
public function outputSchema(): ?array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'post_id' => ['type' => 'integer'],
|
||
'title' => ['type' => 'string'],
|
||
'status' => ['type' => 'string'],
|
||
'tree_rev' => ['type' => 'string'],
|
||
'rev' => ['type' => 'string'],
|
||
'outline' => ['type' => 'string'],
|
||
'nodes' => [
|
||
'type' => 'array',
|
||
'items' => [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'id' => ['type' => 'string'],
|
||
'type' => ['type' => 'string'],
|
||
'label' => ['type' => 'string'],
|
||
'excerpt' => ['type' => 'string'],
|
||
'info' => ['type' => 'string'],
|
||
'presets' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||
'children' => ['type' => 'integer'],
|
||
'editable' => ['type' => 'boolean'],
|
||
'depth' => ['type' => 'integer'],
|
||
'truncated' => [
|
||
'type' => 'boolean',
|
||
'description' => 'Weitere Kinder dieses Knotens wurden wegen "limit" abgeschnitten.',
|
||
],
|
||
'global_id' => [
|
||
'type' => ['string', 'null'],
|
||
'description' => 'Bibliotheks-ID eines globalen Moduls.',
|
||
],
|
||
],
|
||
'required' => [
|
||
'id', 'type', 'label', 'excerpt', 'info', 'presets', 'children', 'editable', 'depth',
|
||
'truncated', 'global_id',
|
||
],
|
||
],
|
||
],
|
||
'truncated' => [
|
||
'type' => 'boolean',
|
||
'description' => 'Irgendwo im Baum wurden Kind-Knoten wegen "limit" abgeschnitten (auch auf '
|
||
. 'oberster Ebene, die kein eigenes "truncated" an einem Knoten zeigen kann).',
|
||
],
|
||
'edit_url' => ['type' => 'string'],
|
||
'hints' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||
],
|
||
'required' => ['post_id', 'tree_rev', 'rev', 'outline', 'nodes', 'truncated', 'hints'],
|
||
];
|
||
}
|
||
|
||
public function annotations(): array
|
||
{
|
||
return Annotations::read();
|
||
}
|
||
|
||
public function isDirect(): bool
|
||
{
|
||
return true;
|
||
}
|
||
|
||
public function permission(array $input): bool|WP_Error
|
||
{
|
||
$page = $this->loader->load($input['post_id'] ?? null, false);
|
||
|
||
return $page instanceof WP_Error ? $page : true;
|
||
}
|
||
|
||
public function execute(array $input): mixed
|
||
{
|
||
$page = $this->loader->load($input['post_id'] ?? null, false);
|
||
if ($page instanceof WP_Error) {
|
||
return $page;
|
||
}
|
||
|
||
$rootId = isset($input['id']) && is_string($input['id']) && $input['id'] !== '' ? $input['id'] : null;
|
||
if ($rootId !== null && preg_match(NodeIndex::ID_PATTERN, $rootId) !== 1) {
|
||
return Errors::invalidInput('id ist keine Pfad-ID.', ['id' => 'z. B. "s0.r1.c0.m2"']);
|
||
}
|
||
if ($rootId !== null && $page->index->find($rootId) === null) {
|
||
return DiviErrors::unknownNode($rootId, $page->post);
|
||
}
|
||
|
||
$outline = new Outline($this->design->read()->presetNames());
|
||
$rows = $outline->rows(
|
||
$page->index,
|
||
$rootId,
|
||
isset($input['depth']) ? (int) $input['depth'] : null,
|
||
isset($input['limit']) ? (int) $input['limit'] : null
|
||
);
|
||
|
||
return [
|
||
'post_id' => (int) $page->post->ID,
|
||
'title' => (string) $page->post->post_title,
|
||
'status' => (string) $page->post->post_status,
|
||
'tree_rev' => $page->treeRev(),
|
||
'rev' => Rev::ofPost($page->post),
|
||
'outline' => $outline->text($rows),
|
||
'nodes' => $rows,
|
||
'truncated' => $outline->wasTruncated(),
|
||
'edit_url' => $page->editUrl(),
|
||
'hints' => $this->hints($page, $rows),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string, mixed>> $rows
|
||
* @return list<string>
|
||
*/
|
||
private function hints(DiviPage $page, array $rows): array
|
||
{
|
||
$hints = [];
|
||
if ($rows === []) {
|
||
$hints[] = $page->isDiviLayout()
|
||
? 'Die Seite hat noch keinen Divi-Inhalt: mit lit-divi-insert (ohne parent) den ersten Abschnitt '
|
||
. 'einfügen.'
|
||
: 'Der Inhalt ist kein Divi-Layout (Block-Editor oder klassisch); lit-content-get zeigt ihn.';
|
||
}
|
||
|
||
if (in_array(false, array_column($rows, 'editable'), true)) {
|
||
$hints[] = 'Knoten mit "nur lesbar" (globale Module, fremde Blöcke) lassen sich nur als Ganzes '
|
||
. 'verschieben, duplizieren oder löschen. Globale Module [global <ID>] im Bibliothekseintrag '
|
||
. 'bearbeiten: lit-divi-outline mit post_id <ID> – die Änderung wirkt auf alle Seiten.';
|
||
}
|
||
|
||
return $hints;
|
||
}
|
||
}
|