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>
189 lines
6.3 KiB
PHP
189 lines
6.3 KiB
PHP
<?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;
|
||
}
|
||
}
|