fix: stop guessing a kind for composite Divi attribute paths

Review fix round 1 on the lit/divi-catalog task:
- AttrPathIndex::collectKinds() no longer records a kind from a
  component.type === "group" node (a composite of several sub-kinds,
  e.g. button.innerContent = text + link + target), and kinds() now
  drops any attrName with more than one distinct candidate kind
  instead of last-write-wins (e.g. imageIcon.innerContent had onoff +
  icon + image sub-fields sharing one attrName). Both previously
  produced a wrong-but-present kind, which is more dangerous for 2b's
  sanitization routing than "unknown". Verified against the committed
  blurb/button/fullwidth-header fixture entries and live against the
  full 100-module catalog.
- AttrPathIndex::fromDefaults() now builds on AttrMap::flatten()
  instead of a parallel walker.
- ModuleCatalog::shortName() replaces the duplicated
  substr($name, strlen('divi/')) in DiviCatalogAbility.

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:
2026-09-26 12:00:35 +02:00
co-authored by Claude Opus 5.5
parent 2f4dcf9ce4
commit 02ae76d4f3
5 changed files with 62 additions and 31 deletions
+32 -27
View File
@@ -93,7 +93,7 @@ final class AttrPathIndex
self::attrNames($settings, $paths);
}
self::fromDefaults($defaults, '', $paths);
self::fromDefaults($defaults, $paths);
$list = array_map('strval', array_keys($paths));
sort($list);
@@ -102,18 +102,30 @@ final class AttrPathIndex
/**
* Known attribute path => value kind (text/richtext/code/url/image/color/enum/onoff/number/
* list/icon), wherever Divi's metadata names an unambiguous field component for the path.
* Not every known path (see paths()) resolves to a kind; callers default to "unknown".
* list/icon), wherever Divi's metadata names exactly one unambiguous field component for the
* path. A path is left out (callers default to "unknown") when Divi's own component is a
* "group" (a composite of several sub-kinds, e.g. button.innerContent = text + link + target)
* or when several distinct "field" components share the same attrName with different kinds
* (e.g. imageIcon.innerContent = onoff useIcon + icon icon + image src) – a wrong-but-present
* kind would be more dangerous for 2b's sanitization routing than an honest "unknown".
*
* @param array<string, mixed> $attributes Metadata "attributes" of one module.
* @return array<string, string>
*/
public static function kinds(array $attributes): array
{
$kinds = ['module.meta.adminLabel' => 'text'];
$candidates = [];
foreach ($attributes as $definition) {
$settings = is_array($definition['settings'] ?? null) ? $definition['settings'] : [];
self::collectKinds($settings, $kinds);
self::collectKinds($settings, $candidates);
}
$kinds = ['module.meta.adminLabel' => 'text'];
foreach ($candidates as $attrName => $found) {
$unique = array_unique($found);
if (count($unique) === 1) {
$kinds[$attrName] = $unique[0];
}
}
return $kinds;
@@ -170,46 +182,39 @@ final class AttrPathIndex
}
/**
* @param array<array-key, mixed> $node
* @param array<string, string> $kinds
* Collects every (attrName => kind) candidate found, without yet resolving conflicts – a
* "group" component (a composite of several sub-kinds) never contributes a candidate.
*
* @param array<array-key, mixed> $node
* @param array<string, list<string>> $candidates
*/
private static function collectKinds(array $node, array &$kinds): void
private static function collectKinds(array $node, array &$candidates): void
{
$attrName = $node['attrName'] ?? null;
$componentName = $node['component']['name'] ?? null;
$component = is_array($node['component'] ?? null) ? $node['component'] : [];
$componentName = $component['name'] ?? null;
if (
is_string($attrName)
&& preg_match(self::ATTR_NAME_PATTERN, $attrName)
&& ($component['type'] ?? null) === 'field'
&& is_string($componentName)
&& isset(self::COMPONENT_KINDS[$componentName])
) {
$kinds[$attrName] = self::COMPONENT_KINDS[$componentName];
$candidates[$attrName][] = self::COMPONENT_KINDS[$componentName];
}
foreach ($node as $value) {
if (is_array($value)) {
self::collectKinds($value, $kinds);
self::collectKinds($value, $candidates);
}
}
}
/**
* @param array<array-key, mixed> $attrs
* @param array<string, bool> $paths
*/
private static function fromDefaults(array $attrs, string $prefix, array &$paths): void
/** @param array<string, mixed> $attrs */
private static function fromDefaults(array $attrs, array &$paths): void
{
foreach ($attrs as $key => $value) {
if ($prefix === '' && in_array($key, AttrMap::META_KEYS, true)) {
continue;
}
$path = $prefix === '' ? (string) $key : $prefix . '.' . $key;
if (AttrMap::isBreakpointMap($value)) {
$paths[$path] = true;
} elseif (is_array($value)) {
self::fromDefaults($value, $path, $paths);
}
foreach (array_keys(AttrMap::flatten($attrs)) as $path) {
$paths[$path] = true;
}
}
}