feat: per-subName attribute-path kinds for composite Divi paths
lit/divi-catalog's path_kinds previously named one kind per whole attribute path, built from Divi's metadata component name alone. For a composite path (Divi groups several named sub-fields, "subName", under one path – e.g. buttonOne.innerContent holds "text" and "linkUrl") that was wrong: the whole path got one guessed kind (or none), even though the sub-fields can differ (fullwidth-header's buttonOne/buttonTwo.innerContent -> "text", though it also carries a "linkUrl"; image.innerContent -> "image", though the value also carries alt/id/titleText the metadata never names on its own). AttrPathIndex::kinds() now resolves a composite path to a map subName => kind instead, preferring Divi's features.dynamicContent.type over the (often shared/generic) field component for a named sub-field – this is what tells a button's plain text sub-field apart from its linkUrl sub-field, both of which use the same generic "divi/text" component. A plain (non- composite) path is unaffected. Any subName the metadata never names (or that disagrees between candidates) is therefore simply absent from the map, and ModuleCatalog::pathKind() defaults it, like every unresolved path, to "unknown" - the strictest routing for 2b's sanitizer, never a guess. ModuleCatalog::pathKind() gains an optional $subName parameter to read a composite path's map. docs/guides/divi-attrs.md documents the new shape and that lit-divi-catalog without `module` lists all modules. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,19 @@ final class DiviCatalogAbilityTest extends TestCase
|
||||
self::assertArrayNotHasKey('button.innerContent', $button['path_kinds']);
|
||||
}
|
||||
|
||||
/** A composite path (several named sub-fields under one attribute path) surfaces as a
|
||||
* subName => kind map, not a single guessed string (spec §5.6, ruling F1 #1). */
|
||||
public function testDetailExposesCompositePathKindsAsASubNameMap(): void
|
||||
{
|
||||
$header = $this->ability()->execute(['module' => 'divi/fullwidth-header'])['module'];
|
||||
|
||||
self::assertSame(
|
||||
['text' => 'text', 'linkUrl' => 'url'],
|
||||
$header['path_kinds']['buttonOne.innerContent']
|
||||
);
|
||||
self::assertSame(['src' => 'image'], $header['path_kinds']['image.innerContent']);
|
||||
}
|
||||
|
||||
public function testUnknownModuleAndPermission(): void
|
||||
{
|
||||
self::assertSame('lit_not_found', $this->ability()->execute(['module' => 'slider-pro'])->get_error_code());
|
||||
|
||||
@@ -157,14 +157,62 @@ final class ModuleCatalogTest extends TestCase
|
||||
$catalog = self::sampleCatalog();
|
||||
|
||||
// button.innerContent's only attrName-annotated node is the "link" sub-group itself
|
||||
// (component.type "group": link text + linkUrl + linkTarget) – not a single scalar kind.
|
||||
// (component.type "group": link text + linkUrl + linkTarget), and its "text" sub-field
|
||||
// carries no attrName of its own – Divi's metadata gives no reliable subName => kind
|
||||
// map here at all, so the whole path stays unknown (never guessed).
|
||||
self::assertSame('unknown', $catalog->pathKind('divi/button', 'button.innerContent'));
|
||||
self::assertArrayNotHasKey('button.innerContent', $catalog->kinds('divi/button'));
|
||||
}
|
||||
|
||||
// imageIcon.innerContent carries three "field" components under the same attrName with
|
||||
// different kinds (useIcon: onoff, icon: icon, src: image) – a genuine conflict.
|
||||
/**
|
||||
* Composite paths (Divi groups several named sub-fields, "subName", under one attribute
|
||||
* path) resolve to a subName => kind map wherever the metadata reliably names each
|
||||
* sub-field's own component (spec §5.6, ruling F1 #1) – asking without a subName, or for
|
||||
* a subName the metadata never names (e.g. an image's auto-synced "alt"), stays unknown.
|
||||
*/
|
||||
public function testDerivesPerSubNameKindsForCompositePaths(): void
|
||||
{
|
||||
$catalog = self::sampleCatalog();
|
||||
|
||||
// imageIcon.innerContent (divi/blurb): useIcon (toggle), icon (icon-picker), src
|
||||
// (upload, image) each carry attrName + subName + their own field component.
|
||||
self::assertSame(
|
||||
['useIcon' => 'onoff', 'icon' => 'icon', 'src' => 'image'],
|
||||
$catalog->kinds('divi/blurb')['imageIcon.innerContent']
|
||||
);
|
||||
self::assertSame('onoff', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'useIcon'));
|
||||
self::assertSame('icon', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'icon'));
|
||||
self::assertSame('image', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'src'));
|
||||
// No subName given, or one Divi's metadata never names: unknown, never guessed.
|
||||
self::assertSame('unknown', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent'));
|
||||
self::assertSame('unknown', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'noSuchField'));
|
||||
|
||||
// fullwidth-header's scrollDown.decoration.icon is the same pattern (onoff/icon/color/number).
|
||||
self::assertSame('unknown', $catalog->pathKind('divi/fullwidth-header', 'scrollDown.decoration.icon'));
|
||||
// fullwidth-header's scrollDown.decoration.icon: show (toggle), color (color-picker),
|
||||
// size (range) – same composite pattern one level deeper.
|
||||
self::assertSame(
|
||||
['show' => 'onoff', 'color' => 'color', 'size' => 'number'],
|
||||
$catalog->kinds('divi/fullwidth-header')['scrollDown.decoration.icon']
|
||||
);
|
||||
|
||||
// buttonOne/buttonTwo.innerContent: both sub-fields use the same generic "divi/text"
|
||||
// field component, but "linkUrl" is a URL, not plain text – only Divi's own
|
||||
// features.dynamicContent.type tells them apart.
|
||||
foreach (['buttonOne', 'buttonTwo'] as $attrName) {
|
||||
self::assertSame('text', $catalog->pathKind('divi/fullwidth-header', $attrName . '.innerContent', 'text'));
|
||||
self::assertSame('url', $catalog->pathKind('divi/fullwidth-header', $attrName . '.innerContent', 'linkUrl'));
|
||||
}
|
||||
|
||||
// image.innerContent only has an attrName+subName for "src" – the value actually
|
||||
// carries more keys at runtime (alt, id, titleText, synced by the upload component),
|
||||
// none of which Divi's metadata names on their own, so they stay unknown rather than
|
||||
// inheriting "image".
|
||||
self::assertSame(['src' => 'image'], $catalog->kinds('divi/fullwidth-header')['image.innerContent']);
|
||||
self::assertSame('image', $catalog->pathKind('divi/fullwidth-header', 'image.innerContent', 'src'));
|
||||
self::assertSame('unknown', $catalog->pathKind('divi/fullwidth-header', 'image.innerContent', 'alt'));
|
||||
self::assertSame('unknown', $catalog->pathKind('divi/fullwidth-header', 'image.innerContent'));
|
||||
|
||||
// A plain (non-composite) path is unaffected: still a flat string, not a map.
|
||||
self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent'));
|
||||
self::assertIsString($catalog->kinds('divi/text')['content.innerContent']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user