feat: lit-library-insert as copy or global instance in the builder's format (E5)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 02:27:16 +02:00
co-authored by Claude Opus 5.5
parent 5035092805
commit 7fe91c2bdb
9 changed files with 580 additions and 8 deletions
+7 -3
View File
@@ -116,8 +116,11 @@ final class DiviIntegration implements Integration
];
}
/** The group "Divi-Bibliothek" (lit/library-*), sharing this integration's library access. */
public function library(): LibraryIntegration
/**
* The group "Divi-Bibliothek" (lit/library-*), sharing this integration's library access;
* $diviWritable is whether lit/divi-insert is enabled (lit-library-insert follows it, audit M2).
*/
public function library(?Closure $diviWritable = null): LibraryIntegration
{
$loader = $this->loader();
@@ -128,7 +131,8 @@ final class DiviIntegration implements Integration
$this->writer($loader),
new DiviSaver(),
$this->log,
$this->modules
$this->modules,
$diviWritable
);
}
@@ -148,6 +148,16 @@ class LibraryAccess
return $this->roles->refusal('edit_global_library');
}
/**
* Inserting from the library into a page (E3): canList(), the role option add_library (Divi's
* "Aus der Bibliothek hinzufügen") and a group "Divi-Bibliothek" that is not read-only or
* disabled (E9). Reading the entry itself stays item() (ruling P5).
*/
public function canInsert(): ?WP_Error
{
return $this->canList() ?? $this->roles->refusal('add_library') ?? $this->writableGate();
}
/** Refusal while the Tools tab keeps this connection from writing to the library. */
public function writableGate(): ?WP_Error
{
@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use Closure;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Pages\DiviChange;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviErrors;
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviSchema;
use LIT\MCP\Integrations\Divi\Pages\DiviWriteAbility;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\WordPress\PostGuard;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/library-insert (spec §5.7, E5): a Divi Library entry into a Divi page – as the global instance
* the Visual Builder writes (GlobalInstance::of; Divi renders the entry's current content, F7) or
* as an independent copy of its roots (a global entry deglobalized like Divi's /load, F10). The
* target works like lit-divi-insert (tree_rev lock, StructureRules by effective name, all or
* nothing, DiviWriter); the entry is read through LibraryAccess::item() (ruling P5), never nests
* instances (ruling P11), and its nodes pass StoredNodeGuard (E6). Needs add_library (E3) and
* follows lit-divi-insert's Tools-tab state (audit M2).
*/
final class LibraryInsertAbility extends DiviWriteAbility
{
private const MODES = ['copy', 'global'];
/** @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled; null = always. */
public function __construct(
DiviWriter $writer,
private LibraryAccess $access,
private ?Closure $diviWritable = null,
private Parser $parser = new Parser()
) {
parent::__construct($writer);
}
public function name(): string
{
return 'lit/library-insert';
}
public function label(): string
{
return 'Aus der Divi-Bibliothek einfügen';
}
public function description(): string
{
return 'Fügt einen Eintrag der Divi-Bibliothek in eine Divi-Seite ein: mode "global" als verknüpfte Instanz '
. '(der Inhalt kommt immer aus der Bibliothek; Änderungen dort wirken auf allen Seiten) oder mode "copy" '
. 'als unabhängige Kopie. Standard: global für globale Einträge, sonst copy. Einträge über '
. 'lit/library-list; Position über parent, before oder after (Pfad-IDs aus lit-divi-outline), sonst ans '
. 'Seitenende. tree_rev ist Pflicht; alles oder nichts.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge(DiviSchema::write(), [
'library_id' => ['type' => 'integer', 'minimum' => 1, 'description' => 'ID aus lit/library-list.'],
'mode' => ['type' => 'string', 'enum' => self::MODES],
], DiviSchema::position()),
'required' => ['post_id', 'tree_rev', 'library_id'],
'additionalProperties' => false,
];
}
public function annotations(): array
{
return Annotations::write(false);
}
protected function precheck(array $input): ?WP_Error
{
$refusal = $this->access->canInsert();
if ($refusal !== null) {
return $refusal;
}
if ($this->diviWritable !== null && ! ($this->diviWritable)()) {
return Errors::unsupported(
'Divi-Änderungen sind für diese Verbindung abgeschaltet (Gruppe „Divi“ auf „nur lesen“ oder '
. 'lit-divi-insert deaktiviert).',
'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.'
);
}
return null;
}
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
{
$mode = $input['mode'] ?? null;
if ($mode !== null && ! in_array($mode, self::MODES, true)) {
return Errors::invalidInput('mode ist „copy“ oder „global“; nichts wurde gespeichert.', [
'mode' => 'copy oder global',
]);
}
$item = $this->item($input['library_id'] ?? null);
if ($item instanceof WP_Error) {
return $item;
}
$id = (int) $item->ID;
$global = $this->access->isGlobal($item);
$mode = is_string($mode) ? $mode : ($global ? 'global' : 'copy');
if ($mode === 'global' && ! $global) {
return Errors::invalidInput(
'Nur globale Bibliothekseinträge lassen sich als Instanz einfügen; nichts wurde gespeichert.',
['mode' => 'der Eintrag ist nicht global – mode "copy" verwenden']
);
}
$blocks = $this->blocks($item, $mode === 'global', $global);
if ($blocks instanceof WP_Error) {
return $blocks;
}
$guard = $composition->storedGuard();
foreach ($blocks as $block) {
$problems = $guard->problems($block);
if ($problems !== []) {
return Errors::invalidInput(
'Der Bibliothekseintrag enthält Einstellungen, die nur im Divi-Builder übertragen werden dürfen; '
. 'nichts wurde gespeichert.',
['library_id' => implode('; ', $problems)]
);
}
}
$editor = $composition->editor();
$position = $editor->position($page->index, $input);
if (is_string($position)) {
return DiviErrors::position($position);
}
$violation = $editor->insert($position, $blocks);
if ($violation !== null) {
return Errors::invalidInput(
'Der Bibliothekseintrag passt nicht an diese Stelle; nichts wurde gespeichert.',
['library_id' => $violation]
);
}
$change = new DiviChange(sprintf(
'Bibliothekseintrag „%s“ (%d) %s eingefügt',
(string) $item->post_title,
$id,
$mode === 'global' ? 'als globale Instanz' : 'als Kopie'
));
foreach ($blocks as $block) {
$change->record($block, 'inserted');
}
if ($mode === 'global') {
$change->warning(sprintf(
'Globale Instanz: den Inhalt liefert Bibliothekseintrag %d; Änderungen dort wirken auf alle Seiten mit '
. 'diesem Modul (lit-divi-outline mit post_id %d).',
$id,
$id
));
}
return $change;
}
/**
* The active library entry $libraryId after LibraryAccess' read checks (ruling P5: the library
* right and edit_post on the entry), or the refusal. Anything else is not found.
*/
private function item(mixed $libraryId): WP_Post|WP_Error
{
$id = PostGuard::id($libraryId);
if ($id === null) {
return Errors::invalidInput('library_id fehlt oder ist keine ID; nichts wurde gespeichert.', [
'library_id' => 'ID aus lit/library-list',
]);
}
$item = $this->access->item($id, false);
if ($item instanceof WP_Error) {
return $item;
}
if (! $item instanceof WP_Post || (string) $item->post_status !== 'publish') {
return Errors::notFound(
sprintf('Bibliothekseintrag %d gibt es nicht (oder er ist nicht aktiv).', $id),
'Einträge mit lit/library-list auflisten.'
);
}
return $item;
}
/** @return list<Block>|WP_Error The blocks to insert. */
private function blocks(WP_Post $item, bool $instance, bool $global): array|WP_Error
{
$id = (int) $item->ID;
try {
$roots = LibraryContent::roots(LibraryContent::load($this->parser, (string) $item->post_content));
} catch (MalformedMarkup) {
return Errors::unsupported(
sprintf('Bibliothekseintrag %d ist kein gültiges Block-Markup; nichts wurde gespeichert.', $id),
'Den Eintrag im Divi-Builder öffnen und speichern.'
);
}
if ($roots === []) {
return Errors::invalidInput(
sprintf('Bibliothekseintrag %d ist leer; nichts wurde gespeichert.', $id),
['library_id' => 'leer']
);
}
if ($instance) {
return $this->instance($roots, $id);
}
return array_map(
static fn (Block $root): Block => $global ? GlobalInstance::deglobalize($root->copy()) : $root->copy(),
$roots
);
}
/**
* The one instance of entry $id (GlobalInstance::of). Ruling P11: never nested instances – the
* root must be a single readable node holding no global module (Divi forbids nesting, F10).
*
* @param list<Block> $roots
* @return list<Block>|WP_Error
*/
private function instance(array $roots, int $id): array|WP_Error
{
if (count($roots) !== 1) {
return Errors::invalidInput(
'Dieser globale Eintrag hat mehrere Wurzeln und lässt sich nur als Kopie einfügen; nichts wurde '
. 'gespeichert.',
['mode' => 'mode "copy" verwenden']
);
}
$root = $roots[0];
if ($root->isOpaque() || LibraryContent::containsGlobal($root)) {
return Errors::invalidInput(
sprintf(
'Bibliothekseintrag %d ist selbst ein globales Modul, enthält eines oder ist nicht lesbar; als '
. 'Instanz entstünde eine Verschachtelung, die Divi nicht erlaubt. Nichts wurde gespeichert.',
$id
),
['mode' => 'mode "copy" verwenden oder den Eintrag im Divi-Builder prüfen']
);
}
return [GlobalInstance::of($root, $id)];
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use Closure;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
@@ -15,13 +16,14 @@ use LIT\MCP\Integrations\Integration;
defined('ABSPATH') || exit;
/**
* The group "Divi-Bibliothek" (E9): lit/library-list, lit/library-save, later lit-library-insert.
* The group "Divi-Bibliothek" (E9): lit/library-list, lit/library-save and lit-library-insert (direct).
* Built by DiviIntegration::library() so it shares the library access (rights, Tools-tab gates –
* its id LibraryAccess::GROUP is what the P9 hiding and the write gate ask). Available with the
* Divi 5 builder, like the Divi group.
*/
final class LibraryIntegration implements Integration
{
/** @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled (audit M2); null = always. */
public function __construct(
private LibraryAccess $access,
private DiviPageLoader $loader,
@@ -29,7 +31,8 @@ final class LibraryIntegration implements Integration
private DiviWriter $writer,
private DiviSaver $saver,
private ChangeLog $log,
private DiviModules $modules
private DiviModules $modules,
private ?Closure $diviWritable = null
) {
}
@@ -60,6 +63,7 @@ final class LibraryIntegration implements Integration
$this->log,
$this->modules
),
new LibraryInsertAbility($this->writer, $this->access, $this->diviWritable),
];
}
}