feat: Fundstellen aller Module eines Typs, Standard-Gruppen-Presets in der Fundstellensuche (ModuleUsageQuery)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
1f9b50f1a6
commit
763d765660
@@ -127,7 +127,10 @@ final class DiviPresetGetAbility implements Ability
|
|||||||
$attrs = is_array($item['attrs'] ?? null) ? $item['attrs'] : [];
|
$attrs = is_array($item['attrs'] ?? null) ? $item['attrs'] : [];
|
||||||
$module = is_string($item['moduleName'] ?? null) ? $item['moduleName'] : $scope->name;
|
$module = is_string($item['moduleName'] ?? null) ? $item['moduleName'] : $scope->name;
|
||||||
$usage = $this->usages->reportFor(
|
$usage = $this->usages->reportFor(
|
||||||
new PresetUsageQuery([['scope' => $scope, 'id' => $id, 'default' => $isDefault]]),
|
PresetUsageQuery::withGroups(
|
||||||
|
[['scope' => $scope, 'id' => $id, 'default' => $isDefault]],
|
||||||
|
$this->writer->groupsOf(...)
|
||||||
|
),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ defined('ABSPATH') || exit;
|
|||||||
/**
|
/**
|
||||||
* lit/divi-presets-delete (spec §6.4, G5): deletes presets nothing uses any more – no page, library entry or Theme
|
* lit/divi-presets-delete (spec §6.4, G5): deletes presets nothing uses any more – no page, library entry or Theme
|
||||||
* Builder layout (GlobalUsages with PresetUsageQuery), and no other preset that nests it. A used preset is refused
|
* Builder layout (GlobalUsages with PresetUsageQuery), and no other preset that nests it. A used preset is refused
|
||||||
* with its usages (in_use); lit/divi-design-detach (Etappe 4b) removes the references first. Only explicit references
|
* with its usages (in_use); lit/divi-design-detach removes the references first. Only explicit references
|
||||||
* block: a default preset loses its default, and the posts with modules that use it implicitly are listed in the dry
|
* block: a default preset loses its default, and the posts with modules that use it implicitly are listed in the dry
|
||||||
* run (spec §6.4). Through the change pipeline with Divi's delete semantics (legacy option cleaned).
|
* run (spec §6.4). Through the change pipeline with Divi's delete semantics (legacy option cleaned).
|
||||||
*/
|
*/
|
||||||
@@ -25,8 +25,8 @@ final class DiviPresetsDeleteAbility implements Ability
|
|||||||
{
|
{
|
||||||
public const MAX_ITEMS = 20;
|
public const MAX_ITEMS = 20;
|
||||||
|
|
||||||
private const DETACH_HINT = 'Erst die Verweise entfernen: lit/divi-design-detach (ab Etappe 4b) oder im '
|
private const DETACH_HINT = 'Erst die Verweise entfernen: lit/divi-design-detach oder im Divi-Builder; '
|
||||||
. 'Divi-Builder; danach erneut löschen.';
|
. 'danach erneut löschen.';
|
||||||
|
|
||||||
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
|
/** @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights). */
|
||||||
private const DEFAULT_WARNING = 'Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.';
|
private const DEFAULT_WARNING = 'Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.';
|
||||||
@@ -179,12 +179,13 @@ final class DiviPresetsDeleteAbility implements Ability
|
|||||||
$diff = PresetDiff::between($before, $book);
|
$diff = PresetDiff::between($before, $book);
|
||||||
|
|
||||||
$defaults = array_values(array_filter($targets, static fn (array $target): bool => $target['default']));
|
$defaults = array_values(array_filter($targets, static fn (array $target): bool => $target['default']));
|
||||||
|
$groupsOf = $this->writer->groupsOf(...);
|
||||||
|
|
||||||
return new DesignPlan(
|
return new DesignPlan(
|
||||||
$book->data(),
|
$book->data(),
|
||||||
$diff,
|
$diff,
|
||||||
PresetDiff::summary($diff),
|
PresetDiff::summary($diff),
|
||||||
$defaults === [] ? null : $this->usages->reportFor(new PresetUsageQuery($defaults)),
|
$defaults === [] ? null : $this->usages->reportFor(PresetUsageQuery::withGroups($defaults, $groupsOf)),
|
||||||
$defaults === [] ? [] : [self::DEFAULT_WARNING],
|
$defaults === [] ? [] : [self::DEFAULT_WARNING],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ defined('ABSPATH') || exit;
|
|||||||
* lit/divi-presets-set (spec §6.3, G7): creates and changes module and option group presets – from the short and full
|
* lit/divi-presets-set (spec §6.3, G7): creates and changes module and option group presets – from the short and full
|
||||||
* form, from a module of a page (from_node), with nested group presets and as the default preset – through the
|
* form, from a module of a page (from_node), with nested group presets and as the default preset – through the
|
||||||
* change pipeline (dry run, design_rev, snapshot, Divi's sync, change log). New names must be free within the
|
* change pipeline (dry run, design_rev, snapshot, Divi's sync, change log). New names must be free within the
|
||||||
* module/group (A5). The dry run lists the posts that use a changed preset or reach a new default.
|
* module/group (A5). The dry run lists the posts that use a changed preset or reach a new default, also implicitly
|
||||||
|
* (ruling R11).
|
||||||
*/
|
*/
|
||||||
final class DiviPresetsSetAbility implements Ability
|
final class DiviPresetsSetAbility implements Ability
|
||||||
{
|
{
|
||||||
@@ -33,9 +34,6 @@ final class DiviPresetsSetAbility implements Ability
|
|||||||
private const MARKUP = 'Vorhandene Presets enthalten HTML/CSS, das beim Speichern ohne das Recht unfiltered_html '
|
private const MARKUP = 'Vorhandene Presets enthalten HTML/CSS, das beim Speichern ohne das Recht unfiltered_html '
|
||||||
. 'verloren ginge; nichts wurde gespeichert.';
|
. 'verloren ginge; nichts wurde gespeichert.';
|
||||||
|
|
||||||
private const GROUP_DEFAULT_WARNING = 'Standard-Gruppen-Presets wirken zusätzlich auf alle Module mit dieser '
|
|
||||||
. 'Gruppe ohne eigenes Gruppen-Preset; diese Seiten sind nicht aufgelistet.';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Closure(): PresetComposer $composer
|
* @param Closure(): PresetComposer $composer
|
||||||
* @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights).
|
* @param (Closure(): bool)|null $unfiltered Whether the user may save unfiltered HTML (DesignRights).
|
||||||
@@ -159,7 +157,7 @@ final class DiviPresetsSetAbility implements Ability
|
|||||||
$invalid = [];
|
$invalid = [];
|
||||||
$targets = [];
|
$targets = [];
|
||||||
$created = [];
|
$created = [];
|
||||||
$warnings = [];
|
$groupsOf = $this->writer->groupsOf(...);
|
||||||
foreach ($items as $index => $item) {
|
foreach ($items as $index => $item) {
|
||||||
$field = sprintf('items[%d]', $index);
|
$field = sprintf('items[%d]', $index);
|
||||||
if (! is_array($item)) {
|
if (! is_array($item)) {
|
||||||
@@ -184,9 +182,6 @@ final class DiviPresetsSetAbility implements Ability
|
|||||||
$book = $book->with($scope, $result['item']);
|
$book = $book->with($scope, $result['item']);
|
||||||
if (($item['make_default'] ?? null) === true) {
|
if (($item['make_default'] ?? null) === true) {
|
||||||
$book = $book->withDefault($scope, $id);
|
$book = $book->withDefault($scope, $id);
|
||||||
if ($scope->kind === PresetScope::GROUP) {
|
|
||||||
$warnings[] = self::GROUP_DEFAULT_WARNING;
|
|
||||||
}
|
|
||||||
} elseif (($item['make_default'] ?? null) === false && $book->defaultId($scope) === $id) {
|
} elseif (($item['make_default'] ?? null) === false && $book->defaultId($scope) === $id) {
|
||||||
$book = $book->withDefault($scope, '');
|
$book = $book->withDefault($scope, '');
|
||||||
}
|
}
|
||||||
@@ -214,8 +209,7 @@ final class DiviPresetsSetAbility implements Ability
|
|||||||
$book->data(),
|
$book->data(),
|
||||||
$diff,
|
$diff,
|
||||||
PresetDiff::summary($diff),
|
PresetDiff::summary($diff),
|
||||||
$targets === [] ? null : $this->usages->reportFor(new PresetUsageQuery($targets)),
|
$targets === [] ? null : $this->usages->reportFor(PresetUsageQuery::withGroups($targets, $groupsOf))
|
||||||
array_values(array_unique($warnings))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||||
|
use LIT\MCP\Integrations\Divi\Library\UsageQuery;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The posts with a module of one type (GlobalUsages::reportFor(), Etappe 4b): candidates contain the block name, an
|
||||||
|
* escaped slash or a JSON escape of a letter (a global instance keeps its type as "blockName" in JSON, fail closed
|
||||||
|
* like ruling P23); matches() parses the content. Global instances of the type (opaque, never rewritten) count, so
|
||||||
|
* the page rewriter reports them as skipped; so does markup the parser rejects.
|
||||||
|
*/
|
||||||
|
final class ModuleUsageQuery implements UsageQuery
|
||||||
|
{
|
||||||
|
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". */
|
||||||
|
public function __construct(private string $module)
|
||||||
|
{
|
||||||
|
if (preg_match(self::NAME, $module) !== 1) {
|
||||||
|
throw new InvalidArgumentException('ModuleUsageQuery needs a full block name such as divi/button.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function candidateCondition(\wpdb $db): string
|
||||||
|
{
|
||||||
|
return (string) $db->prepare(
|
||||||
|
'(post_content LIKE %s OR post_content REGEXP %s OR post_content REGEXP %s)',
|
||||||
|
'%' . $db->esc_like($this->module) . '%',
|
||||||
|
$this->escapedSlash(),
|
||||||
|
self::ESCAPED_LETTER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function matches(string $content): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$index = NodeIndex::build((new Parser())->parse($content));
|
||||||
|
} catch (MalformedMarkup) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($index->entries() as $entry) {
|
||||||
|
if (GlobalInstance::effectiveName($entry['block']) === $this->module) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Regular expression for "divi\/button" and "divi/button" (two literal backslashes in the pattern). */
|
||||||
|
private function escapedSlash(): string
|
||||||
|
{
|
||||||
|
[$namespace, $name] = explode('/', $this->module, 2);
|
||||||
|
|
||||||
|
return $namespace . '(\\\\/|\\\\u002[fF])' . $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi\Design;
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
@@ -17,15 +18,20 @@ defined('ABSPATH') || exit;
|
|||||||
* - module preset $id: a block of the module with $id in its modulePreset stack (array or legacy string);
|
* - module preset $id: a block of the module with $id in its modulePreset stack (array or legacy string);
|
||||||
* the default preset additionally reaches every block of the module without an own preset ("" / "default" /
|
* the default preset additionally reaches every block of the module without an own preset ("" / "default" /
|
||||||
* "_initial" mean the default, GlobalPreset::is_preset_id_as_default);
|
* "_initial" mean the default, GlobalPreset::is_preset_id_as_default);
|
||||||
* - group preset $id: a block with $id in groupPreset.<attr>.presetId whose groupName is the group;
|
* - group preset $id: a block with $id in groupPreset.<attr>.presetId whose groupName is the group; the default
|
||||||
|
* group preset additionally reaches every block whose module has the group (groupsOf) and that has no own group
|
||||||
|
* preset for it (ruling R11) – without $groupsOf only the explicit references are found;
|
||||||
* - markup the parser rejects counts as a use.
|
* - markup the parser rejects counts as a use.
|
||||||
|
* Global instances (opaque) get their content from the library entry, so they never reach a default preset.
|
||||||
* Pure PHP.
|
* Pure PHP.
|
||||||
*/
|
*/
|
||||||
final class PresetUsageMatcher
|
final class PresetUsageMatcher
|
||||||
{
|
{
|
||||||
private const DEFAULT_IDS = ['', 'default', '_initial'];
|
/** Preset IDs that mean "no own preset" (GlobalPreset::is_preset_id_as_default). */
|
||||||
|
public const DEFAULT_IDS = ['', 'default', '_initial'];
|
||||||
|
|
||||||
public function __construct(private Parser $parser = new Parser())
|
/** @param (Closure(string $module): array<string, string>)|null $groupsOf groupId => groupName of a module */
|
||||||
|
public function __construct(private Parser $parser = new Parser(), private ?Closure $groupsOf = null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +43,9 @@ final class PresetUsageMatcher
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
foreach ($index->entries() as $entry) {
|
foreach ($index->entries() as $entry) {
|
||||||
if ($this->usesIn($entry['block'], $scope, $id, $isDefault)) {
|
if ($this->usesIn($entry['block'], $scope, $id, $isDefault, $groups)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,13 +53,15 @@ final class PresetUsageMatcher
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function usesIn(Block $block, PresetScope $scope, string $id, bool $isDefault): bool
|
/** @param array<string, array<string, string>> $groups groupsOf() per module name, filled on demand */
|
||||||
|
private function usesIn(Block $block, PresetScope $scope, string $id, bool $isDefault, array &$groups): bool
|
||||||
{
|
{
|
||||||
if ($scope->kind === PresetScope::MODULE) {
|
if ($scope->kind === PresetScope::MODULE) {
|
||||||
return $this->usesModulePreset($block, $scope, $id, $isDefault);
|
return $this->usesModulePreset($block, $scope, $id, $isDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->usesGroupPreset($block->attrs(), $scope, $id);
|
return $this->usesGroupPreset($block->attrs(), $scope, $id)
|
||||||
|
|| ($isDefault && $this->reachesByDefault($block, $scope, $groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function usesModulePreset(Block $block, PresetScope $scope, string $id, bool $isDefault): bool
|
private function usesModulePreset(Block $block, PresetScope $scope, string $id, bool $isDefault): bool
|
||||||
@@ -84,4 +93,37 @@ final class PresetUsageMatcher
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruling R11: the module of $block has the group $scope and $block has no own group preset for its groupId.
|
||||||
|
*
|
||||||
|
* @param array<string, array<string, string>> $groups
|
||||||
|
*/
|
||||||
|
private function reachesByDefault(Block $block, PresetScope $scope, array &$groups): bool
|
||||||
|
{
|
||||||
|
if ($this->groupsOf === null || ! $block->isDivi() || $block->isOpaque()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[$block->name()] ??= ($this->groupsOf)($block->name());
|
||||||
|
$own = is_array($block->attrs()['groupPreset'] ?? null) ? $block->attrs()['groupPreset'] : [];
|
||||||
|
foreach ($groups[$block->name()] as $groupId => $groupName) {
|
||||||
|
if ($groupName === $scope->name && ! self::hasOwnPreset($own[$groupId] ?? null)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function hasOwnPreset(mixed $entry): bool
|
||||||
|
{
|
||||||
|
if (! is_array($entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_filter((array) ($entry['presetId'] ?? []), 'is_string');
|
||||||
|
|
||||||
|
return array_diff($ids, self::DEFAULT_IDS) !== [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace LIT\MCP\Integrations\Divi\Design;
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use LIT\MCP\Integrations\Divi\Library\UsageQuery;
|
use LIT\MCP\Integrations\Divi\Library\UsageQuery;
|
||||||
|
|
||||||
@@ -11,8 +12,9 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The posts that use any of the presets $targets (GlobalUsages::reportFor()): candidates contain one of the IDs, a
|
* The posts that use any of the presets $targets (GlobalUsages::reportFor()): candidates contain one of the IDs, a
|
||||||
* JSON escape of a letter (an escaped ID or key, fail closed like ruling P23) or – for a default module preset – a
|
* JSON escape of a letter (an escaped ID or key, fail closed like ruling P23), for a default module preset a block
|
||||||
* block of the module; PresetUsageMatcher decides per target.
|
* of the module and for a default group preset any Divi block (ruling R11: which modules have the group is only
|
||||||
|
* known to PresetUsageMatcher::$groupsOf); PresetUsageMatcher decides per target.
|
||||||
*/
|
*/
|
||||||
final class PresetUsageQuery implements UsageQuery
|
final class PresetUsageQuery implements UsageQuery
|
||||||
{
|
{
|
||||||
@@ -33,6 +35,17 @@ final class PresetUsageQuery implements UsageQuery
|
|||||||
$this->targets = array_values($targets);
|
$this->targets = array_values($targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets whose default group presets are also found where a module reaches them implicitly.
|
||||||
|
*
|
||||||
|
* @param list<array{scope: PresetScope, id: string, default: bool}> $targets
|
||||||
|
* @param Closure(string $module): array<string, string> $groupsOf groupId => groupName of a module
|
||||||
|
*/
|
||||||
|
public static function withGroups(array $targets, Closure $groupsOf): self
|
||||||
|
{
|
||||||
|
return new self($targets, new PresetUsageMatcher(groupsOf: $groupsOf));
|
||||||
|
}
|
||||||
|
|
||||||
public function candidateCondition(\wpdb $db): string
|
public function candidateCondition(\wpdb $db): string
|
||||||
{
|
{
|
||||||
$likes = [];
|
$likes = [];
|
||||||
@@ -40,6 +53,8 @@ final class PresetUsageQuery implements UsageQuery
|
|||||||
$likes[] = '%' . $db->esc_like($target['id']) . '%';
|
$likes[] = '%' . $db->esc_like($target['id']) . '%';
|
||||||
if ($target['default'] && $target['scope']->kind === PresetScope::MODULE) {
|
if ($target['default'] && $target['scope']->kind === PresetScope::MODULE) {
|
||||||
$likes[] = '%' . $db->esc_like('wp:' . $target['scope']->name . ' ') . '%';
|
$likes[] = '%' . $db->esc_like('wp:' . $target['scope']->name . ' ') . '%';
|
||||||
|
} elseif ($target['default']) {
|
||||||
|
$likes[] = '%' . $db->esc_like('wp:divi/') . '%';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$likes = array_values(array_unique($likes));
|
$likes = array_values(array_unique($likes));
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ final class DiviPresetsDeleteAbilityTest extends TestCase
|
|||||||
|
|
||||||
self::assertSame(3, $error['usages'][0]['post_id']);
|
self::assertSame(3, $error['usages'][0]['post_id']);
|
||||||
self::assertStringContainsString('lit/divi-design-detach', $error['hint']);
|
self::assertStringContainsString('lit/divi-design-detach', $error['hint']);
|
||||||
|
self::assertStringNotContainsString('4b', $error['hint'], 'the tool exists now');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNestedUseInAnotherPresetCounts(): void
|
public function testNestedUseInAnotherPresetCounts(): void
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ final class DiviPresetsSetAbilityTest extends TestCase
|
|||||||
self::assertSame(['changed', 'default_set'], array_column($dry['diff'], 'action'));
|
self::assertSame(['changed', 'default_set'], array_column($dry['diff'], 'action'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** R11: a default group preset no longer warns that pages are "not listed" – the dry run lists them. */
|
||||||
|
public function testDefaultGroupPresetHasNoNotListedWarning(): void
|
||||||
|
{
|
||||||
|
$dry = self::assertToolError($this->ability()->execute(['items' => [[
|
||||||
|
'group' => 'font-body', 'preset' => 'Lesetext', 'rename' => 'Lesetext neu', 'make_default' => true,
|
||||||
|
]]]), 'confirm_required');
|
||||||
|
|
||||||
|
self::assertSame([], $dry['warnings']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testUnknownPresetAndBadItemsAreCollected(): void
|
public function testUnknownPresetAndBadItemsAreCollected(): void
|
||||||
{
|
{
|
||||||
$result = self::assertToolError($this->ability()->execute(['items' => [
|
$result = self::assertToolError($this->ability()->execute(['items' => [
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ModuleUsageQuery;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class ModuleUsageQueryTest extends TestCase
|
||||||
|
{
|
||||||
|
private static function page(string $inner): string
|
||||||
|
{
|
||||||
|
return '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->' . $inner
|
||||||
|
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCandidatesContainTheNameAnEscapedSlashOrAnEscapedLetter(): void
|
||||||
|
{
|
||||||
|
$db = Mockery::mock(\wpdb::class);
|
||||||
|
$db->allows('esc_like')->andReturnUsing(static fn (string $text): string => $text);
|
||||||
|
$db->expects('prepare')
|
||||||
|
->with(
|
||||||
|
'(post_content LIKE %s OR post_content REGEXP %s OR post_content REGEXP %s)',
|
||||||
|
'%divi/button%',
|
||||||
|
'divi(\\\\/|\\\\u002[fF])button',
|
||||||
|
'\\\\u00[4-7][0-9a-fA-F]'
|
||||||
|
)
|
||||||
|
->andReturn('(prepared)');
|
||||||
|
|
||||||
|
self::assertSame('(prepared)', (new ModuleUsageQuery('divi/button'))->candidateCondition($db));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNeedsAFullBlockName(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
new ModuleUsageQuery('button');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesBlocksOfTheModule(): void
|
||||||
|
{
|
||||||
|
$query = new ModuleUsageQuery('divi/button');
|
||||||
|
|
||||||
|
self::assertTrue($query->matches(self::page('<!-- wp:divi/button {"a":1} /-->')));
|
||||||
|
self::assertTrue($query->matches(self::page('<!-- wp:divi/button /-->')));
|
||||||
|
self::assertTrue($query->matches(self::page(
|
||||||
|
'<!-- wp:divi/button {"a":1} --><!-- wp:divi/text /--><!-- /wp:divi/button -->'
|
||||||
|
)));
|
||||||
|
self::assertFalse($query->matches(self::page('<!-- wp:divi/text {"a":1} /-->')));
|
||||||
|
self::assertFalse($query->matches(self::page('<!-- wp:divi/button-group /-->')), 'another module name');
|
||||||
|
self::assertFalse($query->matches('<p>Kein Divi-Inhalt</p>'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGlobalInstancesOfTheModuleCountSoThatTheyAreReportedAsSkipped(): void
|
||||||
|
{
|
||||||
|
$query = new ModuleUsageQuery('divi/button');
|
||||||
|
|
||||||
|
self::assertTrue($query->matches(self::page(
|
||||||
|
'<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/button"} /-->'
|
||||||
|
)));
|
||||||
|
self::assertTrue($query->matches(self::page(
|
||||||
|
'<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi\/button"} /-->'
|
||||||
|
)), 'an escaped slash decodes to the same name');
|
||||||
|
self::assertFalse($query->matches(self::page(
|
||||||
|
'<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/text"} /-->'
|
||||||
|
)));
|
||||||
|
self::assertTrue($query->matches(self::page(
|
||||||
|
'<!-- wp:divi/button {"globalModule":"1058"} /-->'
|
||||||
|
)), 'Divi 4 conversion: the block itself carries globalModule');
|
||||||
|
self::assertTrue($query->matches(self::page('<!-- wp:divi/button {"a":1,} /-->')), 'unreadable attributes');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBrokenMarkupCountsAsUse(): void
|
||||||
|
{
|
||||||
|
self::assertTrue((new ModuleUsageQuery('divi/button'))->matches(
|
||||||
|
'<!-- wp:divi/section {"a": --><!-- /wp:divi/row -->'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
|
|
||||||
final class PresetUsageMatcherTest extends TestCase
|
final class PresetUsageMatcherTest extends TestCase
|
||||||
{
|
{
|
||||||
|
private const TEXT = '<!-- wp:divi/section --><!-- wp:divi/text %s /--><!-- /wp:divi/section -->';
|
||||||
private const BUTTON = '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
|
private const BUTTON = '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
|
||||||
. '<!-- wp:divi/button %s /--><!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->';
|
. '<!-- wp:divi/button %s /--><!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->';
|
||||||
|
|
||||||
@@ -62,4 +63,58 @@ final class PresetUsageMatcherTest extends TestCase
|
|||||||
false
|
false
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function groupsOf(string $module): array
|
||||||
|
{
|
||||||
|
return $module === 'divi/text' ? ['content.decoration.bodyFont' => 'divi/font-body'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultGroupPresetReachesModulesWithTheGroup(): void
|
||||||
|
{
|
||||||
|
$matcher = new PresetUsageMatcher(groupsOf: self::groupsOf(...));
|
||||||
|
$group = new PresetScope('group', 'divi/font-body');
|
||||||
|
$own = '{"groupPreset":{"content.decoration.bodyFont":{"presetId":["%s"],"groupName":"divi/font-body"}}}';
|
||||||
|
|
||||||
|
self::assertTrue($matcher->uses(sprintf(self::TEXT, '{}'), $group, 'gggggggggg', true));
|
||||||
|
self::assertTrue(
|
||||||
|
$matcher->uses(sprintf(self::TEXT, sprintf($own, 'default')), $group, 'gggggggggg', true),
|
||||||
|
'an entry with the default ID is no own preset'
|
||||||
|
);
|
||||||
|
self::assertFalse(
|
||||||
|
$matcher->uses(sprintf(self::TEXT, sprintf($own, 'hhhhhhhhhh')), $group, 'gggggggggg', true),
|
||||||
|
'an own group preset replaces the default'
|
||||||
|
);
|
||||||
|
self::assertFalse(
|
||||||
|
$matcher->uses(self::page('{}'), $group, 'gggggggggg', true),
|
||||||
|
'a button has no font-body group'
|
||||||
|
);
|
||||||
|
self::assertFalse(
|
||||||
|
$matcher->uses(sprintf(self::TEXT, '{}'), new PresetScope('group', 'divi/spacing'), 'gggggggggg', true),
|
||||||
|
'the text module has no spacing group in this fixture'
|
||||||
|
);
|
||||||
|
self::assertFalse(
|
||||||
|
$matcher->uses(sprintf(self::TEXT, '{}'), $group, 'gggggggggg', false),
|
||||||
|
'a preset that is not the default needs an explicit entry'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultGroupPresetIsNotDetectedWithoutGroupsOf(): void
|
||||||
|
{
|
||||||
|
self::assertFalse((new PresetUsageMatcher())->uses(
|
||||||
|
sprintf(self::TEXT, '{}'),
|
||||||
|
new PresetScope('group', 'divi/font-body'),
|
||||||
|
'gggggggggg',
|
||||||
|
true
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGlobalInstancesDoNotReachADefaultGroupPreset(): void
|
||||||
|
{
|
||||||
|
$matcher = new PresetUsageMatcher(groupsOf: static fn (string $module): array => [
|
||||||
|
'content.decoration.bodyFont' => 'divi/font-body',
|
||||||
|
]);
|
||||||
|
$instance = '<!-- wp:divi/global-layout {"globalModule":"1058","blockName":"divi/text"} /-->';
|
||||||
|
|
||||||
|
self::assertFalse($matcher->uses($instance, new PresetScope('group', 'divi/font-body'), 'gggggggggg', true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,37 @@ final class PresetUsageQueryTest extends TestCase
|
|||||||
|
|
||||||
new PresetUsageQuery([]);
|
new PresetUsageQuery([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testADefaultGroupPresetLooksForEveryDiviBlock(): void
|
||||||
|
{
|
||||||
|
$db = Mockery::mock(\wpdb::class);
|
||||||
|
$db->allows('esc_like')->andReturnUsing(static fn (string $text): string => $text);
|
||||||
|
$db->expects('prepare')
|
||||||
|
->with(
|
||||||
|
'(post_content LIKE %s OR post_content LIKE %s OR post_content REGEXP %s)',
|
||||||
|
'%gggggggggg%',
|
||||||
|
'%wp:divi/%',
|
||||||
|
'\\\\u00[4-7][0-9a-fA-F]'
|
||||||
|
)
|
||||||
|
->andReturn('(prepared)');
|
||||||
|
|
||||||
|
$query = new PresetUsageQuery([
|
||||||
|
['scope' => new PresetScope('group', 'divi/font-body'), 'id' => 'gggggggggg', 'default' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame('(prepared)', $query->candidateCondition($db));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithGroupsDetectsTheImplicitUseOfADefaultGroupPreset(): void
|
||||||
|
{
|
||||||
|
$query = PresetUsageQuery::withGroups(
|
||||||
|
[['scope' => new PresetScope('group', 'divi/font-body'), 'id' => 'gggggggggg', 'default' => true]],
|
||||||
|
static fn (string $module): array => $module === 'divi/text'
|
||||||
|
? ['content.decoration.bodyFont' => 'divi/font-body']
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertTrue($query->matches('<!-- wp:divi/text {} /-->'));
|
||||||
|
self::assertFalse($query->matches('<!-- wp:divi/button {} /-->'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user