- lit/divi-node-get now also returns the post-level rev (Rev::ofPost, same
optimistic lock lit-content-update needs) next to tree_rev, and reports a
node's type as the short name ("button"), matching lit-divi-outline instead
of the namespaced "divi/button". description() notes that $variable(...)$
values shown are re-validated when written, never taken over unchecked.
- lit/divi-outline validates its optional `id` input against the same
path-ID pattern lit-divi-node-get uses (a malformed id is now a validation
error, not a "not found"), and its outputSchema's per-node `required` list
now includes info/presets/children, which are always present.
- NodeSummary::label() (the admin label shown in outlines and node-get)
whitespace-normalizes and bounds its result via Text::shorten(), like every
other summary string, instead of returning a raw, unbounded value.
- docs/superpowers/notes/.../etappe-2a-abnahme.md corrected: only a global
module is "nur lesbar" - a specialty section is a normal, editable
divi/section block; the outline just marks it "[specialty]".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
168 lines
5.9 KiB
PHP
168 lines
5.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||
|
||
use LIT\MCP\Abilities\Ability;
|
||
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;
|
||
|
||
/**
|
||
* 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) oder begrenzte Tiefe (depth).';
|
||
}
|
||
|
||
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).',
|
||
],
|
||
],
|
||
'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'],
|
||
],
|
||
'required' => [
|
||
'id', 'type', 'label', 'excerpt', 'info', 'presets', 'children', 'editable', 'depth',
|
||
],
|
||
],
|
||
],
|
||
'edit_url' => ['type' => 'string'],
|
||
'hints' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||
],
|
||
'required' => ['post_id', 'tree_rev', 'rev', 'outline', 'nodes', 'hints'],
|
||
];
|
||
}
|
||
|
||
public function annotations(): array
|
||
{
|
||
return ['readonly' => true, 'destructive' => false, 'idempotent' => true];
|
||
}
|
||
|
||
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 DiviPageLoader::unknownNode($rootId, $page->post);
|
||
}
|
||
|
||
$outline = new Outline($this->design->read()->presetNames());
|
||
$rows = $outline->rows($page->index, $rootId, isset($input['depth']) ? (int) $input['depth'] : 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,
|
||
'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.'
|
||
: '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) ändern die Divi-Werkzeuge nicht; '
|
||
. 'ihren Inhalt nur im Divi-Builder bearbeiten.';
|
||
}
|
||
|
||
return $hints;
|
||
}
|
||
}
|