Files
WP-LIT-MCP/src/Integrations/Divi/Pages/DiviOutlineAbility.php
T

201 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}