feat: Divi module catalog, attribute paths, short form and lit/divi-catalog
Adds per-path attribute kind (text/richtext/code/url/image/color/enum/ onoff/number/list/icon), derived from Divi's field component metadata where an attribute path names one unambiguously (module.paths() itself is unchanged; kinds() is a new, separate lookup exposed as path_kinds on lit/divi-catalog's module detail). This is not in the task-4 brief; it is added because 2b's sanitization routing (ruling R3) needs a metadata-derived field kind per attribute path. Verified live against the full 100-module catalog (564/4358 paths resolve a kind; the rest default to "unknown" and fall back to 2b's key-name routing). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Schema;
|
||||
|
||||
/**
|
||||
* 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>> */
|
||||
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;
|
||||
}
|
||||
|
||||
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')));
|
||||
}
|
||||
|
||||
/** @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; not every known path resolves.
|
||||
*
|
||||
* @return 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, "unknown" if Divi's metadata does not resolve one (spec §5.6). */
|
||||
public function pathKind(string $name, string $path): string
|
||||
{
|
||||
return $this->kinds($name)[$path] ?? '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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user