Files
WP-LIT-MCP/src/Integrations/Divi/Schema/ModuleCatalog.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

189 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\Schema;
defined('ABSPATH') || exit;
/**
* The modules this Divi installation knows (spec §5.7 lit-divi-catalog), built from
* ModuleRegistration::get_all_core_modules_metadata() – always live, never a copy.
* Pure PHP: the metadata array and a defaults callback are passed in.
*/
final class ModuleCatalog
{
/** @var array<string, array<string, mixed>> Full name => metadata. */
private array $modules = [];
/** @var array<string, list<string>> */
private array $paths = [];
/** @var array<string, array<string, string|array<string, string>>> */
private array $kinds = [];
/** @var callable(string): array<string, mixed> */
private $defaults;
/**
* @param array<array-key, mixed> $metadata Divi's metadata list (keys are ignored).
* @param (callable(string): array<string, mixed>)|null $defaults Default attributes per full module name.
*/
public function __construct(array $metadata, ?callable $defaults = null)
{
foreach ($metadata as $module) {
if (is_array($module) && is_string($module['name'] ?? null)) {
$this->modules[$module['name']] = $module;
}
}
ksort($this->modules);
$this->defaults = $defaults ?? static fn (string $name): array => [];
}
/** "button", "Button" or "divi/button" → "divi/button". */
public static function normalize(string $type): string
{
$type = strtolower(trim($type));
return str_contains($type, '/') ? $type : 'divi/' . $type;
}
/** "divi/button" → "button" (inverse of normalize(), for ShortformMap's short names). */
public static function shortName(string $name): string
{
return str_starts_with($name, 'divi/') ? substr($name, strlen('divi/')) : $name;
}
public function has(string $name): bool
{
return isset($this->modules[$name]);
}
/** @return list<string> */
public function names(): array
{
return array_map('strval', array_keys($this->modules));
}
public function title(string $name): string
{
$title = $this->modules[$name]['title'] ?? $name;
return is_string($title) ? $title : $name;
}
/** "module", "structure" or "child-module". */
public function category(string $name): string
{
$category = $this->modules[$name]['category'] ?? 'module';
return is_string($category) ? $category : 'module';
}
/** @return list<string> Modules allowed as children (childrenName and childModuleName). */
public function childrenOf(string $name): array
{
$module = $this->modules[$name] ?? [];
$children = is_array($module['childrenName'] ?? null) ? $module['childrenName'] : [];
if (is_string($module['childModuleName'] ?? null) && $module['childModuleName'] !== '') {
$children[] = $module['childModuleName'];
}
return array_values(array_unique(array_filter($children, 'is_string')));
}
/**
* Theme Builder areas Divi renders the module in (appearance.themeBuilderLayouts, e.g. divi/post-content:
* ["body"]; ModuleRegistration.php:509-527); null = no limit.
*
* @return list<string>|null
*/
public function themeBuilderAreas(string $name): ?array
{
$areas = $this->modules[$name]['appearance']['themeBuilderLayouts'] ?? null;
if (! is_array($areas)) {
return null;
}
$areas = array_values(array_filter($areas, 'is_string'));
return $areas === [] ? null : $areas;
}
/** @return list<string> */
public function paths(string $name): array
{
if (! isset($this->paths[$name])) {
$attributes = $this->modules[$name]['attributes'] ?? [];
$defaults = $this->has($name) ? ($this->defaults)($name) : [];
$this->paths[$name] = AttrPathIndex::paths(
is_array($attributes) ? $attributes : [],
is_array($defaults) ? $defaults : []
);
}
return $this->paths[$name];
}
public function isKnownPath(string $name, string $path): bool
{
return in_array($path, $this->paths($name), true);
}
/** @return list<string> */
public function suggestions(string $name, string $path): array
{
return AttrPathIndex::suggest($path, $this->paths($name));
}
/**
* Known attribute path => value kind for this module (spec §5.6, ruling R3), wherever
* Divi's metadata names an unambiguous field component – or, for a composite path (several
* named sub-fields under one path, e.g. buttonOne.innerContent's "text"/"linkUrl"), a map
* subName => kind (see AttrPathIndex::kinds()). Not every known path, or every sub-field of
* a composite one, resolves.
*
* @return array<string, string|array<string, string>>
*/
public function kinds(string $name): array
{
if (! isset($this->kinds[$name])) {
$attributes = $this->modules[$name]['attributes'] ?? [];
$this->kinds[$name] = AttrPathIndex::kinds(is_array($attributes) ? $attributes : []);
}
return $this->kinds[$name];
}
/**
* Value kind of one path, or of one named sub-field ($subName) of a composite path;
* "unknown" if Divi's metadata does not resolve one (spec §5.6) – including a composite
* path asked about without a $subName, or for a sub-field the metadata never names.
*/
public function pathKind(string $name, string $path, ?string $subName = null): string
{
$kind = $this->kinds($name)[$path] ?? null;
if (is_string($kind)) {
return $kind;
}
return is_array($kind) && $subName !== null ? ($kind[$subName] ?? 'unknown') : 'unknown';
}
/** @return list<array{name: string, title: string, category: string, children: list<string>}> */
public function summary(): array
{
$rows = [];
foreach ($this->names() as $name) {
$rows[] = [
'name' => $name,
'title' => $this->title($name),
'category' => $this->category($name),
'children' => $this->childrenOf($name),
];
}
return $rows;
}
}