refactor: ein Helfer für Gruppen-Kurznamen (NodeSummary::groupShortName), ESCAPED_LETTER_REGEX am UsageQuery-Interface, Docblock DiviComposition::presets; Tests für Muster, Kurzname und Gruppen-Überdeckung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6d7c59e3fa
commit
e7793fd595
@@ -23,6 +23,12 @@ final class NodeSummary
|
|||||||
/** The admin label is a one-line title in the layers panel, never an unbounded copy. */
|
/** The admin label is a one-line title in the layers panel, never an unbounded copy. */
|
||||||
private const LABEL_LIMIT = 80;
|
private const LABEL_LIMIT = 80;
|
||||||
|
|
||||||
|
/** "font-body" for the group name "divi/font-body" – the key "group_presets" of the write tools takes. */
|
||||||
|
public static function groupShortName(string $groupName): string
|
||||||
|
{
|
||||||
|
return str_starts_with($groupName, 'divi/') ? substr($groupName, 5) : $groupName;
|
||||||
|
}
|
||||||
|
|
||||||
public static function label(Block $block): string
|
public static function label(Block $block): string
|
||||||
{
|
{
|
||||||
$attrs = $block->name() === 'divi/global-layout' && is_array($block->attrs()['localAttrs'] ?? null)
|
$attrs = $block->name() === 'divi/global-layout' && is_array($block->attrs()['localAttrs'] ?? null)
|
||||||
|
|||||||
@@ -174,8 +174,7 @@ final class Outline
|
|||||||
{
|
{
|
||||||
$lines = [];
|
$lines = [];
|
||||||
foreach (NodeSummary::groupPresets($block) as $group) {
|
foreach (NodeSummary::groupPresets($block) as $group) {
|
||||||
$name = $group['groupName'];
|
$short = NodeSummary::groupShortName($group['groupName']);
|
||||||
$short = str_starts_with($name, 'divi/') ? substr($name, 5) : $name;
|
|
||||||
$lines[] = $short . ': ' . implode(', ', $this->namesOf($group['presetIds']));
|
$lines[] = $short . ': ' . implode(', ', $this->namesOf($group['presetIds']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ final class ModuleUsageQuery implements UsageQuery
|
|||||||
{
|
{
|
||||||
private const NAME = '/^([a-z][a-z0-9_-]*)\/([a-z][a-z0-9_-]*)$/D';
|
private const NAME = '/^([a-z][a-z0-9_-]*)\/([a-z][a-z0-9_-]*)$/D';
|
||||||
|
|
||||||
private const ESCAPED_LETTER = '\\\\u00[4-7][0-9a-fA-F]';
|
|
||||||
|
|
||||||
/** @throws InvalidArgumentException When $module is no full block name such as "divi/button". */
|
/** @throws InvalidArgumentException When $module is no full block name such as "divi/button". */
|
||||||
public function __construct(private string $module)
|
public function __construct(private string $module)
|
||||||
{
|
{
|
||||||
@@ -39,7 +37,7 @@ final class ModuleUsageQuery implements UsageQuery
|
|||||||
'(post_content LIKE %s OR post_content REGEXP %s OR post_content REGEXP %s)',
|
'(post_content LIKE %s OR post_content REGEXP %s OR post_content REGEXP %s)',
|
||||||
'%' . $db->esc_like($this->module) . '%',
|
'%' . $db->esc_like($this->module) . '%',
|
||||||
$this->escapedSlash(),
|
$this->escapedSlash(),
|
||||||
self::ESCAPED_LETTER
|
self::ESCAPED_LETTER_REGEX
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Design;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
@@ -280,7 +281,7 @@ final class PresetAssignment
|
|||||||
private static function shortNames(array $groups): string
|
private static function shortNames(array $groups): string
|
||||||
{
|
{
|
||||||
$names = array_unique(array_map(
|
$names = array_unique(array_map(
|
||||||
static fn (string $name): string => str_starts_with($name, 'divi/') ? substr($name, 5) : $name,
|
NodeSummary::groupShortName(...),
|
||||||
array_values($groups)
|
array_values($groups)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
final class PresetUsageQuery implements UsageQuery
|
final class PresetUsageQuery implements UsageQuery
|
||||||
{
|
{
|
||||||
private const ESCAPED_LETTER = '\\\\u00[4-7][0-9a-fA-F]';
|
|
||||||
|
|
||||||
/** @var list<array{scope: PresetScope, id: string, default: bool}> */
|
/** @var list<array{scope: PresetScope, id: string, default: bool}> */
|
||||||
private array $targets;
|
private array $targets;
|
||||||
|
|
||||||
@@ -61,7 +59,7 @@ final class PresetUsageQuery implements UsageQuery
|
|||||||
|
|
||||||
return (string) $db->prepare(
|
return (string) $db->prepare(
|
||||||
'(' . str_repeat('post_content LIKE %s OR ', count($likes)) . 'post_content REGEXP %s)',
|
'(' . str_repeat('post_content LIKE %s OR ', count($likes)) . 'post_content REGEXP %s)',
|
||||||
...array_merge($likes, [self::ESCAPED_LETTER])
|
...array_merge($likes, [self::ESCAPED_LETTER_REGEX])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ final class LibraryUsageQuery implements UsageQuery
|
|||||||
'(LOWER(post_content) LIKE %s OR LOWER(post_content) LIKE %s OR post_content REGEXP %s)',
|
'(LOWER(post_content) LIKE %s OR LOWER(post_content) LIKE %s OR post_content REGEXP %s)',
|
||||||
'%' . $db->esc_like('globalmodule') . '%',
|
'%' . $db->esc_like('globalmodule') . '%',
|
||||||
'%' . $db->esc_like('global_module') . '%',
|
'%' . $db->esc_like('global_module') . '%',
|
||||||
str_repeat(chr(92), 2) . 'u00[4-7][0-9a-fA-F]'
|
self::ESCAPED_LETTER_REGEX
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ defined('ABSPATH') || exit;
|
|||||||
*/
|
*/
|
||||||
interface UsageQuery
|
interface UsageQuery
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* MySQL REGEXP for a JSON-escaped letter ("\\u004x" … "\\u007x"): a name or ID written that way is not found by
|
||||||
|
* LIKE, so candidate conditions keep such posts (fail closed, ruling P23).
|
||||||
|
*/
|
||||||
|
public const ESCAPED_LETTER_REGEX = '\\\\u00[4-7][0-9a-fA-F]';
|
||||||
|
|
||||||
/** A condition for "WHERE (…)", already passed through $db->prepare(). */
|
/** A condition for "WHERE (…)", already passed through $db->prepare(). */
|
||||||
public function candidateCondition(\wpdb $db): string;
|
public function candidateCondition(\wpdb $db): string;
|
||||||
|
|
||||||
|
|||||||
@@ -115,9 +115,8 @@ class DiviComposition
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The one PresetAssignment of this composition (D7, rulings R2, R3): the book comes lazily from Divi's stored
|
* The one PresetAssignment of this composition (D7, rulings R2, R3): the book comes lazily from Divi's stored
|
||||||
* presets, ids must pass the choke point unchanged.
|
* presets, ids must pass the choke point unchanged. Public: DiviIntegration::presetAssignment() hands it to
|
||||||
*
|
* the site-wide tools (lit/divi-presets-assign), which need the same instance as the write tools.
|
||||||
* @internal Public for tests; code uses composer() / warnings().
|
|
||||||
*/
|
*/
|
||||||
public function presets(): PresetAssignment
|
public function presets(): PresetAssignment
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -202,10 +202,9 @@ final class DiviNodeGetAbility implements Ability
|
|||||||
{
|
{
|
||||||
$groups = [];
|
$groups = [];
|
||||||
foreach (NodeSummary::groupPresets($block) as $groupId => $group) {
|
foreach (NodeSummary::groupPresets($block) as $groupId => $group) {
|
||||||
$name = $group['groupName'];
|
|
||||||
$groups[] = [
|
$groups[] = [
|
||||||
'group_id' => $groupId,
|
'group_id' => $groupId,
|
||||||
'group' => str_starts_with($name, 'divi/') ? substr($name, 5) : $name,
|
'group' => NodeSummary::groupShortName($group['groupName']),
|
||||||
'presets' => array_map(
|
'presets' => array_map(
|
||||||
static fn (string $presetId): array => ['id' => $presetId, 'name' => $names[$presetId] ?? ''],
|
static fn (string $presetId): array => ['id' => $presetId, 'name' => $names[$presetId] ?? ''],
|
||||||
$group['presetIds']
|
$group['presetIds']
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
|||||||
return 'Ändert vorhandene Divi-Knoten: changes = Liste von {id, set: {Kurzform-Felder}, attrs: {Attributpfad: '
|
return 'Ändert vorhandene Divi-Knoten: changes = Liste von {id, set: {Kurzform-Felder}, attrs: {Attributpfad: '
|
||||||
. '{Breakpoint: Wert}}, preset, group_presets, clear_overrides, label}. null entfernt einen Wert. '
|
. '{Breakpoint: Wert}}, preset, group_presets, clear_overrides, label}. null entfernt einen Wert. '
|
||||||
. 'preset = Name/ID oder Liste (Stapel, bis ' . PresetAssignment::MAX_STACK . '; bei gleicher Priorität '
|
. 'preset = Name/ID oder Liste (Stapel, bis ' . PresetAssignment::MAX_STACK . '; bei gleicher Priorität '
|
||||||
. 'gewinnt das letzte Preset im Stapel); group_presets = {Gruppe: Preset}, z. B. {"font-body": "Fließtext"}. Überdecken lokale Werte '
|
. 'gewinnt das letzte Preset im Stapel); group_presets = {Gruppe: Preset}, z. B. '
|
||||||
|
. '{"font-body": "Fließtext"}. Überdecken lokale Werte '
|
||||||
. 'ein Preset, meldet die Antwort das; clear_overrides: true entfernt sie (nach set/attrs). Nicht '
|
. 'ein Preset, meldet die Antwort das; clear_overrides: true entfernt sie (nach set/attrs). Nicht '
|
||||||
. 'genannte Einstellungen bleiben unverändert. Jeder Knoten nur einmal pro Aufruf; denselben Pfad nicht '
|
. 'genannte Einstellungen bleiben unverändert. Jeder Knoten nur einmal pro Aufruf; denselben Pfad nicht '
|
||||||
. 'zugleich über set und attrs. tree_rev aus lit-divi-outline ist Pflicht; alles oder nichts.';
|
. 'zugleich über set und attrs. tree_rev aus lit-divi-outline ist Pflicht; alles oder nichts.';
|
||||||
|
|||||||
@@ -64,4 +64,11 @@ final class NodeSummaryTest extends TestCase
|
|||||||
self::assertSame([], NodeSummary::groupPresets(Block::create('divi/text', ['groupPreset' => 'x'])));
|
self::assertSame([], NodeSummary::groupPresets(Block::create('divi/text', ['groupPreset' => 'x'])));
|
||||||
self::assertSame([], NodeSummary::groupPresets(Block::create('divi/text', ['groupPreset' => []])));
|
self::assertSame([], NodeSummary::groupPresets(Block::create('divi/text', ['groupPreset' => []])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGroupShortNameDropsTheDiviPrefixOnly(): void
|
||||||
|
{
|
||||||
|
self::assertSame('font-body', NodeSummary::groupShortName('divi/font-body'));
|
||||||
|
self::assertSame('custom/x', NodeSummary::groupShortName('custom/x'));
|
||||||
|
self::assertSame('spacing', NodeSummary::groupShortName('spacing'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,35 @@ final class WarningsTest extends TestCase
|
|||||||
self::assertStringEndsWith('clear_overrides: true entfernt sie.', $covering[0]);
|
self::assertStringEndsWith('clear_overrides: true entfernt sie.', $covering[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A group preset only triggered loading before: its values are overridden by local ones like a module preset's. */
|
||||||
|
public function testWarnsWhenLocalValuesCoverAGroupPreset(): void
|
||||||
|
{
|
||||||
|
$path = 'button.decoration.font.font';
|
||||||
|
$book = PresetBook::fromDivi(['group' => ['divi/font' => ['default' => '', 'items' => [
|
||||||
|
'g-1' => [
|
||||||
|
'type' => 'group', 'id' => 'g-1', 'name' => 'Fließtext groß', 'groupName' => 'divi/font',
|
||||||
|
'groupId' => $path, 'moduleName' => 'divi/button',
|
||||||
|
'attrs' => ['button' => ['decoration' => ['font' => ['font' => [
|
||||||
|
'desktop' => ['value' => ['color' => '#ffffff']],
|
||||||
|
]]]]],
|
||||||
|
],
|
||||||
|
]]]]);
|
||||||
|
$presets = ComposeFixture::presetAssignment(null, null, null, null, static fn (): PresetBook => $book);
|
||||||
|
$button = Block::create('divi/button', [
|
||||||
|
'groupPreset' => [$path => ['presetId' => ['g-1'], 'groupName' => 'divi/font']],
|
||||||
|
'button' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['color' => '#000000']]]]]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
[$covering, $index] = self::collect([$button], [$button], $presets);
|
||||||
|
|
||||||
|
self::assertCount(1, $covering);
|
||||||
|
self::assertStringStartsWith(
|
||||||
|
$index->idOf($button) . ': lokale Werte überdecken „Fließtext groß“: ',
|
||||||
|
$covering[0]
|
||||||
|
);
|
||||||
|
self::assertStringContainsString($path, $covering[0]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testEqualValuesAndUntouchedBlocksDoNotWarn(): void
|
public function testEqualValuesAndUntouchedBlocksDoNotWarn(): void
|
||||||
{
|
{
|
||||||
$loads = 0;
|
$loads = 0;
|
||||||
|
|||||||
@@ -4,12 +4,23 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
|
||||||
|
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ModuleUsageQuery;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
|
||||||
use LIT\MCP\Integrations\Divi\Library\LibraryUsageQuery;
|
use LIT\MCP\Integrations\Divi\Library\LibraryUsageQuery;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\UsageQuery;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
|
||||||
final class LibraryUsageQueryTest extends TestCase
|
final class LibraryUsageQueryTest extends TestCase
|
||||||
{
|
{
|
||||||
|
/** The one fail-closed pattern for JSON-escaped letters (two backslashes, "u00", a letter range). */
|
||||||
|
public function testEscapedLetterPatternIsPinned(): void
|
||||||
|
{
|
||||||
|
self::assertSame(str_repeat(chr(92), 2) . 'u00[4-7][0-9a-fA-F]', UsageQuery::ESCAPED_LETTER_REGEX);
|
||||||
|
self::assertSame(UsageQuery::ESCAPED_LETTER_REGEX, ModuleUsageQuery::ESCAPED_LETTER_REGEX);
|
||||||
|
self::assertSame(UsageQuery::ESCAPED_LETTER_REGEX, PresetUsageQuery::ESCAPED_LETTER_REGEX);
|
||||||
|
}
|
||||||
|
|
||||||
public function testConditionIsTheFormerCandidateFilter(): void
|
public function testConditionIsTheFormerCandidateFilter(): void
|
||||||
{
|
{
|
||||||
$db = Mockery::mock(\wpdb::class);
|
$db = Mockery::mock(\wpdb::class);
|
||||||
|
|||||||
Reference in New Issue
Block a user