From 45cc044645b33dd24adc908be40993efc0620d23 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sat, 1 Mar 2025 22:28:19 +0100 Subject: [PATCH 1/4] [FEATURE] Add a utility class to calculate selector specificity Calculating and caching the specificity of a selector is a different concern than representing a selector, and it deserves to be in its own class. This also helps solve the problem of selectors having keep their specificity cached and in sync with the selector itself. (We'll have a later change that changes `Selector` to use the new class.) --- src/Property/DependencyCalculator.php | 75 +++++++++++++++++++ .../Property/DependencyCalculatorTest.php | 45 +++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/Property/DependencyCalculator.php create mode 100644 tests/Unit/Property/DependencyCalculatorTest.php diff --git a/src/Property/DependencyCalculator.php b/src/Property/DependencyCalculator.php new file mode 100644 index 00000000..8f20d3c6 --- /dev/null +++ b/src/Property/DependencyCalculator.php @@ -0,0 +1,75 @@ +\\~]+)[\\w]+ # elements + | + \\:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; + + /** + * @var array> + */ + private static $specificityCache = []; + + /** + * @return int<0, max> + */ + public static function calculateSpecificity(string $selector): int + { + if (!isset(self::$specificityCache[$selector])) { + $a = 0; + /// @todo should exclude \# as well as "#" + $aMatches = null; + $b = \substr_count($selector, '#'); + $c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $aMatches); + $d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $aMatches); + self::$specificityCache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + + return self::$specificityCache[$selector]; + } +} diff --git a/tests/Unit/Property/DependencyCalculatorTest.php b/tests/Unit/Property/DependencyCalculatorTest.php new file mode 100644 index 00000000..23d81e77 --- /dev/null +++ b/tests/Unit/Property/DependencyCalculatorTest.php @@ -0,0 +1,45 @@ +}> + */ + public static function provideSelectorsAndSpecificities(): array + { + return [ + 'element' => ['a', 1], + 'element and descendant with pseudo-selector' => ['ol li::before', 3], + 'class' => ['.highlighted', 10], + 'element with class' => ['li.green', 11], + 'class with pseudo-selector' => ['.help:hover', 20], + 'ID' => ['#file', 100], + 'ID and descendant class' => ['#test .help', 110], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function calculateSpecificityReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): void { + self::assertSame($expectedSpecificity, DependencyCalculator::calculateSpecificity($selector)); + } +} From 5439d4c1ef5cfd649af39d015891a297ab0482f7 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 2 Mar 2025 15:15:47 +0100 Subject: [PATCH 2/4] Rename class and move it to sub-namespace --- .../SpecificityCalculator.php} | 4 ++-- .../SpecificityCalculatorTest.php} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename src/Property/{DependencyCalculator.php => Selector/SpecificityCalculator.php} (96%) rename tests/Unit/Property/{DependencyCalculatorTest.php => Selector/SpecificityCalculatorTest.php} (74%) diff --git a/src/Property/DependencyCalculator.php b/src/Property/Selector/SpecificityCalculator.php similarity index 96% rename from src/Property/DependencyCalculator.php rename to src/Property/Selector/SpecificityCalculator.php index 8f20d3c6..9d0376df 100644 --- a/src/Property/DependencyCalculator.php +++ b/src/Property/Selector/SpecificityCalculator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sabberworm\CSS\Property; +namespace Sabberworm\CSS\Property\Selector; /** * Utility class to calculate the specificity of a CSS selector. @@ -11,7 +11,7 @@ * * @internal */ -final class DependencyCalculator +final class SpecificityCalculator { /** * regexp for specificity calculations diff --git a/tests/Unit/Property/DependencyCalculatorTest.php b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php similarity index 74% rename from tests/Unit/Property/DependencyCalculatorTest.php rename to tests/Unit/Property/Selector/SpecificityCalculatorTest.php index 23d81e77..c02eeb71 100644 --- a/tests/Unit/Property/DependencyCalculatorTest.php +++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace Sabberworm\CSS\Tests\Unit\Property; +namespace Sabberworm\CSS\Tests\Unit\Property\Selector; use PHPUnit\Framework\TestCase; -use Sabberworm\CSS\Property\DependencyCalculator; +use Sabberworm\CSS\Property\Selector\SpecificityCalculator; /** - * @covers \Sabberworm\CSS\Property\DependencyCalculator + * @covers \Sabberworm\CSS\Property\Selector\SpecificityCalculator */ -final class DependencyCalculatorTest extends TestCase +final class SpecificityCalculatorTest extends TestCase { /** * @return array}> @@ -40,6 +40,6 @@ public function calculateSpecificityReturnsSpecificityForProvidedSelector( string $selector, int $expectedSpecificity ): void { - self::assertSame($expectedSpecificity, DependencyCalculator::calculateSpecificity($selector)); + self::assertSame($expectedSpecificity, SpecificityCalculator::calculateSpecificity($selector)); } } From aaa0dba5cf7d994b908507b36c88113ac8c765d5 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 2 Mar 2025 15:18:08 +0100 Subject: [PATCH 3/4] Shorten some names --- src/Property/Selector/SpecificityCalculator.php | 10 +++++----- .../Property/Selector/SpecificityCalculatorTest.php | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php index 9d0376df..e997f653 100644 --- a/src/Property/Selector/SpecificityCalculator.php +++ b/src/Property/Selector/SpecificityCalculator.php @@ -53,23 +53,23 @@ final class SpecificityCalculator /** * @var array> */ - private static $specificityCache = []; + private static $cache = []; /** * @return int<0, max> */ - public static function calculateSpecificity(string $selector): int + public static function calculate(string $selector): int { - if (!isset(self::$specificityCache[$selector])) { + if (!isset(self::$cache[$selector])) { $a = 0; /// @todo should exclude \# as well as "#" $aMatches = null; $b = \substr_count($selector, '#'); $c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $aMatches); $d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $aMatches); - self::$specificityCache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d; } - return self::$specificityCache[$selector]; + return self::$cache[$selector]; } } diff --git a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php index c02eeb71..b602ed9e 100644 --- a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php +++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php @@ -36,10 +36,10 @@ public static function provideSelectorsAndSpecificities(): array * * @dataProvider provideSelectorsAndSpecificities */ - public function calculateSpecificityReturnsSpecificityForProvidedSelector( + public function calculateReturnsSpecificityForProvidedSelector( string $selector, int $expectedSpecificity ): void { - self::assertSame($expectedSpecificity, SpecificityCalculator::calculateSpecificity($selector)); + self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); } } From d0942df505b53988ee24dadffc2cebda1fe743e3 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 2 Mar 2025 15:22:06 +0100 Subject: [PATCH 4/4] Add cache-clearning functionality --- .../Selector/SpecificityCalculator.php | 14 +++++- .../Selector/SpecificityCalculatorTest.php | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php index e997f653..56327c87 100644 --- a/src/Property/Selector/SpecificityCalculator.php +++ b/src/Property/Selector/SpecificityCalculator.php @@ -8,8 +8,6 @@ * Utility class to calculate the specificity of a CSS selector. * * The results are cached to avoid recalculating the specificity of the same selector multiple times. - * - * @internal */ final class SpecificityCalculator { @@ -56,7 +54,11 @@ final class SpecificityCalculator private static $cache = []; /** + * Calculates the specificity of the given CSS selector. + * * @return int<0, max> + * + * @internal */ public static function calculate(string $selector): int { @@ -72,4 +74,12 @@ public static function calculate(string $selector): int return self::$cache[$selector]; } + + /** + * Clears the cache in order to lower memory usage. + */ + public static function clearCache(): void + { + self::$cache = []; + } } diff --git a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php index b602ed9e..088bd517 100644 --- a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php +++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php @@ -12,6 +12,11 @@ */ final class SpecificityCalculatorTest extends TestCase { + protected function tearDown(): void + { + SpecificityCalculator::clearCache(); + } + /** * @return array}> */ @@ -42,4 +47,48 @@ public function calculateReturnsSpecificityForProvidedSelector( ): void { self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function calculateAfterClearingCacheReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): void { + SpecificityCalculator::clearCache(); + + self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); + } + + /** + * @test + */ + public function calculateCalledTwoTimesReturnsSameSpecificityForProvidedSelector(): void + { + $selector = '#test .help'; + + $firstResult = SpecificityCalculator::calculate($selector); + $secondResult = SpecificityCalculator::calculate($selector); + + self::assertSame($firstResult, $secondResult); + } + + /** + * @test + */ + public function calculateCalledReturnsSameSpecificityForProvidedSelectorBeforeAndAfterClearingCache(): void + { + $selector = '#test .help'; + + $firstResult = SpecificityCalculator::calculate($selector); + SpecificityCalculator::clearCache(); + $secondResult = SpecificityCalculator::calculate($selector); + + self::assertSame($firstResult, $secondResult); + } }