feat: lit/divi-outline and lit/divi-node-get with tree_rev

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 12:14:38 +02:00
co-authored by Claude Opus 5.5
parent a54a337e0b
commit d27504a750
9 changed files with 677 additions and 1 deletions
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Pages;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
/**
* Loads an entry for the Divi tools: rights and status via PostGuard (spec §4.5), then the
* block tree. Divi 4 shortcode content and unbalanced markup are refused with a hint –
* spec §3.4: Divi 4 content is kept, never edited. Not final so abilities can be tested
* with a mock.
*/
class DiviPageLoader
{
public const OUTLINE_HINT = 'Aktuellen Stand mit lit/divi-outline (direkt: lit-divi-outline) neu lesen und die '
. 'Änderung mit der neuen tree_rev wiederholen.';
public function __construct(private ContentSchema $schema, private Parser $parser = new Parser())
{
}
/** Readers pass $forWrite = false (trashed entries may be read, like lit-content-get). */
public function load(mixed $postId, bool $forWrite): DiviPage|WP_Error
{
$post = PostGuard::editable($postId, $this->schema, 'post_id', ! $forWrite);
if ($post instanceof WP_Error) {
return $post;
}
$content = (string) $post->post_content;
if (! str_contains($content, '<!-- wp:divi/') && str_contains($content, '[et_pb_')) {
return Errors::unsupported(
sprintf(
'Eintrag %d enthält Divi-4-Inhalt (Shortcodes); die Divi-Werkzeuge bearbeiten nur Divi 5.',
$post->ID
),
'Die Seite einmal im Divi-Builder öffnen und speichern – Divi 5 wandelt sie dabei um. Bis dahin '
. 'bleibt der Inhalt unverändert.'
);
}
try {
$document = $this->parser->parse($content);
} catch (MalformedMarkup $e) {
$message = sprintf('[wp-lit-mcp] divi parse of post %d failed: %s', $post->ID, $e->getMessage());
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return Errors::unsupported(
sprintf(
'Der Inhalt von Eintrag %d ist kein gültiges Block-Markup; nichts wurde gelesen oder geändert.',
$post->ID
),
'Die Seite im Divi-Builder öffnen und speichern (Divi repariert dabei die Struktur) oder den Nutzer '
. 'fragen, ob eine frühere Revision (lit/revisions-list) wiederhergestellt werden soll.'
);
}
return new DiviPage($post, $document, NodeIndex::build($document));
}
/**
* Additional checks for writing: the type must use the Divi builder, the content must be a
* Divi 5 layout (or empty) and saving must not publish a missed schedule (spec §2).
*/
public function writable(DiviPage $page): ?WP_Error
{
$post = $page->post;
if (! $this->schema->diviEnabled((string) $post->post_type)) {
return Errors::unsupported(
sprintf('Für den Inhaltstyp "%s" ist der Divi-Builder nicht aktiviert.', $post->post_type),
'In Divi → Theme-Optionen → Builder den Builder für diesen Inhaltstyp aktivieren (Administrator).'
);
}
if (! $page->isDiviLayout()) {
return Errors::unsupported(
sprintf('Eintrag %d ist kein Divi-Layout (Block-Editor- oder klassischer Inhalt).', $post->ID),
'Den Inhalt mit lit/content-update (direkt: lit-content-update) bearbeiten, oder den Nutzer fragen, ob '
. 'die Seite im Divi-Builder neu aufgebaut werden soll.'
);
}
return PostGuard::statusStable($post);
}
public static function unknownNode(string $id, WP_Post $post): WP_Error
{
return Errors::notFound(
sprintf('Knoten "%s" gibt es in Eintrag %d nicht.', $id, $post->ID),
self::OUTLINE_HINT
);
}
}