feat: read the Divi design system live – lit/divi-design-system
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -19,7 +19,7 @@ des verbundenen WordPress-Benutzers.
|
|||||||
|
|
||||||
Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-content-query`,
|
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-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-divi-catalog`.
|
`lit-media-preview`, `lit-media-download`, `lit-media-upload-from-url`, `lit-divi-catalog`, `lit-divi-design-system`.
|
||||||
|
|
||||||
Welche Tools direkt sichtbar sind, lässt sich unter Einstellungen → LIT MCP → Tools ändern (außer bei
|
Welche Tools direkt sichtbar sind, lässt sich unter Einstellungen → LIT MCP → Tools ändern (außer bei
|
||||||
`lit-guide`, das immer direkt bleibt).
|
`lit-guide`, das immer direkt bleibt).
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The site's Divi design system as read live from Divi (spec §2 principle 2, §5.7): global
|
||||||
|
* colors, global variables, theme fonts and sizes, module and group presets. Pure PHP –
|
||||||
|
* fromDivi() takes the raw arrays Divi returns. Placeholders (spec §5.4) resolve against it.
|
||||||
|
*/
|
||||||
|
final class DesignSystem
|
||||||
|
{
|
||||||
|
/** Aliases of the five customizer colors. */
|
||||||
|
public const COLOR_ALIASES = [
|
||||||
|
'primary' => 'gcid-primary-color',
|
||||||
|
'secondary' => 'gcid-secondary-color',
|
||||||
|
'heading' => 'gcid-heading-color',
|
||||||
|
'body' => 'gcid-body-color',
|
||||||
|
'link' => 'gcid-link-color',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Theme fonts as Divi stores them among the global font variables. */
|
||||||
|
public const FONT_VARIABLES = ['heading' => '--et_global_heading_font', 'body' => '--et_global_body_font'];
|
||||||
|
|
||||||
|
/** Theme option => Divi's default, read from the et_divi option. */
|
||||||
|
public const SIZE_OPTIONS = [
|
||||||
|
'body_font_size' => '14',
|
||||||
|
'body_header_size' => '30',
|
||||||
|
'body_font_height' => '1.7',
|
||||||
|
'body_header_height' => '1',
|
||||||
|
'content_width' => '1080',
|
||||||
|
'gutter_width' => '3',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{id: string, label: string, value: string}> $colors
|
||||||
|
* @param list<array{id: string, type: string, label: string, value: string}> $variables
|
||||||
|
* @param list<array{kind: string, module: string, id: string, name: string, default: bool}> $presets
|
||||||
|
* @param array{heading: string, body: string} $fonts
|
||||||
|
* @param array<string, string> $sizes
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private array $colors,
|
||||||
|
private array $variables,
|
||||||
|
private array $presets,
|
||||||
|
private array $fonts,
|
||||||
|
private array $sizes
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array-key, mixed> $colors GlobalData::get_global_colors()
|
||||||
|
* @param array<array-key, mixed> $variables GlobalData::get_global_variables()
|
||||||
|
* @param array<array-key, mixed> $presets GlobalPreset::get_data()
|
||||||
|
* @param array<array-key, mixed> $options get_option('et_divi')
|
||||||
|
*/
|
||||||
|
public static function fromDivi(array $colors, array $variables, array $presets, array $options): self
|
||||||
|
{
|
||||||
|
// Divi returns some groups as stdClass (e.g. every variable type) – work on plain arrays.
|
||||||
|
$colors = (array) self::plain($colors);
|
||||||
|
$variables = (array) self::plain($variables);
|
||||||
|
$presets = (array) self::plain($presets);
|
||||||
|
$colorList = [];
|
||||||
|
foreach ($colors as $id => $color) {
|
||||||
|
if (is_array($color) && ($color['status'] ?? 'active') === 'active' && is_string($color['color'] ?? null)) {
|
||||||
|
$colorList[] = [
|
||||||
|
'id' => (string) $id,
|
||||||
|
'label' => is_string($color['label'] ?? null) ? $color['label'] : (string) $id,
|
||||||
|
'value' => $color['color'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$variableList = [];
|
||||||
|
$fonts = ['heading' => '', 'body' => ''];
|
||||||
|
foreach ($variables as $type => $items) {
|
||||||
|
foreach (is_array($items) ? $items : [] as $id => $item) {
|
||||||
|
if (! is_array($item) || ($item['status'] ?? 'active') !== 'active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = is_scalar($item['value'] ?? null) ? (string) $item['value'] : '';
|
||||||
|
$font = array_search((string) $id, self::FONT_VARIABLES, true);
|
||||||
|
if ($font !== false) {
|
||||||
|
$fonts[$font] = $value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$variableList[] = [
|
||||||
|
'id' => (string) $id,
|
||||||
|
'type' => (string) $type,
|
||||||
|
'label' => is_string($item['label'] ?? null) ? $item['label'] : (string) $id,
|
||||||
|
'value' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sizes = [];
|
||||||
|
foreach (self::SIZE_OPTIONS as $option => $default) {
|
||||||
|
$value = $options[$option] ?? null;
|
||||||
|
$sizes[$option] = is_scalar($value) && (string) $value !== '' ? (string) $value : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($colorList, $variableList, self::presetList($presets), $fonts, $sizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{id: string, label: string, value: string}> */
|
||||||
|
public function colors(): array
|
||||||
|
{
|
||||||
|
return $this->colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{id: string, type: string, label: string, value: string}> */
|
||||||
|
public function variables(): array
|
||||||
|
{
|
||||||
|
return $this->variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{kind: string, module: string, id: string, name: string, default: bool}> */
|
||||||
|
public function presets(): array
|
||||||
|
{
|
||||||
|
return $this->presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{heading: string, body: string} */
|
||||||
|
public function fonts(): array
|
||||||
|
{
|
||||||
|
return $this->fonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function sizes(): array
|
||||||
|
{
|
||||||
|
return $this->sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** gcid of an alias (primary …), an ID or a label (case-insensitive); null when unknown. */
|
||||||
|
public function colorId(string $name): ?string
|
||||||
|
{
|
||||||
|
$key = mb_strtolower(trim($name));
|
||||||
|
$alias = self::COLOR_ALIASES[$key] ?? null;
|
||||||
|
foreach ($this->colors as $color) {
|
||||||
|
if ($color['id'] === $alias || $color['id'] === trim($name) || mb_strtolower($color['label']) === $key) {
|
||||||
|
return $color['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{id: string, type: string, label: string, value: string}|null By label or ID. */
|
||||||
|
public function variable(string $name): ?array
|
||||||
|
{
|
||||||
|
$key = mb_strtolower(trim($name));
|
||||||
|
foreach ($this->variables as $variable) {
|
||||||
|
if ($variable['id'] === trim($name) || mb_strtolower($variable['label']) === $key) {
|
||||||
|
return $variable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Module preset ID by name or ID for a full module name ("divi/button"); null when unknown. */
|
||||||
|
public function modulePresetId(string $module, string $name): ?string
|
||||||
|
{
|
||||||
|
$key = mb_strtolower(trim($name));
|
||||||
|
foreach ($this->presets as $preset) {
|
||||||
|
if (
|
||||||
|
$preset['kind'] === 'module'
|
||||||
|
&& $preset['module'] === $module
|
||||||
|
&& ($preset['id'] === trim($name) || mb_strtolower($preset['name']) === $key)
|
||||||
|
) {
|
||||||
|
return $preset['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> Names of the module presets of $module. */
|
||||||
|
public function modulePresetNames(string $module): array
|
||||||
|
{
|
||||||
|
$names = [];
|
||||||
|
foreach ($this->presets as $preset) {
|
||||||
|
if ($preset['kind'] === 'module' && $preset['module'] === $module) {
|
||||||
|
$names[] = $preset['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> Preset ID => name (all presets). */
|
||||||
|
public function presetNames(): array
|
||||||
|
{
|
||||||
|
$names = [];
|
||||||
|
foreach ($this->presets as $preset) {
|
||||||
|
$names[$preset['id']] = $preset['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $names;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Placeholder that stands for a color in new content (spec §5.4). */
|
||||||
|
public static function colorPlaceholder(string $id, string $label): string
|
||||||
|
{
|
||||||
|
$alias = array_search($id, self::COLOR_ALIASES, true);
|
||||||
|
|
||||||
|
return '{{color:' . ($alias !== false ? $alias : $label) . '}}';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function plain(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (is_object($value)) {
|
||||||
|
$value = get_object_vars($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($value) ? array_map([self::class, 'plain'], $value) : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<array-key, mixed> $data GlobalPreset::get_data():
|
||||||
|
* {module|group: {name: {default, items: {id: item}}}}
|
||||||
|
* @return list<array{kind: string, module: string, id: string, name: string, default: bool}>
|
||||||
|
*/
|
||||||
|
private static function presetList(array $data): array
|
||||||
|
{
|
||||||
|
$list = [];
|
||||||
|
foreach (['module', 'group'] as $kind) {
|
||||||
|
foreach (is_array($data[$kind] ?? null) ? $data[$kind] : [] as $module => $record) {
|
||||||
|
$default = is_array($record) && is_string($record['default'] ?? null) ? $record['default'] : '';
|
||||||
|
$items = is_array($record) && is_array($record['items'] ?? null) ? $record['items'] : [];
|
||||||
|
foreach ($items as $id => $item) {
|
||||||
|
$list[] = [
|
||||||
|
'kind' => $kind,
|
||||||
|
'module' => (string) $module,
|
||||||
|
'id' => (string) $id,
|
||||||
|
'name' => is_array($item) && is_string($item['name'] ?? null) ? $item['name'] : (string) $id,
|
||||||
|
'default' => (string) $id === $default,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use ET\Builder\Packages\GlobalData\GlobalData;
|
||||||
|
use ET\Builder\Packages\GlobalData\GlobalPreset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the design system from Divi's own APIs once per request (spec §2 principle 2 – no
|
||||||
|
* copy is ever stored). Not final so abilities can be unit-tested with a mock.
|
||||||
|
*/
|
||||||
|
class DesignSystemReader
|
||||||
|
{
|
||||||
|
private ?DesignSystem $designSystem = null;
|
||||||
|
|
||||||
|
public function read(): DesignSystem
|
||||||
|
{
|
||||||
|
if ($this->designSystem === null) {
|
||||||
|
$colors = GlobalData::get_global_colors();
|
||||||
|
$variables = GlobalData::get_global_variables();
|
||||||
|
$presets = GlobalPreset::get_data();
|
||||||
|
$options = get_option('et_divi', []);
|
||||||
|
$this->designSystem = DesignSystem::fromDivi(
|
||||||
|
is_array($colors) ? $colors : [],
|
||||||
|
is_array($variables) ? $variables : [],
|
||||||
|
is_array($presets) ? $presets : [],
|
||||||
|
is_array($options) ? $options : []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->designSystem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit/divi-design-system: global colors, variables, theme fonts/sizes and presets with the
|
||||||
|
* placeholder to use for each (spec §5.4, §5.7). Read live from Divi; writing follows in
|
||||||
|
* Etappe 4.
|
||||||
|
*/
|
||||||
|
final class DiviDesignSystemAbility implements Ability
|
||||||
|
{
|
||||||
|
public function __construct(private DesignSystemReader $reader)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'lit/divi-design-system';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Divi-Designsystem';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Liest das Designsystem der Website aus Divi: globale Farben, globale Variablen, Theme-Schriften und '
|
||||||
|
. '-Größen, Modul- und Gruppen-Presets – jeweils mit dem Platzhalter ({{color:…}}, {{var:…}}, '
|
||||||
|
. '{{font:…}}, preset), den neue Inhalte statt fester Werte verwenden sollen.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return ['type' => 'object', 'properties' => [], 'additionalProperties' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function outputSchema(): ?array
|
||||||
|
{
|
||||||
|
$entry = static fn (array $properties): array => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => $properties,
|
||||||
|
'required' => array_keys($properties),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'colors' => $entry([
|
||||||
|
'id' => ['type' => 'string'],
|
||||||
|
'label' => ['type' => 'string'],
|
||||||
|
'value' => ['type' => 'string'],
|
||||||
|
'placeholder' => ['type' => 'string'],
|
||||||
|
]),
|
||||||
|
'variables' => $entry([
|
||||||
|
'id' => ['type' => 'string'],
|
||||||
|
'type' => ['type' => 'string'],
|
||||||
|
'label' => ['type' => 'string'],
|
||||||
|
'value' => ['type' => 'string'],
|
||||||
|
'placeholder' => ['type' => 'string'],
|
||||||
|
]),
|
||||||
|
'fonts' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => ['heading' => ['type' => 'string'], 'body' => ['type' => 'string']],
|
||||||
|
'required' => ['heading', 'body'],
|
||||||
|
],
|
||||||
|
'sizes' => ['type' => 'object'],
|
||||||
|
'presets' => $entry([
|
||||||
|
'kind' => ['type' => 'string'],
|
||||||
|
'module' => ['type' => 'string'],
|
||||||
|
'id' => ['type' => 'string'],
|
||||||
|
'name' => ['type' => 'string'],
|
||||||
|
'default' => ['type' => 'boolean'],
|
||||||
|
]),
|
||||||
|
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
],
|
||||||
|
'required' => ['colors', 'variables', 'fonts', 'sizes', 'presets', '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
|
||||||
|
{
|
||||||
|
$design = $this->reader->read();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'colors' => array_map(
|
||||||
|
static fn (array $color): array => $color + [
|
||||||
|
'placeholder' => DesignSystem::colorPlaceholder($color['id'], $color['label']),
|
||||||
|
],
|
||||||
|
$design->colors()
|
||||||
|
),
|
||||||
|
'variables' => array_map(
|
||||||
|
static fn (array $variable): array => $variable
|
||||||
|
+ ['placeholder' => '{{var:' . $variable['label'] . '}}'],
|
||||||
|
$design->variables()
|
||||||
|
),
|
||||||
|
'fonts' => $design->fonts(),
|
||||||
|
'sizes' => $design->sizes(),
|
||||||
|
'presets' => $design->presets(),
|
||||||
|
'notes' => [
|
||||||
|
'Schriften: {{font:heading}} bzw. {{font:body}}; Presets in der Kurzform über "preset": "<Name>".',
|
||||||
|
'Feste Farben oder Schriften nur verwenden, wenn der Nutzer es ausdrücklich möchte.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi;
|
namespace LIT\MCP\Integrations\Divi;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
|
||||||
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
|
use LIT\MCP\Integrations\Divi\Schema\DiviCatalogAbility;
|
||||||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||||
use LIT\MCP\Integrations\Integration;
|
use LIT\MCP\Integrations\Integration;
|
||||||
@@ -16,10 +18,12 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
|||||||
final class DiviIntegration implements Integration
|
final class DiviIntegration implements Integration
|
||||||
{
|
{
|
||||||
private DiviModules $modules;
|
private DiviModules $modules;
|
||||||
|
private DesignSystemReader $design;
|
||||||
|
|
||||||
public function __construct(private ContentSchema $schema)
|
public function __construct(private ContentSchema $schema)
|
||||||
{
|
{
|
||||||
$this->modules = new DiviModules();
|
$this->modules = new DiviModules();
|
||||||
|
$this->design = new DesignSystemReader();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function id(): string
|
public function id(): string
|
||||||
@@ -36,6 +40,7 @@ final class DiviIntegration implements Integration
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new DiviCatalogAbility($this->modules),
|
new DiviCatalogAbility($this->modules),
|
||||||
|
new DiviDesignSystemAbility($this->design),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class DesignSystemTest extends TestCase
|
||||||
|
{
|
||||||
|
/** Shapes as GlobalData / GlobalPreset return them (Divi 5.2.1, verified in DDEV). */
|
||||||
|
public static function sample(): DesignSystem
|
||||||
|
{
|
||||||
|
return DesignSystem::fromDivi(
|
||||||
|
[
|
||||||
|
'gcid-primary-color' => ['color' => '#2ea3f2', 'label' => 'Primärfarbe', 'status' => 'active'],
|
||||||
|
'gcid-body-color' => ['color' => '#666666', 'label' => 'Fließtextfarbe', 'status' => 'active'],
|
||||||
|
'gcid-a1b2' => ['color' => '#0b3d59', 'label' => 'Nachtblau', 'status' => 'active'],
|
||||||
|
'gcid-old' => ['color' => '#ff0000', 'label' => 'Alt', 'status' => 'inactive'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'numbers' => ['gvid-r1' => ['id' => 'gvid-r1', 'label' => 'Radius', 'value' => '8px', 'status' => 'active']],
|
||||||
|
'strings' => ['gvid-s1' => ['id' => 'gvid-s1', 'label' => 'Telefon', 'value' => '0221 123', 'status' => 'active']],
|
||||||
|
// Divi returns every variable type as stdClass (verified in DDEV).
|
||||||
|
'fonts' => (object) [
|
||||||
|
'--et_global_heading_font' => ['id' => '--et_global_heading_font', 'label' => 'Überschrift', 'value' => 'Open Sans', 'status' => 'active'],
|
||||||
|
'--et_global_body_font' => ['id' => '--et_global_body_font', 'label' => 'Fließtext', 'value' => 'Lato', 'status' => 'active'],
|
||||||
|
],
|
||||||
|
'links' => new \stdClass(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
|
||||||
|
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
|
||||||
|
'p-2' => ['id' => 'p-2', 'name' => 'CTA primär', 'moduleName' => 'divi/button'],
|
||||||
|
]]],
|
||||||
|
'group' => ['divi/font' => ['default' => '', 'items' => [
|
||||||
|
'g-1' => ['id' => 'g-1', 'name' => 'Fließtext groß', 'groupName' => 'divi/font'],
|
||||||
|
]]],
|
||||||
|
],
|
||||||
|
['body_font_size' => '16']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadsColorsVariablesFontsSizesAndPresets(): void
|
||||||
|
{
|
||||||
|
$design = self::sample();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['id' => 'gcid-a1b2', 'label' => 'Nachtblau', 'value' => '#0b3d59'],
|
||||||
|
$design->colors()[2]
|
||||||
|
);
|
||||||
|
self::assertCount(3, $design->colors());
|
||||||
|
self::assertSame(['heading' => 'Open Sans', 'body' => 'Lato'], $design->fonts());
|
||||||
|
self::assertSame(
|
||||||
|
[['id' => 'gvid-r1', 'type' => 'numbers', 'label' => 'Radius', 'value' => '8px'],
|
||||||
|
['id' => 'gvid-s1', 'type' => 'strings', 'label' => 'Telefon', 'value' => '0221 123']],
|
||||||
|
$design->variables()
|
||||||
|
);
|
||||||
|
self::assertSame('16', $design->sizes()['body_font_size']);
|
||||||
|
self::assertSame('30', $design->sizes()['body_header_size']);
|
||||||
|
self::assertSame(
|
||||||
|
['kind' => 'module', 'module' => 'divi/button', 'id' => 'p-1', 'name' => 'Standard', 'default' => true],
|
||||||
|
$design->presets()[0]
|
||||||
|
);
|
||||||
|
self::assertSame(['p-1' => 'Standard', 'p-2' => 'CTA primär', 'g-1' => 'Fließtext groß'], $design->presetNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLooksUpByAliasLabelOrId(): void
|
||||||
|
{
|
||||||
|
$design = self::sample();
|
||||||
|
|
||||||
|
self::assertSame('gcid-primary-color', $design->colorId('primary'));
|
||||||
|
self::assertSame('gcid-a1b2', $design->colorId('nachtblau'));
|
||||||
|
self::assertSame('gcid-a1b2', $design->colorId('gcid-a1b2'));
|
||||||
|
self::assertNull($design->colorId('secondary'));
|
||||||
|
self::assertNull($design->colorId('Alt'));
|
||||||
|
self::assertSame('gvid-s1', $design->variable('telefon')['id'] ?? null);
|
||||||
|
self::assertSame('p-2', $design->modulePresetId('divi/button', 'cta PRIMÄR'));
|
||||||
|
self::assertNull($design->modulePresetId('divi/text', 'Standard'));
|
||||||
|
self::assertSame(['Standard', 'CTA primär'], $design->modulePresetNames('divi/button'));
|
||||||
|
self::assertSame('{{color:primary}}', DesignSystem::colorPlaceholder('gcid-primary-color', 'Primärfarbe'));
|
||||||
|
self::assertSame('{{color:Nachtblau}}', DesignSystem::colorPlaceholder('gcid-a1b2', 'Nachtblau'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToleratesEmptyOrOddInput(): void
|
||||||
|
{
|
||||||
|
$design = DesignSystem::fromDivi([], ['fonts' => 'x'], ['module' => 'y'], []);
|
||||||
|
|
||||||
|
self::assertSame([], $design->colors());
|
||||||
|
self::assertSame(['heading' => '', 'body' => ''], $design->fonts());
|
||||||
|
self::assertSame([], $design->presets());
|
||||||
|
self::assertSame('14', $design->sizes()['body_font_size']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class DiviDesignSystemAbilityTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testReturnsDesignSystemWithPlaceholders(): void
|
||||||
|
{
|
||||||
|
$reader = Mockery::mock(DesignSystemReader::class);
|
||||||
|
$reader->allows('read')->andReturn(DesignSystemTest::sample());
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
$ability = new DiviDesignSystemAbility($reader);
|
||||||
|
|
||||||
|
self::assertTrue($ability->permission([]));
|
||||||
|
$result = $ability->execute([]);
|
||||||
|
|
||||||
|
self::assertSame('{{color:primary}}', $result['colors'][0]['placeholder']);
|
||||||
|
self::assertSame('{{color:Nachtblau}}', $result['colors'][2]['placeholder']);
|
||||||
|
self::assertSame('{{var:Telefon}}', $result['variables'][1]['placeholder']);
|
||||||
|
self::assertSame(['heading' => 'Open Sans', 'body' => 'Lato'], $result['fonts']);
|
||||||
|
self::assertSame('CTA primär', $result['presets'][1]['name']);
|
||||||
|
self::assertStringContainsString('{{font:heading}}', $result['notes'][0]);
|
||||||
|
self::assertTrue($ability->isDirect());
|
||||||
|
self::assertSame(['colors', 'variables', 'fonts', 'sizes', 'presets', 'notes'], $ability->outputSchema()['required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNeedsEditPosts(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
|
||||||
|
$result = (new DiviDesignSystemAbility(Mockery::mock(DesignSystemReader::class)))->permission([]);
|
||||||
|
|
||||||
|
self::assertSame('lit_forbidden', $result->get_error_code());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ final class DiviIntegrationTest extends TestCase
|
|||||||
/** Grows with every task that adds Divi abilities – keep in the order of abilities(). */
|
/** Grows with every task that adds Divi abilities – keep in the order of abilities(). */
|
||||||
private const EXPECTED = [
|
private const EXPECTED = [
|
||||||
'lit/divi-catalog',
|
'lit/divi-catalog',
|
||||||
|
'lit/divi-design-system',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function testProvidesTheDiviAbilitiesOnlyWithDivi5(): void
|
public function testProvidesTheDiviAbilitiesOnlyWithDivi5(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user