Files
WP-LIT-MCP/src/Integrations/Divi/Compose/StructureRules.php
T
janniclas.loosenandClaude Opus 5.5 b1dd35abd8 feat: Divi tools edit Theme Builder layouts; visible layouts need confirm
TB layouts load through ThemeBuilderAccess (P12), live layouts (attached +
enabled, Q8) need a dry run and confirm (Q9) and get the shared restore point
of VisibleEdits, extracted from GlobalEdits (Q2: backup when revisions are
off, P9 refusals, P14 naming). divi/post-content only in body layouts (F22),
also for nested stored nodes. SaveScope::ThemeBuilderLayout sets Divi flags
and clears site CSS + TB cache.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 15:45:07 +02:00

165 lines
6.3 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\Compose;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
defined('ABSPATH') || exit;
/**
* Which node may sit where (spec §5.4 "Struktur"): page → sections; regular section → rows;
* fullwidth section → fullwidth modules (catalog category "fullwidth-module"); column/group →
* modules; column → a nested row too (Divi 5 row is "nestable": true; neither the server
* – BlockParserStore::is_nested_module() only adds et_pb_row_nested – nor the builder's drop
* rules limit the depth); parent module → its child modules (catalog). Columns only come with
* their row; specialty sections, specialty columns (module.advanced.specialtyColumns or
* row-inner children, ColumnModule.php:306-324) and read-only nodes (opaque, non-Divi) are not
* rebuilt. F22: in Theme Builder layouts only modules Divi renders there (appearance.themeBuilderLayouts,
* e.g. divi/post-content only in the body). Pure PHP.
*/
final class StructureRules
{
private const COLUMNS = ['divi/column', 'divi/column-inner'];
private const ROWS = ['divi/row', 'divi/row-inner'];
private const FULLWIDTH = 'fullwidth-module';
/** @param ?string $themeBuilderArea "header", "body" or "footer" when the entry is a Theme Builder layout. */
public function __construct(private ModuleCatalog $catalog, private ?string $themeBuilderArea = null)
{
}
/** Null when $child may be placed into $parent (null = top level of the page), else the German reason. */
public function violation(?Block $parent, string $child): ?string
{
if (! $this->catalog->has($child)) {
return sprintf('unbekanntes Modul "%s" (siehe lit-divi-catalog)', $child);
}
$area = $this->areaViolation($child);
if ($area !== null) {
return $area;
}
if ($parent === null) {
return $child === 'divi/section' ? null : 'auf oberster Ebene sind nur Abschnitte (section) erlaubt';
}
if (! $parent->isDivi() || $parent->isOpaque()) {
return 'der Zielknoten ist nur lesbar (globales Modul oder fremder Block)';
}
$parentName = $parent->name();
if ($parentName === 'divi/section') {
return $this->inSection($parent, $child);
}
if (in_array($parentName, self::ROWS, true)) {
return 'Spalten entstehen mit der Zeile ("columns"); eine neue Zeile mit der gewünschten Aufteilung '
. 'einfügen';
}
if (in_array($parentName, self::COLUMNS, true) && self::isSpecialtyColumn($parent)) {
return 'Spezialspalten (specialty) nur im Divi-Builder umbauen; Module in ihre inneren Spalten einfügen';
}
if (in_array($parentName, self::COLUMNS, true) || $parentName === 'divi/group') {
// A nested row: only straight into a (Divi 5) column – like the builder's drop target.
$nestedRow = $child === 'divi/row' && $parentName === 'divi/column';
return $this->isPlainModule($child) || $child === 'divi/group' || $nestedRow
? null
: sprintf('%s passt nicht in eine Spalte', $child);
}
$children = $this->catalog->childrenOf($parentName);
if ($children === []) {
return sprintf('%s kann keine Kind-Module enthalten', $parentName);
}
return in_array($child, $children, true)
? null
: sprintf('%s erlaubt nur %s als Kind', $parentName, implode(', ', $children));
}
/** A module that goes into a column: catalog category "module" (not "fullwidth-module"). */
public function isPlainModule(string $name): bool
{
return $this->catalog->category($name) === 'module';
}
/** A column of a specialty section: it holds inner rows, not modules. */
private static function isSpecialtyColumn(Block $column): bool
{
$specialty = AttrMap::desktop($column->attrs(), 'module.advanced.specialtyColumns');
if ($specialty !== null && $specialty !== '') {
return true;
}
foreach ($column->children() as $child) {
if ($child->name() === 'divi/row-inner') {
return true;
}
}
return false;
}
private function inSection(Block $section, string $child): ?string
{
$type = AttrMap::desktop($section->attrs(), 'module.advanced.type');
if ($type === 'specialty') {
return 'Spezialabschnitte (specialty) nur im Divi-Builder umbauen; Module in ihre Spalten einfügen';
}
if ($type === 'fullwidth') {
return $this->catalog->category($child) === self::FULLWIDTH
? null
: 'in einen Vollbreite-Abschnitt passen nur fullwidth-Module';
}
return $child === 'divi/row' ? null : 'ein Abschnitt enthält Zeilen (row)';
}
/**
* F22 for stored nodes that arrive with their children (copy, library entry, duplicate): the first module
* below $block that Divi does not render in this Theme Builder area; null for pages.
*/
public function nestedAreaViolation(Block $block): ?string
{
if ($this->themeBuilderArea === null) {
return null;
}
foreach ($block->children() as $child) {
$violation = $this->areaViolation(GlobalInstance::effectiveName($child))
?? $this->nestedAreaViolation($child);
if ($violation !== null) {
return $violation;
}
}
return null;
}
/** F22: in a Theme Builder layout, a module Divi renders only in other areas (Divi's own render rule). */
private function areaViolation(string $child): ?string
{
$areas = $this->catalog->themeBuilderAreas($child);
if ($areas === null || $this->themeBuilderArea === null || in_array($this->themeBuilderArea, $areas, true)) {
return null;
}
return sprintf(
'%s zeigt Divi im Bereich „%s“ nicht (nur %s)',
$child,
$this->themeBuilderArea,
implode(', ', $areas)
);
}
}