diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php new file mode 100644 index 00000000..56327c87 --- /dev/null +++ b/src/Property/Selector/SpecificityCalculator.php @@ -0,0 +1,85 @@ +\\~]+)[\\w]+ # elements + | + \\:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; + + /** + * @var array> + */ + private static $cache = []; + + /** + * Calculates the specificity of the given CSS selector. + * + * @return int<0, max> + * + * @internal + */ + public static function calculate(string $selector): int + { + 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::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + + 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 new file mode 100644 index 00000000..088bd517 --- /dev/null +++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php @@ -0,0 +1,94 @@ +}> + */ + 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 calculateReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): 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); + } +}