diff --git a/src/Integrations/Divi/Design/PresetUsageMatcher.php b/src/Integrations/Divi/Design/PresetUsageMatcher.php new file mode 100644 index 0000000..1f892cd --- /dev/null +++ b/src/Integrations/Divi/Design/PresetUsageMatcher.php @@ -0,0 +1,87 @@ +.presetId whose groupName is the group; + * - markup the parser rejects counts as a use. + * Pure PHP. + */ +final class PresetUsageMatcher +{ + private const DEFAULT_IDS = ['', 'default', '_initial']; + + public function __construct(private Parser $parser = new Parser()) + { + } + + public function uses(string $content, PresetScope $scope, string $id, bool $isDefault): bool + { + try { + $index = NodeIndex::build($this->parser->parse($content)); + } catch (MalformedMarkup) { + return true; + } + + foreach ($index->entries() as $entry) { + if ($this->usesIn($entry['block'], $scope, $id, $isDefault)) { + return true; + } + } + + return false; + } + + private function usesIn(Block $block, PresetScope $scope, string $id, bool $isDefault): bool + { + if ($scope->kind === PresetScope::MODULE) { + return $this->usesModulePreset($block, $scope, $id, $isDefault); + } + + return $this->usesGroupPreset($block->attrs(), $scope, $id); + } + + private function usesModulePreset(Block $block, PresetScope $scope, string $id, bool $isDefault): bool + { + if ($block->name() !== $scope->name) { + return false; + } + + $stack = NodeSummary::presetIds($block); + if (in_array($id, $stack, true)) { + return true; + } + + return $isDefault && array_diff($stack, self::DEFAULT_IDS) === []; + } + + /** @param array $attrs */ + private function usesGroupPreset(array $attrs, PresetScope $scope, string $id): bool + { + $groups = is_array($attrs['groupPreset'] ?? null) ? $attrs['groupPreset'] : []; + foreach ($groups as $group) { + if (! is_array($group) || ($group['groupName'] ?? null) !== $scope->name) { + continue; + } + if (in_array($id, (array) ($group['presetId'] ?? []), true)) { + return true; + } + } + + return false; + } +} diff --git a/src/Integrations/Divi/Design/PresetUsageQuery.php b/src/Integrations/Divi/Design/PresetUsageQuery.php new file mode 100644 index 0000000..ddac7c5 --- /dev/null +++ b/src/Integrations/Divi/Design/PresetUsageQuery.php @@ -0,0 +1,63 @@ + */ + private array $targets; + + /** + * @param list $targets + * @throws InvalidArgumentException Without targets. + */ + public function __construct(array $targets, private PresetUsageMatcher $matcher = new PresetUsageMatcher()) + { + if ($targets === []) { + throw new InvalidArgumentException('PresetUsageQuery needs at least one preset.'); + } + $this->targets = array_values($targets); + } + + public function candidateCondition(\wpdb $db): string + { + $likes = []; + foreach ($this->targets as $target) { + $likes[] = '%' . $db->esc_like($target['id']) . '%'; + if ($target['default'] && $target['scope']->kind === PresetScope::MODULE) { + $likes[] = '%' . $db->esc_like('wp:' . $target['scope']->name . ' ') . '%'; + } + } + $likes = array_values(array_unique($likes)); + + return (string) $db->prepare( + '(' . str_repeat('post_content LIKE %s OR ', count($likes)) . 'post_content REGEXP %s)', + ...array_merge($likes, [self::ESCAPED_LETTER]) + ); + } + + public function matches(string $content): bool + { + foreach ($this->targets as $target) { + if ($this->matcher->uses($content, $target['scope'], $target['id'], $target['default'])) { + return true; + } + } + + return false; + } +} diff --git a/tests/Unit/Integrations/Divi/Design/PresetUsageMatcherTest.php b/tests/Unit/Integrations/Divi/Design/PresetUsageMatcherTest.php new file mode 100644 index 0000000..a2dddd9 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/PresetUsageMatcherTest.php @@ -0,0 +1,65 @@ +' + . ''; + + private static function page(string $buttonAttrs): string + { + return sprintf(self::BUTTON, $buttonAttrs); + } + + public function testModulePresetInTheStack(): void + { + $matcher = new PresetUsageMatcher(); + $button = new PresetScope('module', 'divi/button'); + + self::assertTrue($matcher->uses(self::page('{"modulePreset":["aaaaaaaaaa"]}'), $button, 'aaaaaaaaaa', false)); + self::assertTrue($matcher->uses(self::page('{"modulePreset":["x","aaaaaaaaaa"]}'), $button, 'aaaaaaaaaa', false)); + self::assertTrue($matcher->uses(self::page('{"modulePreset":"aaaaaaaaaa"}'), $button, 'aaaaaaaaaa', false)); + self::assertFalse($matcher->uses(self::page('{"modulePreset":["bbbbbbbbbb"]}'), $button, 'aaaaaaaaaa', false)); + self::assertFalse( + $matcher->uses(self::page('{"modulePreset":["aaaaaaaaaa"]}'), new PresetScope('module', 'divi/text'), 'aaaaaaaaaa', false), + 'an ID of another module does not count' + ); + } + + public function testDefaultModulePresetReachesModulesWithoutOwnPreset(): void + { + $matcher = new PresetUsageMatcher(); + $button = new PresetScope('module', 'divi/button'); + + self::assertTrue($matcher->uses(self::page('{}'), $button, 'dddddddddd', true)); + self::assertTrue($matcher->uses(self::page('{"modulePreset":["default"]}'), $button, 'dddddddddd', true)); + self::assertFalse($matcher->uses(self::page('{"modulePreset":["aaaaaaaaaa"]}'), $button, 'dddddddddd', true)); + } + + public function testGroupPresetById(): void + { + $matcher = new PresetUsageMatcher(); + $group = new PresetScope('group', 'divi/font-body'); + $attrs = '{"groupPreset":{"content.decoration.bodyFont":{"presetId":["gggggggggg"],"groupName":"divi/font-body"}}}'; + + self::assertTrue($matcher->uses(self::page($attrs), $group, 'gggggggggg', false)); + self::assertFalse($matcher->uses(self::page($attrs), new PresetScope('group', 'divi/spacing'), 'gggggggggg', false)); + } + + public function testBrokenMarkupCountsAsUse(): void + { + self::assertTrue((new PresetUsageMatcher())->uses( + '', + new PresetScope('module', 'divi/button'), + 'aaaaaaaaaa', + false + )); + } +} diff --git a/tests/Unit/Integrations/Divi/Design/PresetUsageQueryTest.php b/tests/Unit/Integrations/Divi/Design/PresetUsageQueryTest.php new file mode 100644 index 0000000..8217a97 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/PresetUsageQueryTest.php @@ -0,0 +1,65 @@ +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)', + '%aaaaaaaaaa%', + '%gggggggggg%', + '\\\\u00[4-7][0-9a-fA-F]' + ) + ->andReturn('(prepared)'); + + $query = new PresetUsageQuery([ + ['scope' => new PresetScope('module', 'divi/button'), 'id' => 'aaaaaaaaaa', 'default' => false], + ['scope' => new PresetScope('group', 'divi/font-body'), 'id' => 'gggggggggg', 'default' => false], + ]); + + self::assertSame('(prepared)', $query->candidateCondition($db)); + self::assertTrue($query->matches('')); + self::assertFalse($query->matches('')); + } + + public function testADefaultModulePresetLooksForTheModule(): 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)', + '%dddddddddd%', + '%wp:divi/button %', + '\\\\u00[4-7][0-9a-fA-F]' + ) + ->andReturn('(prepared)'); + + $query = new PresetUsageQuery([ + ['scope' => new PresetScope('module', 'divi/button'), 'id' => 'dddddddddd', 'default' => true], + ]); + + self::assertSame('(prepared)', $query->candidateCondition($db)); + self::assertTrue($query->matches('')); + } + + public function testNeedsATarget(): void + { + $this->expectException(\InvalidArgumentException::class); + + new PresetUsageQuery([]); + } +}