feat: PresetOverrides – lokale Werte finden, beschreiben und entfernen, die Presets überdecken
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
c3f08e2d33
commit
6546e23e7f
@@ -0,0 +1,305 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local values that cover values of the presets a block names explicitly (ruling R4, decision G9). Divi merges
|
||||||
|
* preset and block attributes with array_replace_recursive, so the comparison runs on leaves: one value at
|
||||||
|
* "<attribute path>|<breakpoint>|<state>|<sub path>" (the sub path is empty for a scalar state value, e.g. the text of
|
||||||
|
* "title.innerContent"). A local padding.top covers a preset's padding.top, never its padding.bottom.
|
||||||
|
*
|
||||||
|
* Counted are the presets of the block's modulePreset stack, the entries of its groupPreset, and the groupPresets
|
||||||
|
* nested in the assigned module presets – not the default presets Divi applies implicitly. A group preset stores its
|
||||||
|
* attributes below its own groupId; they are moved to the key the block (or the nesting module preset) uses for it.
|
||||||
|
* Two values are the same when their scalars are the same text (Divi's builder writes "700", an API client may write
|
||||||
|
* 700), lists and objects compare by content. Pure PHP; the preset data enters through the closure.
|
||||||
|
*/
|
||||||
|
final class PresetOverrides
|
||||||
|
{
|
||||||
|
private const MAX_PATHS = 5;
|
||||||
|
|
||||||
|
/** @param Closure(string $id): ?array<string, mixed> $item Stored preset item (module or group) or null. */
|
||||||
|
public function __construct(private Closure $item)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $attrs
|
||||||
|
* @return array<string, mixed> "path|breakpoint|state|subpath" => value; AttrMap::META_KEYS are left out.
|
||||||
|
*/
|
||||||
|
public static function leaves(array $attrs): array
|
||||||
|
{
|
||||||
|
$leaves = [];
|
||||||
|
foreach (AttrMap::flatten($attrs) as $path => $map) {
|
||||||
|
if (! AttrMap::isBreakpointMap($map)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($map as $breakpoint => $states) {
|
||||||
|
$states = self::plain($states);
|
||||||
|
foreach (is_array($states) ? $states : [] as $state => $value) {
|
||||||
|
self::collect($path . '|' . $breakpoint . '|' . $state, $value, '', $leaves);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $leaves;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $attrs Attributes of a block.
|
||||||
|
* @return array<string, list<string>> Preset ID => local leaves with another value than the preset's.
|
||||||
|
*/
|
||||||
|
public function find(array $attrs): array
|
||||||
|
{
|
||||||
|
$local = self::leaves($attrs);
|
||||||
|
$found = [];
|
||||||
|
foreach ($this->assigned($attrs) as $id => $leaves) {
|
||||||
|
foreach ($leaves as $key => $value) {
|
||||||
|
if (array_key_exists($key, $local) && self::canonical($local[$key]) !== self::canonical($value)) {
|
||||||
|
$found[$id][] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes every local leaf that lies on a leaf of an assigned preset – also one with the same value, which
|
||||||
|
* changes nothing – and the parents that become empty.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attrs
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function clear(array $attrs): array
|
||||||
|
{
|
||||||
|
$covered = [];
|
||||||
|
foreach ($this->assigned($attrs) as $leaves) {
|
||||||
|
$covered += $leaves;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys(self::leaves($attrs)) as $key) {
|
||||||
|
if (array_key_exists($key, $covered)) {
|
||||||
|
$attrs = AttrMap::remove($attrs, self::pathOf($key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One warning per overridden preset (ruling R5).
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attrs
|
||||||
|
* @param array<string, string> $names Preset ID => name.
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function describe(string $nodeId, array $attrs, array $names): array
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
foreach ($this->find($attrs) as $id => $keys) {
|
||||||
|
$paths = array_values(array_unique(array_map([self::class, 'label'], $keys)));
|
||||||
|
$shown = implode(', ', array_slice($paths, 0, self::MAX_PATHS));
|
||||||
|
$more = count($paths) - self::MAX_PATHS;
|
||||||
|
$lines[] = sprintf(
|
||||||
|
'%s: lokale Werte überdecken „%s“: %s%s – clear_overrides: true entfernt sie.',
|
||||||
|
$nodeId,
|
||||||
|
$names[$id] ?? $id,
|
||||||
|
$shown,
|
||||||
|
$more > 0 ? sprintf(' und %d weitere', $more) : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaves of the explicitly assigned presets, in assignment order: each module preset of the stack followed by the
|
||||||
|
* group presets nested in it, then the block's own group presets.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attrs
|
||||||
|
* @return array<string, array<string, mixed>> Preset ID => leaves (keys as in leaves()).
|
||||||
|
*/
|
||||||
|
private function assigned(array $attrs): array
|
||||||
|
{
|
||||||
|
$assigned = [];
|
||||||
|
foreach (self::idList($attrs['modulePreset'] ?? null) as $id) {
|
||||||
|
$item = $this->lookup($id);
|
||||||
|
if ($item === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->add($assigned, $id, $item, null);
|
||||||
|
foreach (self::groupEntries($item['groupPresets'] ?? null) as $target => $ids) {
|
||||||
|
$this->addGroupPresets($assigned, $target, $ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::groupEntries($attrs['groupPreset'] ?? null) as $target => $ids) {
|
||||||
|
$this->addGroupPresets($assigned, $target, $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $assigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, mixed>> $assigned
|
||||||
|
* @param list<string> $ids
|
||||||
|
*/
|
||||||
|
private function addGroupPresets(array &$assigned, string $target, array $ids): void
|
||||||
|
{
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$item = $this->lookup($id);
|
||||||
|
if ($item !== null) {
|
||||||
|
$this->add($assigned, $id, $item, $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, mixed>> $assigned
|
||||||
|
* @param array<string, mixed> $item
|
||||||
|
*/
|
||||||
|
private function add(array &$assigned, string $id, array $item, ?string $target): void
|
||||||
|
{
|
||||||
|
$attrs = is_array($item['attrs'] ?? null) ? $item['attrs'] : [];
|
||||||
|
$leaves = self::leaves($attrs);
|
||||||
|
$from = $item['groupId'] ?? null;
|
||||||
|
if ($target !== null && is_string($from) && $from !== $target) {
|
||||||
|
$leaves = self::moved($leaves, $from, $target);
|
||||||
|
}
|
||||||
|
|
||||||
|
$assigned[$id] = ($assigned[$id] ?? []) + $leaves;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed>|null */
|
||||||
|
private function lookup(string $id): ?array
|
||||||
|
{
|
||||||
|
$item = ($this->item)($id);
|
||||||
|
|
||||||
|
return is_array($item) ? $item : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaves below the attribute path $from, moved below $to; leaves outside $from do not belong to the group.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $leaves
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function moved(array $leaves, string $from, string $to): array
|
||||||
|
{
|
||||||
|
$moved = [];
|
||||||
|
foreach ($leaves as $key => $value) {
|
||||||
|
[$path, $rest] = explode('|', $key, 2);
|
||||||
|
if ($path === $from) {
|
||||||
|
$moved[$to . '|' . $rest] = $value;
|
||||||
|
} elseif (str_starts_with($path, $from . '.')) {
|
||||||
|
$moved[$to . substr($path, strlen($from)) . '|' . $rest] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $leaves
|
||||||
|
*/
|
||||||
|
private static function collect(string $head, mixed $value, string $sub, array &$leaves): void
|
||||||
|
{
|
||||||
|
$value = self::plain($value);
|
||||||
|
if (is_array($value) && ! array_is_list($value)) {
|
||||||
|
foreach ($value as $key => $inner) {
|
||||||
|
self::collect($head, $inner, $sub === '' ? (string) $key : $sub . '.' . $key, $leaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value !== []) {
|
||||||
|
$leaves[$head . '|' . $sub] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pathOf(string $key): string
|
||||||
|
{
|
||||||
|
[$path, $breakpoint, $state, $sub] = explode('|', $key, 4);
|
||||||
|
|
||||||
|
return $path . '.' . $breakpoint . '.' . $state . ($sub === '' ? '' : '.' . $sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "<path>" for desktop/value, else "<path> (<breakpoint>[, <state>])" (ruling R5). */
|
||||||
|
private static function label(string $key): string
|
||||||
|
{
|
||||||
|
[$path, $breakpoint, $state] = explode('|', $key, 4);
|
||||||
|
if ($breakpoint === 'desktop' && $state === 'value') {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path . ' (' . $breakpoint . ($state === 'value' ? '' : ', ' . $state) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
private static function idList(mixed $ids): array
|
||||||
|
{
|
||||||
|
if (is_string($ids)) {
|
||||||
|
$ids = [$ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($ids)
|
||||||
|
? array_values(array_filter($ids, static fn (mixed $id): bool => is_string($id) && $id !== ''))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<string>> Attribute path of the group => preset IDs.
|
||||||
|
*/
|
||||||
|
private static function groupEntries(mixed $entries): array
|
||||||
|
{
|
||||||
|
$entries = self::plain($entries);
|
||||||
|
$result = [];
|
||||||
|
foreach (is_array($entries) ? $entries : [] as $groupId => $entry) {
|
||||||
|
$ids = is_array($entry) ? self::idList($entry['presetId'] ?? null) : [];
|
||||||
|
if ($ids !== []) {
|
||||||
|
$result[(string) $groupId] = $ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function plain(mixed $value): mixed
|
||||||
|
{
|
||||||
|
return $value instanceof stdClass ? get_object_vars($value) : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A form two equal values share: scalars as text, objects by sorted keys, lists in order. */
|
||||||
|
private static function canonical(mixed $value): string
|
||||||
|
{
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure PHP (spec §4.4).
|
||||||
|
return (string) json_encode(self::normalized($value), JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalized(mixed $value): mixed
|
||||||
|
{
|
||||||
|
$value = self::plain($value);
|
||||||
|
if (! is_array($value)) {
|
||||||
|
return is_scalar($value) ? (string) $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = array_map([self::class, 'normalized'], $value);
|
||||||
|
if (! array_is_list($value)) {
|
||||||
|
ksort($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\PresetOverrides;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
final class PresetOverridesTest extends TestCase
|
||||||
|
{
|
||||||
|
private const SPACING = 'module.decoration.spacing';
|
||||||
|
private const BODY_FONT = 'content.decoration.bodyFont';
|
||||||
|
|
||||||
|
/** A breakpoint map with one desktop value, the shape Divi stores at an attribute path. */
|
||||||
|
private static function desktop(mixed $value): array
|
||||||
|
{
|
||||||
|
return ['desktop' => ['value' => $value]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Presets in Divi's stored format (formats §3): a module preset nesting a group preset, a group preset. */
|
||||||
|
private static function overrides(): PresetOverrides
|
||||||
|
{
|
||||||
|
$items = [
|
||||||
|
'mmmmmmmmmm' => [
|
||||||
|
'type' => 'module', 'id' => 'mmmmmmmmmm', 'name' => 'Blurb Leistung', 'moduleName' => 'divi/blurb',
|
||||||
|
'attrs' => [
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop([
|
||||||
|
'padding' => ['top' => '20px', 'bottom' => '10px'],
|
||||||
|
])]],
|
||||||
|
'title' => ['decoration' => ['font' => ['font' => self::desktop(['weight' => '700'])]]],
|
||||||
|
],
|
||||||
|
'groupPresets' => [
|
||||||
|
'title.decoration.bodyFont' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font-body'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'ssssssssss' => [
|
||||||
|
'type' => 'group', 'id' => 'ssssssssss', 'name' => 'Abstand Karte', 'groupName' => 'divi/spacing',
|
||||||
|
'groupId' => self::SPACING, 'moduleName' => 'divi/blurb',
|
||||||
|
'attrs' => ['module' => ['decoration' => ['spacing' => self::desktop([
|
||||||
|
'margin' => ['bottom' => '24px'],
|
||||||
|
])]]],
|
||||||
|
],
|
||||||
|
'ffffffffff' => [
|
||||||
|
'type' => 'group', 'id' => 'ffffffffff', 'name' => 'Fließtext', 'groupName' => 'divi/font-body',
|
||||||
|
'groupId' => self::BODY_FONT, 'moduleName' => 'divi/text',
|
||||||
|
'attrs' => ['content' => ['decoration' => ['bodyFont' => ['desktop' => [
|
||||||
|
'value' => ['size' => '18px'],
|
||||||
|
'hover' => ['size' => '20px'],
|
||||||
|
], 'phone' => ['value' => ['size' => '16px']]]]]],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return new PresetOverrides(static fn (string $id): ?array => $items[$id] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLeavesFlattenBreakpointsStatesAndSubpaths(): void
|
||||||
|
{
|
||||||
|
$leaves = PresetOverrides::leaves([
|
||||||
|
'builderVersion' => '5.2.1',
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'groupPreset' => [self::SPACING => ['presetId' => ['ssssssssss'], 'groupName' => 'divi/spacing']],
|
||||||
|
'module' => ['decoration' => ['spacing' => [
|
||||||
|
'desktop' => ['value' => ['padding' => ['top' => '20px'], 'syncVertical' => 'on'], 'hover' => [
|
||||||
|
'margin' => ['bottom' => '4px'],
|
||||||
|
]],
|
||||||
|
'phone' => ['value' => ['padding' => ['top' => '8px']]],
|
||||||
|
]]],
|
||||||
|
'title' => ['innerContent' => self::desktop('Hallo'), 'decoration' => ['font' => ['font' => self::desktop(
|
||||||
|
new stdClass()
|
||||||
|
)]]],
|
||||||
|
'content' => ['decoration' => ['bodyFont' => self::desktop(['families' => ['Lato', 'Arial']])]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'content.decoration.bodyFont|desktop|value|families' => ['Lato', 'Arial'],
|
||||||
|
'module.decoration.spacing|desktop|value|padding.top' => '20px',
|
||||||
|
'module.decoration.spacing|desktop|value|syncVertical' => 'on',
|
||||||
|
'module.decoration.spacing|desktop|hover|margin.bottom' => '4px',
|
||||||
|
'module.decoration.spacing|phone|value|padding.top' => '8px',
|
||||||
|
'title.innerContent|desktop|value|' => 'Hallo',
|
||||||
|
],
|
||||||
|
$leaves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindReportsOnlyDifferingValuesOfTheModulePreset(): void
|
||||||
|
{
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '40px', 'bottom' => '10px']])]],
|
||||||
|
// 700 as a number is the same value as "700" (the builder writes strings, an API client may not).
|
||||||
|
'title' => ['decoration' => ['font' => ['font' => self::desktop(['weight' => 700])]]],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['mmmmmmmmmm' => ['module.decoration.spacing|desktop|value|padding.top']],
|
||||||
|
self::overrides()->find($attrs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoOverlapNoReport(): void
|
||||||
|
{
|
||||||
|
// A local padding.left does not cover the preset's padding.top/bottom (array_replace_recursive).
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['left' => '5px']])]],
|
||||||
|
'title' => ['innerContent' => self::desktop('Text')],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame([], self::overrides()->find($attrs));
|
||||||
|
self::assertSame([], self::overrides()->find([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupPresetLeavesMoveFromTheStoredGroupIdToTheBlocksKey(): void
|
||||||
|
{
|
||||||
|
// The preset was made for content.decoration.bodyFont; this block uses it on text.decoration.bodyFont.
|
||||||
|
$attrs = [
|
||||||
|
'groupPreset' => ['text.decoration.bodyFont' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font-body']],
|
||||||
|
'text' => ['decoration' => ['bodyFont' => [
|
||||||
|
'desktop' => ['value' => ['size' => '22px'], 'hover' => ['size' => '20px']],
|
||||||
|
'phone' => ['value' => ['size' => '12px']],
|
||||||
|
]]],
|
||||||
|
// Same path as the preset's stored groupId, but not the block's key: no overlap.
|
||||||
|
'content' => ['decoration' => ['bodyFont' => self::desktop(['size' => '99px'])]],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['ffffffffff' => [
|
||||||
|
'text.decoration.bodyFont|desktop|value|size',
|
||||||
|
'text.decoration.bodyFont|phone|value|size',
|
||||||
|
]],
|
||||||
|
self::overrides()->find($attrs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNestedGroupPresetsOfAnAssignedModulePresetCount(): void
|
||||||
|
{
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'title' => ['decoration' => ['bodyFont' => self::desktop(['size' => '30px'])]],
|
||||||
|
];
|
||||||
|
|
||||||
|
// The nested entry sits on title.decoration.bodyFont of the module preset; its leaves move there.
|
||||||
|
self::assertSame(
|
||||||
|
['ffffffffff' => ['title.decoration.bodyFont|desktop|value|size']],
|
||||||
|
self::overrides()->find($attrs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPresetsNobodyAssignedAreNotCounted(): void
|
||||||
|
{
|
||||||
|
// Default presets ("", "default", "_initial") and unknown IDs are no explicit assignment (ruling R4).
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => ['', 'default', '_initial', 'unbekannt'],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '40px']])]],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame([], self::overrides()->find($attrs));
|
||||||
|
self::assertSame($attrs, self::overrides()->clear($attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStackAndSingleStringAreRead(): void
|
||||||
|
{
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => 'mmmmmmmmmm',
|
||||||
|
'groupPreset' => [self::SPACING => ['presetId' => 'ssssssssss', 'groupName' => 'divi/spacing']],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop([
|
||||||
|
'padding' => ['bottom' => '11px'], 'margin' => ['bottom' => '1px'],
|
||||||
|
])]],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'mmmmmmmmmm' => ['module.decoration.spacing|desktop|value|padding.bottom'],
|
||||||
|
'ssssssssss' => ['module.decoration.spacing|desktop|value|margin.bottom'],
|
||||||
|
],
|
||||||
|
self::overrides()->find($attrs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClearRemovesLocalLeavesOnPresetLeavesAndEmptyParents(): void
|
||||||
|
{
|
||||||
|
$attrs = [
|
||||||
|
'builderVersion' => '5.2.1',
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop([
|
||||||
|
// padding.top is a preset leaf (different value), padding.bottom too (same value), left is not.
|
||||||
|
'padding' => ['top' => '40px', 'bottom' => '10px', 'left' => '5px'],
|
||||||
|
])]],
|
||||||
|
'title' => [
|
||||||
|
'innerContent' => self::desktop('Hallo'),
|
||||||
|
'decoration' => ['font' => ['font' => self::desktop(['weight' => '400'])]],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'builderVersion' => '5.2.1',
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['left' => '5px']])]],
|
||||||
|
'title' => ['innerContent' => self::desktop('Hallo')],
|
||||||
|
],
|
||||||
|
self::overrides()->clear($attrs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClearDropsAWholeBranchWhenNothingElseIsLeft(): void
|
||||||
|
{
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'title' => ['decoration' => ['font' => ['font' => self::desktop(['weight' => '400'])]]],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame(['modulePreset' => ['mmmmmmmmmm']], self::overrides()->clear($attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescribeFollowsRulingR5(): void
|
||||||
|
{
|
||||||
|
$attrs = [
|
||||||
|
'modulePreset' => ['mmmmmmmmmm'],
|
||||||
|
'groupPreset' => ['text.decoration.bodyFont' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font-body']],
|
||||||
|
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '40px']])]],
|
||||||
|
'text' => ['decoration' => ['bodyFont' => [
|
||||||
|
'desktop' => ['hover' => ['size' => '1px'], 'value' => ['size' => '2px']],
|
||||||
|
'phone' => ['value' => ['size' => '3px']],
|
||||||
|
]]],
|
||||||
|
];
|
||||||
|
$names = ['mmmmmmmmmm' => 'Blurb Leistung', 'ffffffffff' => 'Fließtext'];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
's0.r0.c0.m2: lokale Werte überdecken „Blurb Leistung“: module.decoration.spacing '
|
||||||
|
. '– clear_overrides: true entfernt sie.',
|
||||||
|
's0.r0.c0.m2: lokale Werte überdecken „Fließtext“: text.decoration.bodyFont, '
|
||||||
|
. 'text.decoration.bodyFont (desktop, hover), text.decoration.bodyFont (phone) '
|
||||||
|
. '– clear_overrides: true entfernt sie.',
|
||||||
|
],
|
||||||
|
self::overrides()->describe('s0.r0.c0.m2', $attrs, $names)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescribeShowsAtMostFivePathsAndCountsTheRest(): void
|
||||||
|
{
|
||||||
|
$preset = ['id' => 'pppppppppp', 'name' => 'Viele', 'attrs' => []];
|
||||||
|
$local = ['modulePreset' => ['pppppppppp']];
|
||||||
|
foreach (range(1, 7) as $number) {
|
||||||
|
$preset['attrs']['module']['decoration']['p' . $number] = self::desktop('a');
|
||||||
|
$local['module']['decoration']['p' . $number] = self::desktop('b');
|
||||||
|
}
|
||||||
|
$overrides = new PresetOverrides(static fn (string $id): ?array => $id === 'pppppppppp' ? $preset : null);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['x1: lokale Werte überdecken „pppppppppp“: module.decoration.p1, module.decoration.p2, '
|
||||||
|
. 'module.decoration.p3, module.decoration.p4, module.decoration.p5 und 2 weitere '
|
||||||
|
. '– clear_overrides: true entfernt sie.'],
|
||||||
|
$overrides->describe('x1', $local, [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDescribeIsEmptyWithoutOverrides(): void
|
||||||
|
{
|
||||||
|
self::assertSame([], self::overrides()->describe('s0', ['modulePreset' => ['mmmmmmmmmm']], []));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user