From 2f4dcf9ce4bace15c830b977decbdac4ac1cdd23 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sat, 26 Sep 2026 11:47:43 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- docs/guides/overview.md | 2 +- src/Integrations/Divi/DiviIntegration.php | 41 + .../Divi/Schema/AttrPathIndex.php | 215 + .../Divi/Schema/DiviCatalogAbility.php | 153 + src/Integrations/Divi/Schema/DiviModules.php | 44 + .../Divi/Schema/ModuleCatalog.php | 150 + src/Integrations/Divi/Schema/ShortformMap.php | 270 ++ src/Plugin.php | 4 +- tests/Fixtures/divi/metadata-sample.json | 3977 +++++++++++++++++ tests/Integration/divi-metadata-fixture.php | 56 + tests/Integration/divi-shortform-check.php | 57 + .../Integrations/Divi/DiviIntegrationTest.php | 37 + .../Divi/Schema/DiviCatalogAbilityTest.php | 70 + .../Divi/Schema/ModuleCatalogTest.php | 153 + 14 files changed, 5227 insertions(+), 2 deletions(-) create mode 100644 src/Integrations/Divi/DiviIntegration.php create mode 100644 src/Integrations/Divi/Schema/AttrPathIndex.php create mode 100644 src/Integrations/Divi/Schema/DiviCatalogAbility.php create mode 100644 src/Integrations/Divi/Schema/DiviModules.php create mode 100644 src/Integrations/Divi/Schema/ModuleCatalog.php create mode 100644 src/Integrations/Divi/Schema/ShortformMap.php create mode 100644 tests/Fixtures/divi/metadata-sample.json create mode 100644 tests/Integration/divi-metadata-fixture.php create mode 100644 tests/Integration/divi-shortform-check.php create mode 100644 tests/Unit/Integrations/Divi/DiviIntegrationTest.php create mode 100644 tests/Unit/Integrations/Divi/Schema/DiviCatalogAbilityTest.php create mode 100644 tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php diff --git a/docs/guides/overview.md b/docs/guides/overview.md index 15df9ad..ede5509 100644 --- a/docs/guides/overview.md +++ b/docs/guides/overview.md @@ -19,7 +19,7 @@ des verbundenen WordPress-Benutzers. Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`, `lit-content-get`, `lit-content-create`, `lit-content-update`, `lit-post-publish`, `lit-media-search`, -`lit-media-preview`, `lit-media-download`, `lit-media-upload-from-url`. +`lit-media-preview`, `lit-media-download`, `lit-media-upload-from-url`, `lit-divi-catalog`. Welche Tools direkt sichtbar sind, lässt sich unter Einstellungen → LIT MCP → Tools ändern (außer bei `lit-guide`, das immer direkt bleibt). diff --git a/src/Integrations/Divi/DiviIntegration.php b/src/Integrations/Divi/DiviIntegration.php new file mode 100644 index 0000000..30cde14 --- /dev/null +++ b/src/Integrations/Divi/DiviIntegration.php @@ -0,0 +1,41 @@ +modules = new DiviModules(); + } + + public function id(): string + { + return 'divi'; + } + + public function isAvailable(): bool + { + return DiviModules::isAvailable(); + } + + public function abilities(): array + { + return [ + new DiviCatalogAbility($this->modules), + ]; + } +} diff --git a/src/Integrations/Divi/Schema/AttrPathIndex.php b/src/Integrations/Divi/Schema/AttrPathIndex.php new file mode 100644 index 0000000..2a33866 --- /dev/null +++ b/src/Integrations/Divi/Schema/AttrPathIndex.php @@ -0,0 +1,215 @@ + sub paths holding the breakpoint maps. */ + public const NESTED = [ + 'decoration.font' => ['font', 'textShadow'], + 'decoration.bodyFont' => [ + 'body.font', 'body.textShadow', 'link.font', 'link.textShadow', 'ul.font', 'ul.textShadow', 'ul.list', + 'ol.font', 'ol.textShadow', 'ol.list', 'quote.font', 'quote.textShadow', 'quote.border', + ], + 'decoration.headingFont' => [ + 'h1.font', 'h1.textShadow', 'h2.font', 'h2.textShadow', 'h3.font', 'h3.textShadow', + 'h4.font', 'h4.textShadow', 'h5.font', 'h5.textShadow', 'h6.font', 'h6.textShadow', + ], + 'advanced.text' => ['text', 'textShadow'], + ]; + + /** Keys inside a settings category that describe the UI, not an attribute group. */ + private const UI_KEYS = ['groupType', 'item', 'items', 'component', 'groups', 'priority', 'render', 'panel']; + + /** + * Divi field component name => value kind (spec §5.6/ruling R3: 2b routes sanitization by + * this, falling back to key-name matching where a path has no unambiguous scalar kind). + * Structural/style groups (fonts, spacing, shadows, backgrounds, …) rarely name a single + * component for their whole group in the metadata, so they are simply left out here; see + * ModuleCatalog::pathKind() for the "unknown" default. + */ + private const COMPONENT_KINDS = [ + 'divi/text' => 'text', + 'divi/textarea' => 'text', + 'divi/timepicker' => 'text', + 'divi/google-api-key-field' => 'text', + 'divi/richtext' => 'richtext', + 'divi/code' => 'code', + 'divi/link' => 'url', + 'divi/button-link' => 'url', + 'divi/upload' => 'image', + 'divi/upload-gallery' => 'image', + 'divi/color-picker' => 'color', + 'divi/select' => 'enum', + 'divi/select-menu' => 'enum', + 'divi/select-column-class' => 'enum', + 'divi/select-widget-area' => 'enum', + 'divi/checkboxes' => 'list', + 'divi/sortable-list' => 'list', + 'divi/toggle' => 'onoff', + 'divi/range' => 'number', + 'divi/icon-picker' => 'icon', + ]; + + private const ATTR_NAME_PATTERN = '/^[A-Za-z0-9]+(\.[A-Za-z0-9]+)+$/'; + + /** + * @param array $attributes Metadata "attributes" of one module. + * @param array $defaults Its default attributes. + * @return list Sorted. + */ + public static function paths(array $attributes, array $defaults): array + { + $paths = ['module.meta.adminLabel' => true]; + foreach ($attributes as $attrName => $definition) { + $settings = is_array($definition['settings'] ?? null) ? $definition['settings'] : []; + foreach ($settings as $category => $groups) { + if ($category === 'innerContent') { + $paths[$attrName . '.innerContent'] = true; + continue; + } + + if ($category === 'meta' || ! is_array($groups)) { + continue; + } + + foreach (array_keys($groups) as $group) { + if (is_string($group) && ! in_array($group, self::UI_KEYS, true)) { + self::add($paths, $attrName . '.' . $category . '.' . $group); + } + } + } + + self::attrNames($settings, $paths); + } + + self::fromDefaults($defaults, '', $paths); + $list = array_map('strval', array_keys($paths)); + sort($list); + + return $list; + } + + /** + * 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". + * + * @param array $attributes Metadata "attributes" of one module. + * @return array + */ + public static function kinds(array $attributes): array + { + $kinds = ['module.meta.adminLabel' => 'text']; + foreach ($attributes as $definition) { + $settings = is_array($definition['settings'] ?? null) ? $definition['settings'] : []; + self::collectKinds($settings, $kinds); + } + + return $kinds; + } + + /** + * Up to $max known paths closest to $path (same attribute group first). + * + * @param list $known + * @return list + */ + public static function suggest(string $path, array $known, int $max = 5): array + { + $first = explode('.', $path)[0]; + $scored = []; + foreach ($known as $candidate) { + $penalty = explode('.', $candidate)[0] === $first ? 0 : 10; + $scored[$candidate] = levenshtein($path, $candidate) + $penalty; + } + asort($scored); + + return array_slice(array_map('strval', array_keys($scored)), 0, $max); + } + + /** @param array $paths */ + private static function add(array &$paths, string $path): void + { + $segments = explode('.', $path); + $nested = count($segments) === 3 ? (self::NESTED[$segments[1] . '.' . $segments[2]] ?? null) : null; + if ($nested === null) { + $paths[$path] = true; + + return; + } + + foreach ($nested as $sub) { + $paths[$path . '.' . $sub] = true; + } + } + + /** + * @param array $settings + * @param array $paths + */ + private static function attrNames(array $settings, array &$paths): void + { + foreach ($settings as $key => $value) { + if ($key === 'attrName' && is_string($value) && preg_match(self::ATTR_NAME_PATTERN, $value)) { + self::add($paths, $value); + } elseif (is_array($value)) { + self::attrNames($value, $paths); + } + } + } + + /** + * @param array $node + * @param array $kinds + */ + private static function collectKinds(array $node, array &$kinds): void + { + $attrName = $node['attrName'] ?? null; + $componentName = $node['component']['name'] ?? null; + if ( + is_string($attrName) + && preg_match(self::ATTR_NAME_PATTERN, $attrName) + && is_string($componentName) + && isset(self::COMPONENT_KINDS[$componentName]) + ) { + $kinds[$attrName] = self::COMPONENT_KINDS[$componentName]; + } + + foreach ($node as $value) { + if (is_array($value)) { + self::collectKinds($value, $kinds); + } + } + } + + /** + * @param array $attrs + * @param array $paths + */ + private static function fromDefaults(array $attrs, string $prefix, 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); + } + } + } +} diff --git a/src/Integrations/Divi/Schema/DiviCatalogAbility.php b/src/Integrations/Divi/Schema/DiviCatalogAbility.php new file mode 100644 index 0000000..2451e24 --- /dev/null +++ b/src/Integrations/Divi/Schema/DiviCatalogAbility.php @@ -0,0 +1,153 @@ + 'object', + 'properties' => [ + 'module' => ['type' => 'string', 'description' => 'Modul, z. B. "button" oder "divi/button".'], + ], + 'additionalProperties' => false, + ]; + } + + public function outputSchema(): ?array + { + return [ + 'type' => 'object', + 'properties' => [ + 'modules' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'title' => ['type' => 'string'], + 'category' => ['type' => 'string'], + 'children' => ['type' => 'array', 'items' => ['type' => 'string']], + 'shortform' => ['type' => 'boolean'], + ], + 'required' => ['name', 'title', 'category', 'children', 'shortform'], + ], + ], + 'module' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'title' => ['type' => 'string'], + 'category' => ['type' => 'string'], + 'children' => ['type' => 'array', 'items' => ['type' => 'string']], + 'item_type' => ['type' => ['string', 'null']], + 'shortform' => ['type' => 'object'], + 'paths' => ['type' => 'array', 'items' => ['type' => 'string']], + 'path_kinds' => [ + 'type' => 'object', + 'description' => 'Bekannter Attributpfad => Feldart (text, richtext, code, url, ' + . 'image, color, enum, onoff, number, list, icon), soweit aus den Divi-Metadaten ' + . 'ableitbar; fehlende Pfade gelten als unbekannt.', + ], + ], + 'required' => ['name', 'title', 'category', 'children', 'shortform', 'paths', 'path_kinds'], + ], + 'notes' => ['type' => 'array', 'items' => ['type' => 'string']], + ], + 'required' => ['notes'], + ]; + } + + public function annotations(): array + { + return ['readonly' => true, 'destructive' => false, 'idempotent' => true]; + } + + public function isDirect(): bool + { + return true; + } + + public function permission(array $input): bool|WP_Error + { + return current_user_can('edit_posts') + ? true + : Errors::forbidden('Keine Berechtigung, Inhalte zu bearbeiten.'); + } + + public function execute(array $input): mixed + { + $catalog = $this->modules->catalog(); + $notes = [self::CODE_NOTE, 'Details zur Vollform: lit-guide mit topic "divi-attrs".']; + + if (! is_string($input['module'] ?? null) || $input['module'] === '') { + $modules = []; + foreach ($catalog->summary() as $module) { + $modules[] = $module + ['shortform' => ShortformMap::has(substr($module['name'], strlen('divi/')))]; + } + + return ['modules' => $modules, 'notes' => $notes]; + } + + $name = ModuleCatalog::normalize($input['module']); + if (! $catalog->has($name)) { + return Errors::notFound( + sprintf('Modul "%s" gibt es auf dieser Website nicht.', $input['module']), + 'Verfügbare Module mit lit-divi-catalog ohne module abrufen.' + ); + } + + $short = substr($name, strlen('divi/')); + $item = ShortformMap::childType($short); + + return [ + 'module' => [ + 'name' => $name, + 'title' => $catalog->title($name), + 'category' => $catalog->category($name), + 'children' => $catalog->childrenOf($name), + 'item_type' => $item, + 'shortform' => JsonMap::of(ShortformMap::fields($short)), + 'paths' => $catalog->paths($name), + 'path_kinds' => JsonMap::of($catalog->kinds($name)), + ], + 'notes' => $notes, + ]; + } +} diff --git a/src/Integrations/Divi/Schema/DiviModules.php b/src/Integrations/Divi/Schema/DiviModules.php new file mode 100644 index 0000000..2386df2 --- /dev/null +++ b/src/Integrations/Divi/Schema/DiviModules.php @@ -0,0 +1,44 @@ +catalog === null) { + $metadata = ModuleRegistration::get_all_core_modules_metadata(); + $this->catalog = new ModuleCatalog( + is_array($metadata) ? $metadata : [], + static function (string $name): array { + $defaults = ModuleRegistration::get_default_attrs($name, 'default'); + + return is_array($defaults) ? $defaults : []; + } + ); + } + + return $this->catalog; + } + + /** Divi version written into builderVersion of new blocks. */ + public function builderVersion(): string + { + return defined('ET_BUILDER_PRODUCT_VERSION') ? (string) ET_BUILDER_PRODUCT_VERSION : '5.2.1'; + } +} diff --git a/src/Integrations/Divi/Schema/ModuleCatalog.php b/src/Integrations/Divi/Schema/ModuleCatalog.php new file mode 100644 index 0000000..47b7083 --- /dev/null +++ b/src/Integrations/Divi/Schema/ModuleCatalog.php @@ -0,0 +1,150 @@ +> 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; + } + + 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'))); + } + + /** @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; not every known path 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, "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}> */ + 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; + } +} diff --git a/src/Integrations/Divi/Schema/ShortformMap.php b/src/Integrations/Divi/Schema/ShortformMap.php new file mode 100644 index 0000000..8c4ba1b --- /dev/null +++ b/src/Integrations/Divi/Schema/ShortformMap.php @@ -0,0 +1,270 @@ + where Divi 5 keeps + * the value. Paths come from Divi's conversion outlines (…/module-library/src/components/ + * /conversion-outline.json, "*" = breakpoint/state) and are checked against the live + * catalog by tests/Integration/divi-shortform-check.php. + * + * A field is ['path' => attribute path, 'sub' => key inside the desktop value or null, + * 'kind' => value kind, 'description' => German help text], plus 'values' for enums, + * 'keys' ([url, id, alt]) for media and 'with' (fixed extra sub values) where needed. + * Kinds: text, html, code, url, media, enum, onoff, level, number, ids, color. + */ +final class ShortformMap +{ + /** @var array>}>|null */ + private static ?array $modules = null; + + /** @return list Short module names with a short form. */ + public static function types(): array + { + return array_keys(self::modules()); + } + + /** $type is the short name ("button"). */ + public static function has(string $type): bool + { + return isset(self::modules()[$type]); + } + + /** @return array> */ + public static function fields(string $type): array + { + return self::modules()[$type]['fields'] ?? []; + } + + /** Short name of the item type a parent holds in "items" (accordion → accordion-item). */ + public static function childType(string $type): ?string + { + return self::modules()[$type]['child'] ?? null; + } + + /** Field whose emptiness makes the module "empty" (spec §5.5), null if none. */ + public static function primary(string $type): ?string + { + return self::modules()[$type]['primary'] ?? null; + } + + /** + * Current desktop values of the short-form fields (media fields are write-only and left out). + * + * @param array $attrs + * @return array + */ + public static function values(string $type, array $attrs): array + { + $values = []; + foreach (self::fields($type) as $field => $spec) { + if ($spec['kind'] === 'media') { + continue; + } + + $value = AttrMap::desktop($attrs, (string) $spec['path']); + if ($spec['sub'] !== null) { + $value = is_array($value) ? ($value[$spec['sub']] ?? null) : null; + } + + if ($value !== null) { + $values[$field] = $value; + } + } + + return $values; + } + + /** + * @param array $extra keys, values or with. + * @return array + */ + private static function field( + string $path, + ?string $sub, + string $kind, + string $description, + array $extra = [] + ): array { + return array_merge(['path' => $path, 'sub' => $sub, 'kind' => $kind, 'description' => $description], $extra); + } + + /** + * @param array> $fields + * @return array{primary: ?string, child: ?string, fields: array>} + */ + private static function module(?string $primary, array $fields, ?string $child = null): array + { + return ['primary' => $primary, 'child' => $child, 'fields' => $fields]; + } + + /** @return array>}> */ + private static function modules(): array + { + if (self::$modules !== null) { + return self::$modules; + } + + $title = self::field('title.innerContent', null, 'text', 'Titel'); + $content = self::field('content.innerContent', null, 'html', 'Inhalt (HTML erlaubt)'); + $open = self::field('module.advanced.open', null, 'onoff', 'geöffnet (true/false)'); + $level = self::field('title.decoration.font.font', 'headingLevel', 'level', 'Überschriftenebene h1–h6'); + $buttonText = self::field('button.innerContent', 'text', 'text', 'Button-Beschriftung'); + $buttonUrl = self::field('button.innerContent', 'linkUrl', 'url', 'Button-Linkziel'); + $media = static fn (string $path, array $keys, string $description, array $with = []): array => self::field( + $path, + null, + 'media', + $description, + $with === [] ? ['keys' => $keys] : ['keys' => $keys, 'with' => $with] + ); + + self::$modules = [ + 'text' => self::module('content', ['content' => $content]), + 'heading' => self::module('text', [ + 'text' => self::field('title.innerContent', null, 'text', 'Überschrift'), + 'level' => $level, + ]), + 'button' => self::module('text', [ + 'text' => self::field('button.innerContent', 'text', 'text', 'Beschriftung'), + 'url' => self::field('button.innerContent', 'linkUrl', 'url', 'Linkziel'), + 'new_window' => self::field('button.innerContent', 'linkTarget', 'onoff', 'in neuem Fenster öffnen'), + ]), + 'image' => self::module('src', [ + 'image_id' => $media( + 'image.innerContent', + ['src', 'id', 'alt'], + 'Bild aus der Mediathek (Anhang-ID); setzt URL, ID und Alt-Text' + ), + 'src' => self::field('image.innerContent', 'src', 'url', 'Bild-URL'), + 'alt' => self::field('image.innerContent', 'alt', 'text', 'Alt-Text'), + 'title' => self::field('image.innerContent', 'titleText', 'text', 'Titel'), + 'url' => self::field('image.innerContent', 'linkUrl', 'url', 'Linkziel'), + ]), + 'blurb' => self::module('title', [ + 'title' => self::field('title.innerContent', 'text', 'text', 'Titel'), + 'url' => self::field('title.innerContent', 'url', 'url', 'Link des Titels'), + 'content' => $content, + 'image_id' => $media( + 'imageIcon.innerContent', + ['src', null, 'alt'], + 'Bild aus der Mediathek (Anhang-ID)', + ['useIcon' => 'off'] + ), + 'src' => self::field('imageIcon.innerContent', 'src', 'url', 'Bild-URL', [ + 'with' => ['useIcon' => 'off'], + ]), + 'alt' => self::field('imageIcon.innerContent', 'alt', 'text', 'Alt-Text'), + 'level' => $level, + ]), + 'cta' => self::module('title', [ + 'title' => $title, + 'content' => $content, + 'button_text' => $buttonText, + 'button_url' => $buttonUrl, + ]), + 'icon' => self::module(null, [ + 'url' => self::field('icon.innerContent', 'url', 'url', 'Linkziel'), + 'title' => self::field('icon.innerContent', 'title', 'text', 'Titel'), + ]), + 'divider' => self::module(null, [ + 'show' => self::field('divider.advanced.line', 'show', 'onoff', 'Linie anzeigen'), + 'color' => self::field('divider.advanced.line', 'color', 'color', 'Farbe, z. B. {{color:primary}}'), + ]), + 'testimonial' => self::module('content', [ + 'author' => self::field('author.innerContent', null, 'text', 'Name'), + 'job_title' => self::field('jobTitle.innerContent', null, 'text', 'Funktion'), + 'company' => self::field('company.innerContent', 'text', 'text', 'Firma'), + 'company_url' => self::field('company.innerContent', 'linkUrl', 'url', 'Link der Firma'), + 'content' => $content, + 'portrait_id' => $media('portrait.innerContent', ['url', null, null], 'Porträt aus der Mediathek'), + 'portrait_url' => self::field('portrait.innerContent', 'url', 'url', 'Porträt-URL'), + ]), + 'team-member' => self::module('name', [ + 'name' => self::field('name.innerContent', null, 'text', 'Name'), + 'position' => self::field('position.innerContent', null, 'text', 'Position'), + 'content' => $content, + 'image_id' => $media('image.innerContent', ['url', null, null], 'Foto aus der Mediathek'), + 'image_url' => self::field('image.innerContent', 'url', 'url', 'Foto-URL'), + ]), + 'accordion' => self::module(null, [], 'accordion-item'), + 'accordion-item' => self::module('title', ['title' => $title, 'content' => $content, 'open' => $open]), + 'tabs' => self::module(null, [], 'tab'), + 'tab' => self::module('title', ['title' => $title, 'content' => $content]), + 'toggle' => self::module('title', ['title' => $title, 'content' => $content, 'open' => $open]), + 'gallery' => self::module('image_ids', [ + 'image_ids' => self::field('image.advanced.galleryIds', null, 'ids', 'Anhang-IDs, z. B. [12, 33]'), + ]), + 'slider' => self::module(null, [], 'slide'), + 'slide' => self::module('title', [ + 'title' => $title, + 'content' => $content, + 'button_text' => $buttonText, + 'button_url' => $buttonUrl, + 'image_id' => $media('image.innerContent', ['src', null, 'alt'], 'Bild aus der Mediathek'), + 'src' => self::field('image.innerContent', 'src', 'url', 'Bild-URL'), + 'alt' => self::field('image.innerContent', 'alt', 'text', 'Alt-Text'), + ]), + 'contact-form' => self::module(null, [ + 'title' => $title, + 'email' => self::field('email.advanced.receiver', null, 'text', 'Empfänger (leer = Admin-E-Mail)'), + 'success_message' => self::field('module.advanced.successMessage', null, 'text', 'Text nach Absenden'), + 'submit_text' => self::field('button.innerContent', 'text', 'text', 'Beschriftung Absenden'), + ], 'contact-field'), + 'contact-field' => self::module('title', [ + 'title' => self::field('fieldItem.innerContent', null, 'text', 'Feldname'), + 'id' => self::field('fieldItem.advanced.id', null, 'text', 'Feld-ID'), + 'type' => self::field('fieldItem.advanced.type', null, 'enum', 'Feldart (text = mehrzeilig)', [ + 'values' => ['input', 'email', 'text', 'checkbox', 'radio', 'select'], + ]), + 'required' => self::field('fieldItem.advanced.required', null, 'onoff', 'Pflichtfeld'), + ]), + 'map' => self::module('address', [ + 'address' => self::field('map.innerContent', 'address', 'text', 'Adresse'), + 'lat' => self::field('map.innerContent', 'lat', 'number', 'Breitengrad'), + 'lng' => self::field('map.innerContent', 'lng', 'number', 'Längengrad'), + 'zoom' => self::field('map.innerContent', 'zoom', 'number', 'Zoomstufe'), + ], 'map-pin'), + 'map-pin' => self::module('title', [ + 'title' => $title, + 'content' => $content, + 'address' => self::field('pin.innerContent', 'address', 'text', 'Adresse'), + 'lat' => self::field('pin.innerContent', 'lat', 'number', 'Breitengrad'), + 'lng' => self::field('pin.innerContent', 'lng', 'number', 'Längengrad'), + ]), + 'video' => self::module('src', [ + 'src' => self::field('video.innerContent', 'src', 'url', 'Video-URL (YouTube, MP4)'), + 'thumbnail' => self::field('thumbnail.innerContent', 'src', 'url', 'Vorschaubild-URL'), + ]), + 'number-counter' => self::module('number', [ + 'number' => self::field('number.innerContent', null, 'text', 'Zahl'), + 'title' => $title, + 'percent' => self::field('number.advanced.enablePercentSign', null, 'onoff', 'Prozentzeichen anzeigen'), + ]), + 'social-media-follow' => self::module(null, [], 'social-media-follow-network'), + 'social-media-follow-network' => self::module('url', [ + 'network' => self::field( + 'socialNetwork.innerContent', + 'title', + 'text', + 'Netzwerk, z. B. facebook, instagram, linkedin, youtube' + ), + 'url' => self::field('socialNetwork.innerContent', 'link', 'url', 'Profil-URL'), + 'label' => self::field('socialNetwork.innerContent', 'label', 'text', 'Beschriftung'), + ]), + 'icon-list' => self::module(null, [], 'icon-list-item'), + 'icon-list-item' => self::module('text', [ + 'text' => self::field('content.innerContent', null, 'text', 'Text'), + ]), + 'code' => self::module('content', [ + 'content' => self::field('content.innerContent', null, 'code', 'HTML/Code – nur ohne passendes Modul'), + ]), + ]; + + return self::$modules; + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 99127ff..9ed09db 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -21,6 +21,7 @@ use LIT\MCP\ChangeLog\ChangeLogCleanup; use LIT\MCP\ChangeLog\ChangeLogInstaller; use LIT\MCP\Guides\InstructionsBuilder; use LIT\MCP\Integrations\Core\CoreIntegration; +use LIT\MCP\Integrations\Divi\DiviIntegration; use LIT\MCP\Integrations\IntegrationRegistry; use LIT\MCP\Integrations\Media\MediaIntegration; use LIT\MCP\Integrations\Pods\PodsFieldSource; @@ -66,7 +67,8 @@ final class Plugin $registry = new IntegrationRegistry( new CoreIntegration(self::pluginDir() . '/docs/guides'), new WordPressIntegration($schema, $changeLog), - new MediaIntegration($schema, $changeLog, $limits) + new MediaIntegration($schema, $changeLog, $limits), + new DiviIntegration($schema) ); $policy = ExposurePolicy::fromOption(); diff --git a/tests/Fixtures/divi/metadata-sample.json b/tests/Fixtures/divi/metadata-sample.json new file mode 100644 index 0000000..0133d13 --- /dev/null +++ b/tests/Fixtures/divi/metadata-sample.json @@ -0,0 +1,3977 @@ +{ + "divi": "5.2.1", + "metadata": [ + { + "name": "divi/section", + "title": "Section", + "category": "structure", + "childrenName": [ + "divi/row" + ], + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": { + "groupType": "group-item", + "item": { + "groupSlug": "contentElements", + "priority": 5, + "render": true, + "component": { + "type": "group", + "name": "divi/elements", + "props": { + "grouped": false + } + } + } + }, + "html": [], + "loop": [], + "link": { + "item": { + "groupSlug": "contentLink", + "component": { + "props": { + "fieldLabel": "Section" + } + } + } + }, + "innerShadow": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "attrName": "module.advanced.innerShadow", + "label": "Show Inner Shadow", + "description": "Here you can select whether or not your section has an inner shadow. This can look great when you have colored backgrounds or background images.", + "category": "configuration", + "render": true, + "priority": 30, + "features": { + "responsive": false, + "hover": false, + "sticky": false + }, + "component": { + "type": "field", + "name": "divi/toggle" + } + } + }, + "dividers": { + "panel": "design", + "priority": 20, + "groupName": "dividers", + "groupType": "group", + "component": { + "name": "divi/dividers" + } + }, + "gutter": { + "groupType": "group-item", + "item": { + "groupSlug": "designSizing", + "priority": 10, + "render": true, + "component": { + "name": "divi/gutter", + "type": "group", + "props": { + "grouped": false, + "visible": false + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "contentBackground", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/background", + "props": { + "grouped": false + } + } + } + }, + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "layout": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "attrName": "module.decoration.layout", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/layout", + "props": { + "grouped": false + } + } + } + }, + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sizing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSizing", + "priority": 20, + "render": true, + "component": { + "name": "divi/sizing", + "type": "group", + "props": { + "grouped": false + } + } + } + }, + "spacing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSpacing", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/spacing", + "props": { + "grouped": false + } + } + } + }, + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "column1": { + "settings": { + "advanced": [], + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "contentBackground", + "priority": 60, + "render": true, + "attrName": "column1.decoration.background", + "component": { + "type": "group", + "name": "divi/background", + "props": { + "grouped": false, + "fieldLabel": "Column 1" + } + } + } + }, + "spacing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSpacing", + "priority": 60, + "render": true, + "attrName": "column1.decoration.spacing", + "component": { + "type": "group", + "name": "divi/spacing", + "props": { + "grouped": false, + "fieldLabel": "Column 1", + "fields": { + "margin": { + "render": false + } + } + } + } + } + } + } + } + }, + "column2": { + "settings": { + "advanced": [], + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "contentBackground", + "priority": 60, + "render": true, + "attrName": "column2.decoration.background", + "component": { + "type": "group", + "name": "divi/background", + "props": { + "grouped": false, + "fieldLabel": "Column 2" + } + } + } + }, + "spacing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSpacing", + "priority": 60, + "render": true, + "attrName": "column2.decoration.spacing", + "component": { + "type": "group", + "name": "divi/spacing", + "props": { + "grouped": false, + "fieldLabel": "Column 2", + "fields": { + "margin": { + "render": false + } + } + } + } + } + } + } + } + }, + "column3": { + "settings": { + "advanced": [], + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "contentBackground", + "priority": 60, + "render": true, + "attrName": "column3.decoration.background", + "component": { + "type": "group", + "name": "divi/background", + "props": { + "grouped": false, + "fieldLabel": "Column 3" + } + } + } + }, + "spacing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSpacing", + "priority": 60, + "render": true, + "attrName": "column3.decoration.spacing", + "component": { + "type": "group", + "name": "divi/spacing", + "props": { + "grouped": false, + "fieldLabel": "Column 3", + "fields": { + "margin": { + "render": false + } + } + } + } + } + } + } + } + }, + "innerSizing": { + "settings": { + "decoration": { + "sizing": { + "groupType": "group-items", + "items": { + "width": { + "groupSlug": "designSizing", + "priority": 14, + "render": true, + "subName": "width", + "label": "Inner Width", + "description": "By default, elements will extend the full width of their parent element. If you would like to set a custom static width, you can do so using this option.", + "features": { + "sticky": false, + "dynamicContent": { + "type": "number" + } + }, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "width", + "defaultUnit": "%", + "max": 100 + } + } + }, + "maxWidth": { + "groupSlug": "designSizing", + "priority": 15, + "render": true, + "subName": "maxWidth", + "label": "Inner Max Width", + "description": "Setting a maximum width will prevent your element from ever surpassing the defined width value. Maximum width can be used in combination with the standard width setting. Maximum width supersedes the normal width value.", + "features": { + "sticky": false, + "dynamicContent": { + "type": "number" + } + }, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "max-width", + "defaultUnit": "px", + "max": 1080 + } + } + }, + "alignment": { + "groupSlug": "designSizing", + "priority": 16, + "render": true, + "subName": "alignment", + "label": "Row Alignment", + "description": "Align the module to the left, right or center.", + "features": { + "hover": false, + "sticky": false + }, + "multipleChoices": false, + "component": { + "name": "divi/button-options", + "type": "field", + "props": { + "allowClickToUnset": false, + "options": { + "left": { + "icon": "divi/align-left" + }, + "center": { + "icon": "divi/align-center" + }, + "right": { + "icon": "divi/align-right" + } + }, + "showLabel": false + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/row", + "title": "Row", + "category": "structure", + "childModuleName": "divi/column", + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": { + "groupType": "group-item", + "item": { + "groupSlug": "contentElements", + "priority": 5, + "render": true, + "component": { + "type": "group", + "name": "divi/elements", + "props": { + "grouped": false + } + } + } + }, + "html": [], + "loop": [], + "link": { + "item": { + "groupSlug": "contentLink", + "component": { + "props": { + "fieldLabel": "Row" + } + } + } + }, + "gutter": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "priority": 20, + "render": true, + "component": { + "name": "divi/gutter", + "type": "group", + "props": { + "grouped": false + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "order": [], + "filters": [], + "interactions": [], + "overflow": [], + "position": [], + "scroll": [], + "layout": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/layout", + "props": { + "grouped": false + } + } + } + }, + "sizing": { + "groupType": "group-items", + "items": { + "sizing": { + "groupSlug": "sizing", + "priority": 110, + "render": true, + "component": { + "type": "group", + "name": "divi/sizing", + "props": { + "grouped": false, + "fields": { + "maxWidth": { + "component": { + "props": { + "defaultUnit": "px", + "max": 2560 + } + } + } + } + } + } + } + } + }, + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + } + } + }, + { + "name": "divi/column", + "title": "Column", + "category": "structure", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "layout": [], + "overflow": [], + "order": [], + "transition": [], + "position": [], + "scroll": [], + "spacing": [], + "sizing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSizing", + "priority": 20, + "render": true, + "component": { + "type": "group", + "name": "divi/sizing", + "props": { + "grouped": false, + "fields": { + "alignment": { + "render": false + }, + "width": { + "defaultAttr": { + "desktop": { + "value": { + "width": null + } + } + }, + "component": { + "props": { + "defaultUnit": "" + } + } + } + } + } + } + } + }, + "sticky": [], + "transform": [], + "zIndex": [] + }, + "advanced": { + "elements": { + "groupType": "group-item", + "item": { + "groupSlug": "contentElements", + "priority": 5, + "render": true, + "component": { + "type": "group", + "name": "divi/elements", + "props": { + "grouped": false + } + } + } + }, + "html": [], + "loop": [], + "link": { + "item": { + "groupSlug": "contentLink", + "component": { + "props": { + "fieldLabel": "Column" + } + } + } + } + }, + "meta": { + "meta": [] + } + } + } + } + }, + { + "name": "divi/text", + "title": "Text", + "category": "module", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": [], + "html": [], + "link": [], + "loop": [], + "text": { + "groupType": "group-item", + "item": { + "groupSlug": "designText", + "priority": 15, + "render": true, + "component": { + "type": "group", + "name": "divi/text", + "props": { + "grouped": false, + "groupTab": "body", + "groupTabLabel": "Body", + "icon": "divi/text-align-left", + "fieldLabel": "Text", + "fields": { + "textShadowGroup": { + "render": false + } + } + } + } + } + } + }, + "decoration": { + "layout": [], + "animation": [], + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sizing": [], + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "content": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "groupSlug": "contentText", + "priority": 5, + "render": true, + "attrName": "content.innerContent", + "label": "Body", + "description": "Here you can create the content that will be used within the module.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "text" + }, + "sticky": false, + "preset": "content" + }, + "component": { + "name": "divi/richtext", + "type": "field" + } + } + }, + "decoration": { + "headingFont": [], + "bodyFont": { + "groupType": "group-item", + "item": { + "groupSlug": "designText", + "priority": 5, + "render": true, + "component": { + "name": "divi/font-body", + "type": "group", + "props": { + "grouped": false, + "dynamicSubgroupHost": true, + "groups": { + "body": { + "groupTabLabel": "Body", + "icon": "divi/text-align-left", + "groupTab": "body", + "fieldLabel": "Text" + } + }, + "fields": { + "body": { + "textAlign": { + "render": false + } + } + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/heading", + "title": "Heading", + "category": "module", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": [], + "html": [], + "link": [], + "loop": [], + "text": { + "groupType": "group", + "groupName": "divi/text", + "panel": "design", + "priority": 10, + "component": { + "props": { + "fields": { + "orientation": { + "render": false + }, + "color": { + "render": false + } + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "layout": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sizing": [], + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "title": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "groupSlug": "contentText", + "priority": 5, + "render": true, + "attrName": "title.innerContent", + "label": "Heading", + "description": "Input your value to action title here.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "text" + }, + "responsive": true, + "sticky": false, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + } + }, + "decoration": { + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designTitleText", + "priority": 10, + "render": true, + "component": { + "name": "divi/font", + "type": "group", + "props": { + "grouped": false, + "fieldLabel": "Heading", + "dynamicSubgroupHost": true, + "fields": { + "headingLevel": { + "render": true + } + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/button", + "title": "Button", + "category": "module", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": [], + "html": [], + "loop": [], + "text": { + "component": { + "props": { + "fields": { + "orientation": { + "render": false + }, + "textShadowGroup": { + "render": false + } + } + } + } + }, + "alignment": { + "groupType": "group-item", + "item": { + "groupSlug": "designAlignment", + "priority": 5, + "render": true, + "attrName": "module.advanced.alignment", + "label": "Button Alignment", + "description": "Here you can define the alignment of Button.", + "category": "configuration", + "multipleChoices": false, + "features": { + "hover": false, + "sticky": false, + "preset": [ + "html" + ] + }, + "component": { + "type": "field", + "name": "divi/button-options", + "props": { + "options": { + "left": { + "icon": "divi/align-left" + }, + "center": { + "icon": "divi/align-center" + }, + "right": { + "icon": "divi/align-right" + } + }, + "showLabel": false + } + } + } + } + }, + "decoration": { + "layout": [], + "animation": [], + "attributes": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "button": { + "settings": { + "innerContent": { + "groupType": "into-multiple-groups", + "groups": { + "text": { + "groupType": "group-item", + "item": { + "description": "Input your desired button text.", + "groupSlug": "contentText", + "label": "Button Text", + "priority": 10, + "render": true, + "subName": "text", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "text" + }, + "sticky": false, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + } + }, + "link": { + "groupType": "group-item", + "item": { + "groupSlug": "contentLink", + "attrName": "button.innerContent", + "priority": 20, + "render": true, + "component": { + "type": "group", + "name": "divi/button-link", + "props": { + "grouped": false, + "attrName": "button.innerContent", + "fieldLabel": "Button" + } + } + } + } + } + }, + "decoration": { + "background": [], + "border": [], + "boxShadow": [], + "button": { + "component": { + "props": { + "dynamicSubgroupHost": true, + "dynamicSubgroupLockedSubgroupIds": [ + "button.decoration.transform", + "button.decoration.filters", + "button.decoration.animation" + ], + "fields": { + "alignment": { + "render": false + }, + "boxShadowGroup": { + "render": false + }, + "buttonIconGroup": { + "component": { + "props": { + "fields": { + "placement": { + "features": { + "hover": false + } + }, + "onHover": { + "features": { + "hover": false + } + } + } + } + } + }, + "fontGroup": { + "component": { + "props": { + "fields": { + "lineHeight": { + "render": true + }, + "textAlign": { + "render": false + } + } + } + } + }, + "spacingGroup": { + "render": false + } + } + } + } + }, + "font": [], + "sizing": [], + "spacing": { + "component": { + "props": { + "fields": { + "margin": { + "render": false + }, + "padding": { + "render": false + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/image", + "title": "Image", + "category": "module", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": [], + "html": [], + "loop": [], + "align": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageAlignment", + "label": "Image Alignment", + "description": "Here you can choose the image alignment.", + "category": "layout", + "multipleChoices": false, + "onChangeParams": [ + "multipleChoices" + ], + "render": true, + "priority": 50, + "features": { + "hover": false, + "sticky": false + }, + "component": { + "type": "field", + "name": "divi/button-options", + "props": { + "options": { + "left": { + "icon": "divi/align-left" + }, + "center": { + "icon": "divi/align-center" + }, + "right": { + "icon": "divi/align-right" + } + }, + "showLabel": false + } + } + } + }, + "sizing": { + "groupType": "group-items", + "items": { + "forceFullwidth": { + "groupSlug": "designSizing", + "attrName": "module.advanced.sizing", + "subName": "forceFullwidth", + "label": "Force Fullwidth", + "description": "When enabled, this will force your image to extend 100% of the width of the column it's in.", + "category": "layout", + "render": true, + "priority": 100, + "features": { + "hover": false, + "sticky": false + }, + "component": { + "type": "field", + "name": "divi/toggle" + } + }, + "sizing": { + "groupSlug": "designSizing", + "priority": 110, + "render": true, + "component": { + "type": "group", + "name": "divi/sizing", + "props": { + "grouped": false + } + } + } + } + }, + "spacing": { + "groupType": "group-items", + "items": { + "showBottomSpace": { + "groupSlug": "designSpacing", + "subName": "showBottomSpace", + "label": "Show Space Below The Image", + "description": "Here you can choose whether or not the image should have a space below it.", + "category": "layout", + "render": true, + "priority": 120, + "features": { + "preset": [ + "html" + ] + }, + "component": { + "type": "field", + "name": "divi/toggle" + } + }, + "spacing": { + "groupSlug": "designSpacing", + "priority": 130, + "render": true, + "component": { + "type": "group", + "name": "divi/spacing", + "props": { + "grouped": false + } + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "layout": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "image": { + "settings": { + "innerContent": { + "groupType": "group-items", + "items": { + "src": { + "groupSlug": "contentImage", + "subName": "src", + "label": "Image", + "description": "Upload an image.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "image" + }, + "sticky": false, + "responsive": true, + "hover": true, + "preset": "content" + }, + "render": true, + "priority": 10, + "component": { + "type": "field", + "name": "divi/upload", + "props": { + "syncImageData": { + "src": true, + "id": true, + "alt": true, + "titleText": true + } + } + } + }, + "linkUrl": { + "groupSlug": "contentImageLink", + "subName": "linkUrl", + "label": "Image Link URL", + "description": "If you would like your image to be a link, input your destination URL here. No link will be created if this field is left blank.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "url" + }, + "sticky": false, + "responsive": false, + "hover": false, + "preset": "content" + }, + "render": true, + "priority": 20, + "component": { + "type": "field", + "name": "divi/text" + } + }, + "linkTarget": { + "groupSlug": "contentImageLink", + "subName": "linkTarget", + "label": "Image Link Target", + "description": "Here you can choose whether or not your link opens in a new window.", + "category": "configuration", + "features": { + "responsive": false, + "hover": false, + "sticky": false, + "preset": "content" + }, + "render": true, + "priority": 30, + "component": { + "type": "field", + "name": "divi/select", + "props": { + "options": { + "off": { + "label": "In The Current Tab" + }, + "on": { + "label": "In A New Tab" + } + } + } + } + } + } + }, + "advanced": { + "lightbox": { + "groupType": "group-item", + "item": { + "groupSlug": "contentImageLink", + "label": "Open in Lightbox", + "description": "Here you can choose whether or not the image should open in Lightbox. Note: if you select to open the image in Lightbox, url options below will be ignored.", + "category": "configuration", + "render": true, + "priority": 10, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": "content" + }, + "component": { + "type": "field", + "name": "divi/toggle" + } + } + }, + "overlay": { + "groupType": "group-items", + "items": { + "use": { + "groupSlug": "designImageOverlay", + "subName": "use", + "label": "Image Overlay", + "description": "If enabled, an overlay color and icon will be displayed when a visitors hovers over the image", + "category": "layout", + "render": true, + "priority": 40, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": [ + "style" + ] + }, + "component": { + "type": "field", + "name": "divi/toggle" + } + }, + "backgroundColor": { + "groupSlug": "designImageOverlay", + "subName": "backgroundColor", + "label": "Hover Overlay Color", + "description": "Here you can define a custom color for the overlay", + "features": { + "hover": false, + "preset": [ + "style" + ], + "dynamicContent": { + "type": "color" + } + }, + "render": true, + "priority": 60, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + } + }, + "overlayIcon": { + "groupType": "group-items", + "items": { + "hoverIcon": { + "groupSlug": "designImageOverlay", + "subName": "hoverIcon", + "label": "Hover Icon Picker", + "description": "Here you can define a custom icon for the overlay.", + "category": "configuration", + "features": { + "sticky": false, + "hover": false, + "preset": [ + "style" + ] + }, + "render": true, + "priority": 70, + "component": { + "type": "field", + "name": "divi/icon-picker" + } + }, + "iconColor": { + "groupSlug": "designImageOverlay", + "subName": "iconColor", + "label": "Overlay Icon Color", + "description": "Here you can define a custom color for the overlay icon", + "render": true, + "priority": 50, + "features": { + "hover": false, + "preset": [ + "style" + ], + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + } + } + }, + "decoration": { + "border": [], + "boxShadow": [] + } + } + } + } + }, + { + "name": "divi/blurb", + "title": "Blurb", + "category": "module", + "childrenName": [], + "attributes": { + "imageIcon": { + "settings": { + "innerContent": { + "groupType": "group-items", + "items": { + "useIcon": { + "groupSlug": "contentImageIcon", + "attrName": "imageIcon.innerContent", + "subName": "useIcon", + "label": "Use Icon", + "description": "Here you can choose whether icon set below should be used.", + "category": "basic_option", + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": "content" + }, + "render": true, + "priority": 10, + "component": { + "type": "field", + "name": "divi/toggle" + } + }, + "icon": { + "groupSlug": "contentImageIcon", + "attrName": "imageIcon.innerContent", + "subName": "icon", + "label": "Icon", + "description": "Choose an icon to display with your blurb.", + "category": "basic_option", + "features": { + "sticky": false, + "preset": "content" + }, + "render": true, + "priority": 10, + "component": { + "type": "field", + "name": "divi/icon-picker" + } + }, + "src": { + "groupSlug": "contentImageIcon", + "attrName": "imageIcon.innerContent", + "subName": "src", + "label": "Image", + "description": "Upload an image to display at the top of your blurb.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "image" + }, + "sticky": false, + "responsive": true, + "hover": true, + "preset": "content" + }, + "render": true, + "priority": 10, + "component": { + "type": "field", + "name": "divi/upload", + "props": { + "syncImageData": { + "src": true, + "id": true, + "alt": true, + "titleText": true + } + } + } + } + } + }, + "advanced": { + "color": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "attrName": "imageIcon.advanced.color", + "label": "Icon Color", + "description": "Here you can define a custom color for your icon.", + "render": true, + "priority": 10, + "features": { + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + }, + "placement": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "attrName": "imageIcon.advanced.placement", + "label": "Image/Icon Placement", + "description": "Here you can choose where to place the icon.", + "category": "layout", + "render": true, + "priority": 30, + "features": { + "hover": false, + "sticky": false + }, + "component": { + "type": "field", + "name": "divi/select", + "props": { + "options": { + "top": { + "label": "Top" + }, + "left": { + "label": "Left" + } + } + } + } + } + } + }, + "decoration": { + "sizing": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 52, + "render": true, + "component": { + "type": "group", + "name": "divi/sizing", + "props": { + "attrName": "imageIcon.decoration.sizing", + "grouped": true, + "fieldLabel": "Image/Icon", + "dynamicSubgroupHostLayoutStyle": "block", + "fields": { + "iconFontSize": { + "attrName": "imageIcon.decoration.sizing", + "subName": "iconFontSize", + "defaultAttr": { + "desktop": { + "value": { + "iconFontSize": "96px" + } + } + }, + "label": "Icon Font Size", + "description": "Control the size of the icon by increasing or decreasing the font size.", + "priority": 1, + "category": "font_option", + "render": true, + "features": { + "dynamicContent": { + "type": "number" + } + }, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "font-size", + "defaultUnit": "px", + "min": 1, + "minLimit": 1, + "max": 120 + } + } + } + } + } + } + } + }, + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 55, + "render": true, + "component": { + "type": "group", + "name": "divi/background", + "props": { + "attrName": "imageIcon.decoration.background", + "grouped": true, + "fieldLabel": "Image/Icon" + } + } + } + }, + "border": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 60, + "render": true, + "component": { + "type": "group", + "name": "divi/border", + "props": { + "grouped": true, + "fieldLabel": "Image/Icon" + } + } + } + }, + "spacing": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 60, + "render": true, + "component": { + "type": "group", + "name": "divi/spacing", + "props": { + "grouped": true, + "fieldLabel": "Image/Icon" + } + } + } + }, + "boxShadow": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 60, + "render": true, + "component": { + "type": "group", + "name": "divi/box-shadow", + "props": { + "grouped": true, + "fieldLabel": "Image" + } + } + } + }, + "filters": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 60, + "render": true, + "component": { + "type": "group", + "name": "divi/filters", + "props": { + "grouped": true, + "fieldLabel": "Image/Icon" + } + } + } + }, + "animation": { + "groupType": "group-item", + "item": { + "groupSlug": "designImageIcon", + "priority": 70, + "render": true, + "component": { + "type": "group", + "name": "divi/animation", + "props": { + "attrName": "imageIcon.decoration.animation", + "grouped": true, + "fieldLabel": "Image/Icon" + } + } + } + } + } + } + }, + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": [], + "html": [], + "link": [], + "loop": [], + "text": [] + }, + "decoration": { + "animation": { + "groupType": "group-item", + "item": { + "groupSlug": "designAnimation", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/animation", + "props": { + "grouped": false + } + } + } + }, + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "layout": [], + "order": [], + "overflow": [], + "position": [], + "scroll": [], + "sizing": { + "groupType": "group-item", + "item": { + "groupSlug": "designSizing", + "priority": 20, + "render": true, + "component": { + "type": "group", + "name": "divi/sizing", + "props": { + "grouped": false + } + } + } + }, + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "title": { + "settings": { + "innerContent": [], + "decoration": { + "font": { + "component": { + "props": { + "dynamicSubgroupHost": true, + "dynamicSubgroupHostLayoutStyle": "flex" + } + } + } + } + } + }, + "content": { + "settings": { + "innerContent": { + "item": { + "groupSlug": "contentText", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "text" + } + } + } + }, + "decoration": { + "bodyFont": { + "component": { + "props": { + "dynamicSubgroupHost": true, + "dynamicSubgroupHostLayoutStyle": "flex" + } + } + } + } + } + }, + "contentContainer": { + "settings": { + "decoration": { + "sizing": { + "groupType": "group-items", + "items": { + "maxWidth": { + "groupSlug": "designSizing", + "attrName": "contentContainer.decoration.sizing", + "subName": "maxWidth", + "label": "Content Width", + "description": "Adjust the width of the content within the blurb.", + "category": "layout", + "priority": 10, + "render": true, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "width", + "max": 1100 + } + }, + "features": { + "dynamicContent": { + "type": "number" + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/code", + "title": "Code", + "category": "module", + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "html": [], + "link": [], + "loop": [], + "text": { + "component": { + "props": { + "fields": { + "color": { + "render": false + }, + "textShadowGroup": { + "render": false + } + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sizing": [], + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "content": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "groupSlug": "contentText", + "priority": 5, + "render": true, + "attrName": "content.innerContent", + "label": "Code", + "description": "Here you can create the content that will be used within the module.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "text" + }, + "preset": "content" + }, + "component": { + "name": "divi/code", + "type": "field", + "props": { + "mode": "html" + } + } + } + } + } + } + } + }, + { + "name": "divi/accordion", + "title": "Accordion", + "category": "module", + "childrenName": [ + "divi/accordion-item" + ], + "childModuleName": "divi/accordion-item", + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": { + "groupType": "group-item", + "item": { + "groupSlug": "contentElements", + "priority": 5, + "render": true, + "component": { + "type": "group", + "name": "divi/elements", + "props": { + "grouped": false + } + } + } + }, + "html": [], + "link": [], + "loop": [], + "text": { + "priority": 15, + "component": { + "props": { + "fields": { + "color": { + "render": false + }, + "textShadowGroup": { + "render": true + }, + "orientation": { + "render": true + } + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "layout": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/layout", + "props": { + "grouped": false, + "defaultGroupAttr": { + "desktop": { + "value": { + "flexDirection": "column", + "columnGap": "30px", + "rowGap": "30px" + } + } + } + } + } + } + }, + "border": [], + "boxShadow": [], + "overflow": [], + "order": [], + "position": [], + "scroll": { + "groupType": "group-item", + "item": { + "priority": 20, + "render": true, + "groupSlug": "advancedScrollModule", + "component": { + "type": "group", + "name": "divi/scroll", + "props": { + "grouped": false, + "fields": { + "gridMotion": { + "render": true + } + } + } + } + } + }, + "sizing": [], + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "title": { + "settings": { + "decoration": { + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designTitleText", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/font", + "props": { + "grouped": false, + "groupLabel": "Title Text", + "fieldLabel": "Title", + "dynamicSubgroupHost": true, + "fields": { + "color": { + "render": true, + "priority": 5 + }, + "headingLevel": { + "render": true + } + } + } + } + } + } + } + } + }, + "closedToggleIcon": { + "settings": { + "decoration": { + "icon": { + "groupType": "group-items", + "items": { + "contentIcon": { + "groupSlug": "contentToggleIcon", + "attrName": "closedToggleIcon.decoration.icon", + "priority": 10, + "render": true, + "category": "basic_option", + "component": { + "type": "group", + "name": "divi/icon", + "props": { + "grouped": false, + "groupLabel": "Toggle Icon", + "fields": { + "icon": { + "render": true + }, + "color": { + "render": false + }, + "useSize": { + "render": false + }, + "size": { + "render": false + } + } + } + } + }, + "designIcon": { + "groupSlug": "designToggleIcon", + "attrName": "closedToggleIcon.decoration.icon", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/icon", + "props": { + "grouped": false, + "fieldLabel": "Icon", + "fields": { + "icon": { + "render": false + }, + "color": { + "render": true + }, + "useSize": { + "render": true + }, + "size": { + "render": true, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "icon-font-size", + "defaultUnit": "px", + "min": 1, + "minLimit": 1, + "max": 120 + } + } + } + } + } + } + } + } + } + } + } + }, + "content": { + "settings": { + "decoration": { + "bodyFont": { + "priority": 30, + "component": { + "props": { + "dynamicSubgroupHost": true, + "groups": { + "body": { + "groupLabel": "Body Text", + "fieldLabel": "Body" + } + } + } + } + } + } + } + }, + "openToggle": { + "settings": { + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "designToggle", + "attrName": "openToggle.decoration.background", + "subName": "color", + "label": "Open Toggle Background Color", + "description": "You can pick unique background colors for toggles when they are in their open and closed states. Choose the open state background color here.", + "priority": 10, + "render": true, + "features": { + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + }, + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designTitleText", + "attrName": "openToggle.decoration.font.font", + "subName": "color", + "label": "Open Title Text Color", + "description": "You can pick unique text colors for toggle titles when they are open and closed. Choose the open state title color here.", + "priority": 5, + "render": true, + "features": { + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + } + } + } + }, + "closedToggle": { + "settings": { + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "designToggle", + "attrName": "closedToggle.decoration.background", + "subName": "color", + "label": "Closed Toggle Background Color", + "description": "You can pick unique background colors for toggles when they are in their open and closed states. Choose the closed state background color here.", + "priority": 10, + "render": true, + "features": { + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + }, + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designClosedTitleText", + "attrName": "closedToggle.decoration.font", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/font", + "props": { + "grouped": false, + "groupLabel": "Closed Title Text", + "fieldLabel": "Closed Title", + "dynamicSubgroupHost": true, + "fields": { + "color": { + "priority": 5, + "render": true + } + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/accordion-item", + "title": "Accordion Item", + "category": "child-module", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "advanced": { + "elements": [], + "html": [], + "link": [], + "loop": [], + "text": { + "priority": 15, + "component": { + "props": { + "fields": { + "color": { + "render": false + } + } + } + } + } + }, + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "border": [], + "boxShadow": [], + "layout": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sizing": [], + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "title": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "groupSlug": "contentText", + "attrName": "title.innerContent", + "label": "Title", + "description": "The title will appear above the content and when the toggle is closed.", + "category": "basic_option", + "priority": 10, + "render": true, + "features": { + "dynamicContent": { + "type": "text" + }, + "sticky": false, + "preset": "content" + }, + "component": { + "type": "field", + "name": "divi/text" + } + } + }, + "decoration": { + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designTitleText", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/font", + "props": { + "grouped": false, + "groupLabel": "Title Text", + "fieldLabel": "Title", + "dynamicSubgroupHost": true, + "dynamicSubgroupHostLayoutStyle": "moduleLayout", + "fields": { + "color": { + "priority": 5, + "render": true + }, + "headingLevel": { + "render": true + } + } + } + } + } + } + } + } + }, + "closedToggleIcon": { + "settings": { + "decoration": { + "icon": { + "groupType": "group-items", + "items": { + "icon": { + "groupSlug": "designToggleIcon", + "attrName": "closedToggleIcon.decoration.icon", + "label": "Closed Icon", + "priority": 10, + "render": true, + "category": "basic_option", + "features": { + "preset": [ + "style", + "html" + ] + }, + "component": { + "type": "field", + "name": "divi/icon-picker" + } + }, + "iconAttributes": { + "groupSlug": "designToggleIcon", + "attrName": "closedToggleIcon.decoration.icon", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/icon", + "props": { + "grouped": false, + "fieldLabel": "Icon", + "fields": { + "icon": { + "render": false + }, + "color": { + "render": true + }, + "useSize": { + "render": true + }, + "size": { + "render": true, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "icon-font-size", + "defaultUnit": "px", + "min": 1, + "minLimit": 1, + "max": 120 + } + } + } + } + } + } + } + } + } + } + } + }, + "openToggle": { + "settings": { + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "designToggle", + "attrName": "openToggle.decoration.background", + "subName": "color", + "label": "Open Toggle Background Color", + "description": "You can pick unique background colors for toggles when they are in their open and closed states. Choose the open state background color here.", + "priority": 10, + "render": true, + "features": { + "sticky": false, + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + }, + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designTitleText", + "attrName": "openToggle.decoration.font.font", + "subName": "color", + "label": "Open Title Text Color", + "description": "You can pick unique text colors for toggle titles when they are open and closed. Choose the open state title color here.", + "priority": 5, + "render": true, + "features": { + "sticky": false, + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + } + } + } + }, + "closedToggle": { + "settings": { + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "designToggle", + "attrName": "closedToggle.decoration.background", + "subName": "color", + "label": "Closed Toggle Background Color", + "description": "You can pick unique background colors for toggles when they are in their open and closed states. Choose the open state background color here.", + "priority": 10, + "render": true, + "features": { + "sticky": false, + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + }, + "font": { + "groupType": "group-items", + "items": { + "color": { + "groupSlug": "designClosedTitleText", + "attrName": "closedToggle.decoration.font.font", + "subName": "color", + "label": "Closed Title Text Color", + "description": "You can pick unique text colors for toggle titles when they are open and closed. Choose the closed state title color here.", + "priority": 5, + "render": true, + "features": { + "sticky": false, + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + }, + "font": { + "groupSlug": "designClosedTitleText", + "attrName": "closedToggle.decoration.font", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/font", + "props": { + "grouped": false, + "groupLabel": "Closed Title Text", + "fieldLabel": "Closed Title", + "dynamicSubgroupHost": true, + "fields": { + "color": { + "render": false + } + } + } + } + } + } + } + } + } + }, + "content": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "groupSlug": "contentText", + "attrName": "content.innerContent", + "label": "Body", + "description": "Input the main text content for your module here.", + "category": "basic_option", + "priority": 10, + "render": true, + "features": { + "sticky": false, + "preset": "content", + "dynamicContent": { + "type": "text" + } + }, + "component": { + "type": "field", + "name": "divi/richtext" + } + } + }, + "decoration": { + "bodyFont": { + "priority": 30, + "component": { + "props": { + "dynamicSubgroupHost": true, + "dynamicSubgroupHostLayoutStyle": "moduleLayout", + "groups": { + "body": { + "groupLabel": "Body Text", + "fieldLabel": "Body" + } + } + } + } + } + } + } + } + } + }, + { + "name": "divi/fullwidth-header", + "title": "Hero", + "category": "fullwidth-module", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "meta": { + "meta": [] + }, + "advanced": { + "elements": [], + "html": [], + "link": [], + "loop": [], + "text": { + "groupType": "group-items", + "items": { + "text": { + "groupSlug": "designLayout", + "attrName": "module.advanced.text.text", + "subName": "orientation", + "label": "Text & Logo Alignment", + "description": "This controls how your text is aligned within the module.", + "category": "layout", + "priority": 10, + "render": true, + "multipleChoices": false, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": [ + "html" + ] + }, + "component": { + "name": "divi/button-options", + "type": "field", + "props": { + "allowClickToUnset": false, + "defaultValue": "left", + "options": { + "left": { + "icon": "divi/text-align-left", + "label": "Left" + }, + "center": { + "icon": "divi/text-align-center", + "label": "Center" + }, + "right": { + "icon": "divi/text-align-right", + "label": "Right" + } + }, + "showLabel": false + } + } + }, + "textGroup": { + "groupSlug": "designText", + "priority": 20, + "render": true, + "component": { + "type": "group", + "name": "divi/text", + "props": { + "fields": { + "orientation": { + "render": false + } + }, + "grouped": false + } + } + } + } + }, + "headerFullscreen": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "attrName": "module.advanced.headerFullscreen", + "label": "Make Fullscreen", + "description": "Here you can choose whether the header is expanded to fullscreen size.", + "category": "configuration", + "priority": 20, + "render": true, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": [ + "html" + ] + }, + "component": { + "name": "divi/toggle", + "type": "field" + } + } + } + }, + "decoration": { + "layout": { + "groupType": "group-item", + "item": { + "groupSlug": "designLayout", + "priority": 30, + "render": true, + "component": { + "type": "group", + "name": "divi/layout", + "props": { + "grouped": false + } + } + } + }, + "animation": [], + "attributes": [], + "border": [], + "background": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "overflow": [], + "order": [], + "position": [], + "scroll": [], + "sizing": { + "groupType": "group-items", + "items": { + "sizing": { + "groupSlug": "designSizing", + "priority": 20, + "render": true, + "component": { + "type": "group", + "name": "divi/sizing", + "props": { + "grouped": false + } + } + } + } + }, + "spacing": [], + "sticky": [], + "transform": [], + "transition": [], + "zIndex": [] + } + } + }, + "image": { + "settings": { + "innerContent": { + "groupType": "group-items", + "items": { + "imageInnerContentSrc": { + "priority": 20, + "groupSlug": "contentImage", + "render": true, + "attrName": "image.innerContent", + "subName": "src", + "label": "Header Image", + "description": "Upload your desired image, or type in the URL to the image you would like to display.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "image" + }, + "preset": "content" + }, + "component": { + "type": "field", + "name": "divi/upload", + "props": { + "syncImageData": { + "src": true, + "id": true, + "alt": true, + "titleText": true + } + } + } + } + } + }, + "decoration": { + "border": { + "groupType": "group-item", + "item": { + "groupSlug": "designImage", + "attrName": "image.decoration.border", + "priority": 20, + "render": true, + "component": { + "type": "group", + "name": "divi/border", + "props": { + "grouped": true + } + } + } + }, + "boxShadow": { + "groupType": "group-item", + "item": { + "groupSlug": "designImage", + "attrName": "image.decoration.boxShadow", + "priority": 30, + "render": true, + "component": { + "type": "group", + "name": "divi/box-shadow", + "props": { + "grouped": true + } + } + } + }, + "filters": { + "groupType": "group-item", + "item": { + "groupSlug": "designImage", + "attrName": "image.decoration.filters", + "priority": 30, + "render": true, + "component": { + "type": "group", + "name": "divi/filters", + "props": { + "grouped": true + } + } + } + } + }, + "advanced": { + "orientation": { + "groupType": "group-item", + "item": { + "groupSlug": "designImage", + "attrName": "image.advanced.orientation", + "label": "Image Alignment", + "description": "This controls the orientation of the image within the module.", + "category": "layout", + "priority": 10, + "render": true, + "multipleChoices": false, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": [ + "html" + ] + }, + "component": { + "name": "divi/select", + "type": "field" + } + } + } + } + } + }, + "logo": { + "settings": { + "innerContent": { + "groupType": "group-items", + "items": { + "logoInnerContentSrc": { + "priority": 10, + "groupSlug": "contentImage", + "render": true, + "attrName": "logo.innerContent", + "subName": "src", + "label": "Logo Image", + "description": "Upload your desired image, or type in the URL to the image you would like to display.", + "category": "basic_option", + "features": { + "dynamicContent": { + "type": "image" + }, + "preset": "content" + }, + "component": { + "type": "field", + "name": "divi/upload", + "props": { + "syncImageData": { + "src": true, + "id": true, + "alt": true, + "titleText": true, + "width": true, + "height": true + } + } + } + } + } + } + } + }, + "title": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "priority": 10, + "groupSlug": "contentText", + "render": true, + "attrName": "title.innerContent", + "label": "Title", + "description": "Enter your page title here.", + "category": "basic_option", + "features": { + "sticky": false, + "dynamicContent": { + "type": "text" + }, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + } + }, + "decoration": { + "font": { + "priority": 30, + "groupLabel": "Title Text", + "component": { + "type": "group", + "name": "divi/font", + "props": { + "dynamicSubgroupHost": true, + "fields": { + "headingLevel": { + "render": true + } + } + } + } + } + } + } + }, + "subhead": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "priority": 20, + "groupSlug": "contentText", + "render": true, + "attrName": "subhead.innerContent", + "label": "Subtitle", + "description": "If you would like to use a subhead, add it here. Your subhead will appear below your title in a small font.", + "category": "basic_option", + "features": { + "sticky": false, + "dynamicContent": { + "type": "text" + }, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + } + }, + "decoration": { + "font": { + "groupType": "group-item", + "item": { + "groupSlug": "designSubtitleText", + "priority": 10, + "render": true, + "component": { + "type": "group", + "name": "divi/font", + "props": { + "fieldLabel": "Subtitle", + "grouped": false, + "dynamicSubgroupHost": true + } + } + } + } + } + } + }, + "content": { + "settings": { + "innerContent": { + "groupType": "group-item", + "item": { + "priority": 50, + "groupSlug": "contentText", + "render": true, + "attrName": "content.innerContent", + "label": "Body", + "description": "Content entered here will appear below the subheading text.", + "category": "basic_option", + "features": { + "sticky": false, + "preset": "content", + "dynamicContent": { + "type": "text" + } + } + } + }, + "advanced": { + "orientation": { + "groupType": "group-item", + "item": { + "priority": 10, + "groupSlug": "designText", + "render": true, + "attrName": "content.advanced.orientation", + "label": "Text Vertical Alignment", + "description": "This setting determines the vertical alignment of your content. Your content can either be vertically centered, or aligned to the bottom.", + "category": "layout", + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": [ + "html" + ] + }, + "component": { + "type": "field", + "name": "divi/select" + } + } + }, + "maxWidth": { + "groupType": "group-item", + "item": { + "priority": 10, + "groupSlug": "designSizing", + "render": true, + "attrName": "content.advanced.maxWidth", + "label": "Content Width", + "description": "By default, elements will extend the full width of their parent element. If you would like to set a custom static width, you can do so using this option.", + "category": "layout", + "features": { + "hover": false, + "sticky": false, + "dynamicContent": { + "type": "number" + } + }, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "max-width", + "defaultUnit": "%", + "max": 100 + } + } + } + } + }, + "decoration": { + "bodyFont": { + "priority": 35, + "component": { + "type": "group", + "name": "divi/font-body", + "props": { + "dynamicSubgroupHost": true, + "groups": { + "body": { + "fieldLabel": "Body" + } + } + } + } + } + } + } + }, + "buttonOne": { + "settings": { + "innerContent": { + "groupType": "group-items", + "items": { + "buttonOneText": { + "priority": 40, + "groupSlug": "contentText", + "render": true, + "attrName": "buttonOne.innerContent", + "subName": "text", + "label": "Button #1", + "description": "Enter the text for the Button.", + "category": "basic_option", + "features": { + "sticky": false, + "dynamicContent": { + "type": "text" + }, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + }, + "buttonOneLinkUrl": { + "groupSlug": "contentLink", + "priority": 5, + "render": true, + "attrName": "buttonOne.innerContent", + "subName": "linkUrl", + "label": "Button #1 Link URL", + "description": "Enter the URL for the Button.", + "category": "basic_option", + "features": { + "responsive": false, + "hover": false, + "sticky": false, + "dynamicContent": { + "type": "url" + }, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + } + } + }, + "decoration": { + "background": [], + "border": [], + "boxShadow": [], + "button": { + "priority": 40, + "component": { + "props": { + "dynamicSubgroupHost": true, + "fields": { + "sizingGroup": { + "component": { + "props": { + "fields": { + "alignment": { + "render": false + }, + "alignSelf": { + "render": false + }, + "gridAlignSelf": { + "render": false + }, + "gridJustifySelf": { + "render": false + } + } + } + } + } + }, + "groupLabel": "Button One" + } + } + }, + "font": [], + "sizing": [], + "spacing": [] + } + } + }, + "buttonTwo": { + "settings": { + "innerContent": { + "groupType": "group-items", + "items": { + "buttonTwoText": { + "groupSlug": "contentText", + "attrName": "buttonTwo.innerContent", + "subName": "text", + "label": "Button #2", + "description": "Enter the text for the Button.", + "category": "basic_option", + "priority": 40, + "render": true, + "features": { + "sticky": false, + "dynamicContent": { + "type": "text" + }, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + }, + "buttonTwoLinkUrl": { + "groupSlug": "contentLink", + "priority": 10, + "render": true, + "attrName": "buttonTwo.innerContent", + "subName": "linkUrl", + "label": "Button #2 Link URL", + "description": "Enter the URL for the Button.", + "category": "basic_option", + "features": { + "responsive": false, + "hover": false, + "sticky": false, + "dynamicContent": { + "type": "url" + }, + "preset": "content" + }, + "component": { + "name": "divi/text", + "type": "field" + } + } + } + }, + "decoration": { + "background": [], + "border": [], + "boxShadow": [], + "button": { + "groupType": "group", + "priority": 45, + "component": { + "props": { + "dynamicSubgroupHost": true, + "fields": { + "sizingGroup": { + "component": { + "props": { + "fields": { + "alignment": { + "render": false + }, + "alignSelf": { + "render": false + }, + "gridAlignSelf": { + "render": false + }, + "gridJustifySelf": { + "render": false + } + } + } + } + } + }, + "groupLabel": "Button Two" + } + } + }, + "font": [], + "sizing": [], + "spacing": [] + } + } + }, + "scrollDown": { + "settings": { + "decoration": { + "icon": { + "groupType": "group-items", + "items": { + "scrollDownShow": { + "groupSlug": "designScrollDown", + "attrName": "scrollDown.decoration.icon", + "subName": "show", + "label": "Show Scroll Down Button", + "description": "Here you can choose whether the scroll down button is shown.", + "category": "configuration", + "render": true, + "priority": 10, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "preset": [ + "html" + ] + }, + "component": { + "type": "field", + "name": "divi/toggle" + } + }, + "scrollDownIcon": { + "groupSlug": "designScrollDown", + "attrName": "scrollDown.decoration.icon", + "label": "Icon", + "description": "Choose an icon to display for the scroll down button.", + "category": "configuration", + "render": true, + "priority": 20, + "features": { + "hover": false, + "sticky": false, + "responsive": true, + "preset": [ + "html", + "style" + ] + }, + "component": { + "type": "field", + "name": "divi/icon-picker", + "props": { + "rowCount": 1, + "showSearch": false, + "showModal": false, + "showFilter": false + } + } + }, + "scrollDownIconColor": { + "groupSlug": "designScrollDown", + "attrName": "scrollDown.decoration.icon", + "subName": "color", + "label": "Scroll Down Icon Color", + "description": "Here you can define a custom color for the scroll down icon.", + "render": true, + "priority": 30, + "features": { + "hover": true, + "sticky": false, + "responsive": true, + "preset": [ + "html", + "style" + ], + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + }, + "scrollDownIconSize": { + "groupSlug": "designScrollDown", + "attrName": "scrollDown.decoration.icon", + "subName": "size", + "label": "Scroll Down Icon Size", + "description": "Increase or decrease the size of the scroll down arrow that appears towards the bottom of the module.", + "category": "layout", + "render": true, + "priority": 40, + "features": { + "hover": true, + "sticky": false, + "responsive": true, + "preset": [ + "html", + "style" + ] + }, + "component": { + "type": "field", + "name": "divi/range", + "props": { + "cssProperty": "font-size" + } + } + } + } + } + } + } + }, + "overlay": { + "settings": { + "decoration": { + "background": { + "groupType": "group-item", + "item": { + "groupSlug": "designOverlay", + "attrName": "overlay.decoration.background", + "subName": "color", + "label": "Background Overlay Color", + "description": "Pick a color to use for the background overlay. Decreasing the opacity will allow background images and gradients to show through while still keeping the text readable.", + "render": true, + "priority": 10, + "features": { + "hover": false, + "sticky": false, + "responsive": false, + "dynamicContent": { + "type": "color" + } + }, + "component": { + "type": "field", + "name": "divi/color-picker" + } + } + } + } + } + } + } + }, + { + "name": "divi/group", + "title": "Group", + "category": "structure", + "childrenName": [], + "attributes": { + "module": { + "settings": { + "decoration": { + "animation": [], + "attributes": [], + "background": [], + "border": [], + "boxShadow": [], + "conditions": [], + "disabledOn": [], + "filters": [], + "interactions": [], + "overflow": [], + "order": [], + "transition": [], + "position": [], + "scroll": [], + "spacing": [], + "sizing": [], + "sticky": [], + "transform": [], + "zIndex": [], + "layout": [] + }, + "advanced": { + "elements": { + "groupType": "group-item", + "item": { + "groupSlug": "contentElements", + "priority": 5, + "render": true, + "component": { + "type": "group", + "name": "divi/elements", + "props": { + "grouped": false + } + } + } + }, + "html": [], + "loop": [], + "link": { + "item": { + "groupSlug": "contentLink", + "component": { + "props": { + "fieldLabel": "Group" + } + } + } + } + }, + "meta": { + "meta": [] + } + } + } + } + } + ], + "defaults": { + "divi/section": { + "module": { + "advanced": { + "innerShadow": { + "desktop": { + "value": "off" + } + } + }, + "meta": { + "adminLabel": { + "desktop": { + "value": "Section" + } + } + } + } + }, + "divi/row": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Row" + } + } + } + } + }, + "divi/column": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Column" + } + } + }, + "decoration": { + "sizing": { + "desktop": { + "value": { + "flexType": "24_24" + } + } + } + } + } + }, + "divi/text": { + "module": { + "advanced": { + "text": { + "text": { + "desktop": { + "value": { + "color": "light" + } + } + } + } + } + } + }, + "divi/heading": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Heading" + } + } + } + }, + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "headingLevel": "h1" + } + } + } + } + } + } + }, + "divi/button": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Button" + } + } + }, + "advanced": { + "html": { + "desktop": { + "value": { + "elementType": "a" + } + } + }, + "text": { + "text": { + "desktop": { + "value": { + "color": "light" + } + } + } + } + } + }, + "button": { + "decoration": { + "button": { + "desktop": { + "value": { + "icon": { + "enable": "on" + } + } + } + } + } + } + }, + "divi/image": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Image" + } + } + }, + "advanced": { + "spacing": { + "desktop": { + "value": { + "showBottomSpace": "on" + } + } + }, + "forceFullwidth": { + "desktop": { + "value": "off" + } + } + } + }, + "image": { + "innerContent": { + "desktop": { + "value": { + "linkTarget": "off" + } + } + }, + "advanced": { + "lightbox": { + "desktop": { + "value": "off" + } + }, + "overlay": { + "desktop": { + "value": { + "use": "off" + } + } + } + } + } + }, + "divi/blurb": { + "imageIcon": { + "innerContent": { + "desktop": { + "value": { + "useIcon": "off" + } + } + }, + "advanced": { + "color": { + "desktop": { + "value": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-primary-color\",\"settings\":{}}})$" + } + }, + "placement": { + "desktop": { + "value": "top" + } + } + } + }, + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Blurb" + } + } + }, + "advanced": { + "text": { + "text": { + "desktop": { + "value": { + "color": "light" + } + } + } + } + } + }, + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "headingLevel": "h4" + } + } + } + } + } + } + }, + "divi/code": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Code" + } + } + }, + "decoration": { + "spacing": { + "desktop": { + "value": { + "margin": { + "bottom": "0px" + } + } + } + } + } + } + }, + "divi/accordion": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Accordion" + } + } + } + }, + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "headingLevel": "h5" + } + } + } + } + } + }, + "closedToggleIcon": { + "decoration": { + "icon": { + "desktop": { + "value": { + "useSize": "off" + } + } + } + } + } + }, + "divi/accordion-item": { + "module": { + "advanced": { + "open": { + "desktop": { + "value": "off" + } + } + } + }, + "closedToggleIcon": { + "decoration": { + "icon": { + "desktop": { + "value": { + "useSize": "off" + } + } + } + } + } + }, + "divi/fullwidth-header": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Hero" + } + } + }, + "advanced": { + "html": { + "desktop": { + "value": { + "elementType": "section" + } + } + }, + "text": { + "text": { + "desktop": { + "value": { + "color": "dark", + "orientation": "left" + } + } + } + }, + "headerFullscreen": { + "desktop": { + "value": "off" + } + } + }, + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-primary-color\",\"settings\":{}}})$" + } + } + } + } + }, + "image": { + "advanced": { + "orientation": { + "desktop": { + "value": "center" + } + } + } + }, + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "headingLevel": "h1" + } + } + } + } + } + }, + "content": { + "advanced": { + "orientation": { + "desktop": { + "value": "center" + } + } + } + }, + "buttonOne": { + "decoration": { + "button": { + "desktop": { + "value": { + "icon": { + "enable": "on" + } + } + } + } + } + }, + "buttonTwo": { + "decoration": { + "button": { + "desktop": { + "value": { + "icon": { + "enable": "on" + } + } + } + } + } + }, + "scrollDown": { + "decoration": { + "icon": { + "desktop": { + "value": { + "show": "off", + "unicode": ";", + "type": "divi", + "weight": "400", + "size": "50px" + } + } + } + } + } + }, + "divi/group": { + "module": { + "meta": { + "adminLabel": { + "desktop": { + "value": "Group" + } + } + } + } + } + } +} diff --git a/tests/Integration/divi-metadata-fixture.php b/tests/Integration/divi-metadata-fixture.php new file mode 100644 index 0000000..02d1ef7 --- /dev/null +++ b/tests/Integration/divi-metadata-fixture.php @@ -0,0 +1,56 @@ + $definition) { + $attributes[$attrName] = ['settings' => $definition['settings'] ?? []]; + } + + $metadata[] = array_filter([ + 'name' => $module['name'], + 'title' => $module['title'] ?? '', + 'category' => $module['category'] ?? '', + 'childrenName' => $module['childrenName'] ?? null, + 'childModuleName' => $module['childModuleName'] ?? null, + 'attributes' => $attributes, + ], static fn ($value): bool => $value !== null); + $defaults[$module['name']] = ModuleRegistration::get_default_attrs($module['name'], 'default'); +} + +$file = dirname(__DIR__) . '/Fixtures/divi/metadata-sample.json'; +$json = wp_json_encode( + ['divi' => ET_BUILDER_PRODUCT_VERSION, 'metadata' => $metadata, 'defaults' => $defaults], + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE +); +if (! is_string($json) || file_put_contents($file, $json . "\n") === false) { + WP_CLI::error('Cannot write ' . $file); +} +WP_CLI::success(sprintf('metadata-sample.json: %d modules, %d bytes', count($metadata), strlen($json))); diff --git a/tests/Integration/divi-shortform-check.php b/tests/Integration/divi-shortform-check.php new file mode 100644 index 0000000..2489ed7 --- /dev/null +++ b/tests/Integration/divi-shortform-check.php @@ -0,0 +1,57 @@ +catalog(); +$problems = []; +foreach (ShortformMap::types() as $type) { + $name = ModuleCatalog::normalize($type); + if (! $catalog->has($name)) { + $problems[] = $name . ': unknown module'; + continue; + } + + foreach (ShortformMap::fields($type) as $field => $spec) { + if (! $catalog->isKnownPath($name, (string) $spec['path'])) { + $problems[] = sprintf( + '%s.%s: unknown path %s (closest: %s)', + $type, + $field, + $spec['path'], + implode(', ', $catalog->suggestions($name, (string) $spec['path'])) + ); + } + } + + $child = ShortformMap::childType($type); + if ($child !== null && ! in_array(ModuleCatalog::normalize($child), $catalog->childrenOf($name), true)) { + $problems[] = sprintf('%s: %s is no allowed child (%s)', $type, $child, implode(', ', $catalog->childrenOf($name))); + } +} + +if ($problems !== []) { + WP_CLI::error("Short form does not match the catalog:\n" . implode("\n", $problems)); +} + +WP_CLI::success(sprintf( + '%d short-form modules match the catalog of %d modules (%.0f ms).', + count(ShortformMap::types()), + count($catalog->names()), + (microtime(true) - $started) * 1000 +)); diff --git a/tests/Unit/Integrations/Divi/DiviIntegrationTest.php b/tests/Unit/Integrations/Divi/DiviIntegrationTest.php new file mode 100644 index 0000000..e8760ef --- /dev/null +++ b/tests/Unit/Integrations/Divi/DiviIntegrationTest.php @@ -0,0 +1,37 @@ +id()); + self::assertFalse($integration->isAvailable(), 'no Divi classes in unit tests'); + self::assertSame( + self::EXPECTED, + array_map(static fn (Ability $ability): string => $ability->name(), $integration->abilities()) + ); + + foreach ($integration->abilities() as $ability) { + self::assertSame('object', $ability->inputSchema()['type'], $ability->name()); + self::assertFalse($ability->inputSchema()['additionalProperties'], $ability->name()); + self::assertNotNull($ability->outputSchema(), $ability->name()); + } + } +} diff --git a/tests/Unit/Integrations/Divi/Schema/DiviCatalogAbilityTest.php b/tests/Unit/Integrations/Divi/Schema/DiviCatalogAbilityTest.php new file mode 100644 index 0000000..5b1b3b6 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Schema/DiviCatalogAbilityTest.php @@ -0,0 +1,70 @@ +allows('catalog')->andReturn(ModuleCatalogTest::sampleCatalog()); + + return new DiviCatalogAbility($modules); + } + + public function testListsModulesWithShortformFlag(): void + { + $result = $this->ability()->execute([]); + + $button = array_values(array_filter($result['modules'], static fn (array $m): bool => $m['name'] === 'divi/button')); + self::assertSame( + [['name' => 'divi/button', 'title' => 'Button', 'category' => 'module', 'children' => [], 'shortform' => true]], + $button + ); + $header = array_values(array_filter( + $result['modules'], + static fn (array $m): bool => $m['name'] === 'divi/fullwidth-header' + )); + self::assertFalse($header[0]['shortform']); + self::assertSame(DiviCatalogAbility::CODE_NOTE, $result['notes'][0]); + } + + public function testDetailOfOneModule(): void + { + $result = $this->ability()->execute(['module' => 'Accordion']); + + self::assertSame('divi/accordion', $result['module']['name']); + self::assertSame(['divi/accordion-item'], $result['module']['children']); + self::assertSame('accordion-item', $result['module']['item_type']); + self::assertContains('title.decoration.font.font', $result['module']['paths']); + + $button = $this->ability()->execute(['module' => 'divi/button'])['module']; + self::assertSame('linkUrl', $button['shortform']['url']['sub']); + } + + public function testDetailExposesTheAttributePathKindsForSanitizationRouting(): void + { + $button = $this->ability()->execute(['module' => 'divi/button'])['module']; + + self::assertSame('url', $button['path_kinds']['button.innerContent']); + self::assertArrayNotHasKey('no.such.path', $button['path_kinds']); + } + + public function testUnknownModuleAndPermission(): void + { + self::assertSame('lit_not_found', $this->ability()->execute(['module' => 'slider-pro'])->get_error_code()); + + Functions\when('current_user_can')->justReturn(false); + self::assertSame('lit_forbidden', $this->ability()->permission([])->get_error_code()); + self::assertTrue($this->ability()->annotations()['readonly']); + self::assertTrue($this->ability()->isDirect()); + } +} diff --git a/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php b/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php new file mode 100644 index 0000000..cd4fbd3 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php @@ -0,0 +1,153 @@ + (array) ($sample['defaults'][$name] ?? []) + ); + } + + public function testKnowsModulesTitlesCategoriesAndChildren(): void + { + $catalog = self::sampleCatalog(); + + self::assertTrue($catalog->has('divi/button')); + self::assertFalse($catalog->has('button')); + self::assertSame('divi/button', ModuleCatalog::normalize(' Button ')); + self::assertSame('divi/accordion-item', ModuleCatalog::normalize('divi/accordion-item')); + self::assertSame('structure', $catalog->category('divi/section')); + self::assertSame(['divi/accordion-item'], $catalog->childrenOf('divi/accordion')); + self::assertSame(['divi/column'], $catalog->childrenOf('divi/row')); + self::assertContains( + ['name' => 'divi/heading', 'title' => 'Heading', 'category' => 'module', 'children' => []], + $catalog->summary() + ); + } + + public function testDerivesAttributePathsFromSettingsAndDefaults(): void + { + $catalog = self::sampleCatalog(); + + foreach ( + [ + 'button.innerContent', 'button.decoration.button', 'button.decoration.background', + 'module.advanced.alignment', 'module.decoration.spacing', 'module.meta.adminLabel', + 'module.advanced.text.text', + ] as $path + ) { + self::assertTrue($catalog->isKnownPath('divi/button', $path), $path); + } + + self::assertTrue($catalog->isKnownPath('divi/heading', 'title.decoration.font.font')); + self::assertFalse($catalog->isKnownPath('divi/heading', 'title.decoration.font')); + self::assertTrue($catalog->isKnownPath('divi/text', 'content.decoration.bodyFont.body.font')); + self::assertTrue($catalog->isKnownPath('divi/accordion-item', 'module.advanced.open')); + self::assertTrue($catalog->isKnownPath('divi/column', 'module.decoration.sizing')); + self::assertFalse($catalog->isKnownPath('divi/button', 'button.innerContent.text')); + self::assertSame('title.decoration.font.font', $catalog->suggestions('divi/heading', 'title.decoration.fnt.font')[0]); + } + + public function testPathIndexFromHandwrittenMetadata(): void + { + $paths = AttrPathIndex::paths( + [ + 'title' => ['settings' => [ + 'innerContent' => ['groupType' => 'group-item', 'item' => ['attrName' => 'title.innerContent']], + 'decoration' => ['font' => [], 'groupType' => 'x'], + ]], + 'module' => ['settings' => [ + 'meta' => ['meta' => []], + 'advanced' => ['text' => ['item' => ['attrName' => 'module.advanced.alignment']]], + ]], + ], + ['module' => ['advanced' => ['open' => ['desktop' => ['value' => 'off']]]], 'builderVersion' => '5'] + ); + + self::assertSame( + [ + 'module.advanced.alignment', 'module.advanced.open', 'module.advanced.text.text', + 'module.advanced.text.textShadow', 'module.meta.adminLabel', 'title.decoration.font.font', + 'title.decoration.font.textShadow', 'title.innerContent', + ], + $paths + ); + } + + public function testEveryShortformPathIsKnownForTheSampledModules(): void + { + $catalog = self::sampleCatalog(); + foreach (['text', 'heading', 'button', 'image', 'blurb', 'code', 'accordion-item'] as $type) { + foreach (ShortformMap::fields($type) as $field => $spec) { + self::assertTrue( + $catalog->isKnownPath('divi/' . $type, $spec['path']), + sprintf('%s.%s → %s', $type, $field, $spec['path']) + ); + } + } + } + + public function testShortformTableIsConsistent(): void + { + $kinds = ['text', 'html', 'code', 'url', 'media', 'enum', 'onoff', 'level', 'number', 'ids', 'color']; + foreach (ShortformMap::types() as $type) { + $fields = ShortformMap::fields($type); + $primary = ShortformMap::primary($type); + self::assertTrue($primary === null || isset($fields[$primary]), $type . ' primary'); + $child = ShortformMap::childType($type); + self::assertTrue($child === null || ShortformMap::has($child), $type . ' child'); + foreach ($fields as $field => $spec) { + self::assertContains($spec['kind'], $kinds, $type . '.' . $field); + self::assertSame(1, preg_match('/^[a-zA-Z]+(\.[a-zA-Z]+)+$/', $spec['path']), $type . '.' . $field); + self::assertSame($spec['kind'] === 'media', isset($spec['keys']), $type . '.' . $field); + self::assertSame($spec['kind'] === 'enum', isset($spec['values']), $type . '.' . $field); + } + } + + self::assertSame( + ['text' => 'Termin', 'url' => '/kontakt'], + ShortformMap::values('button', ['button' => ['innerContent' => ['desktop' => ['value' => [ + 'text' => 'Termin', 'linkUrl' => '/kontakt', + ]]]]]) + ); + } + + public function testDerivesAttributePathKindsFromComponentMetadataWherePossible(): void + { + $catalog = self::sampleCatalog(); + + self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent')); + self::assertSame('text', $catalog->pathKind('divi/heading', 'title.innerContent')); + self::assertSame('code', $catalog->pathKind('divi/code', 'content.innerContent')); + self::assertSame('url', $catalog->pathKind('divi/button', 'button.innerContent')); + self::assertSame('text', $catalog->pathKind('divi/button', 'module.meta.adminLabel')); + // Structural/style groups (fonts, backgrounds, …) carry no single scalar component in + // Divi's metadata; 2b falls back to key-name routing for them (spec §5.6, ruling R3). + self::assertSame('unknown', $catalog->pathKind('divi/button', 'button.decoration.background')); + self::assertSame('unknown', $catalog->pathKind('divi/button', 'no.such.path')); + + // Every kind in the module's kinds() map is a string, keyed by a known attribute path. + $paths = $catalog->paths('divi/heading'); + foreach ($catalog->kinds('divi/heading') as $path => $kind) { + self::assertIsString($kind); + self::assertContains($path, $paths); + } + } +}