> Full name => metadata. */ private array $modules = []; /** @var array> */ private array $paths = []; /** @var array>> */ private array $kinds = []; /** @var callable(string): array */ private $defaults; /** * @param array $metadata Divi's metadata list (keys are ignored). * @param (callable(string): array)|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 */ 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 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|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 */ 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 */ 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> */ 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}> */ 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; } }