feat: global instance in the builder's format, deglobalize, library content rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 00:53:38 +02:00
co-authored by Claude Opus 5.5
parent ce1ad7c6c3
commit 2700fd615a
11 changed files with 587 additions and 4 deletions
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\BlockTree;
defined('ABSPATH') || exit;
/**
* post_content of Divi Library entries (F4, fixtures stage3): the nodes sit in a divi/placeholder
* frame; a section/row/module entry has exactly one root, a layout entry sections. Pure PHP.
*/
final class LibraryContent
{
public const WRAPPER = 'divi/placeholder';
/**
* Parses library content for copying and unifies its line breaks (Container::unifyLineBreaks()).
* Never used for the content a tool edits in place – that keeps its bytes.
*
* @throws MalformedMarkup
*/
public static function load(Parser $parser, string $content): Document
{
$document = $parser->parse($content);
$document->unifyLineBreaks();
return $document;
}
/** post_content of a library entry holding $node, framed like the builder's "add to library". */
public static function wrap(Block $node): string
{
return '<!-- wp:' . self::WRAPPER . ' -->' . Serializer::block($node) . '<!-- /wp:' . self::WRAPPER . ' -->';
}
/** @return list<Block> Top-level nodes: the children of the first divi/placeholder, else of the document. */
public static function roots(Document $document): array
{
foreach ($document->children() as $block) {
if ($block->isPlaceholder()) {
return $block->children();
}
}
return $document->children();
}
/** Whether $block is or contains a global module. */
public static function containsGlobal(Block $block): bool
{
if ($block->isGlobal()) {
return true;
}
foreach ($block->children() as $child) {
if (self::containsGlobal($child)) {
return true;
}
}
return false;
}
/**
* Why the roots $after may not replace $before, or null. Instances on pages expect their
* entry's root type (blockName), so a section/row/module entry keeps exactly one root of the
* same type; a layout entry (or one with several roots) holds sections only. A global entry
* contains no global modules (F10: Divi forbids nesting).
*
* @param list<Block> $before
* @param list<Block> $after
*/
public static function shapeViolation(array $before, array $after, ?string $layoutType, bool $global): ?string
{
if ($after === []) {
return 'Der Bibliothekseintrag darf nicht leer werden.';
}
if ($layoutType === 'layout' || count($before) !== 1) {
foreach ($after as $root) {
if (GlobalInstance::effectiveName($root) !== 'divi/section') {
return 'Ein Layout-Eintrag der Bibliothek enthält nur Abschnitte.';
}
}
} else {
$expected = GlobalInstance::effectiveName($before[0]);
if (count($after) !== 1 || GlobalInstance::effectiveName($after[0]) !== $expected) {
return sprintf(
'Der Bibliothekseintrag muss genau ein %s bleiben (Instanzen auf den Seiten erwarten diesen Typ).',
$expected
);
}
}
if ($global) {
foreach ($after as $root) {
if (self::containsGlobal($root)) {
return 'Ein globaler Bibliothekseintrag darf keine globalen Module enthalten (Divi erlaubt keine '
. 'Verschachtelung).';
}
}
}
return null;
}
}