Skip to content

Commit cd5f357

Browse files
authored
Merge d0942df into f912e71
2 parents f912e71 + d0942df commit cd5f357

File tree

2 files changed

+179
-0
lines changed

2 files changed

+179
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sabberworm\CSS\Property\Selector;
6+
7+
/**
8+
* Utility class to calculate the specificity of a CSS selector.
9+
*
10+
* The results are cached to avoid recalculating the specificity of the same selector multiple times.
11+
*/
12+
final class SpecificityCalculator
13+
{
14+
/**
15+
* regexp for specificity calculations
16+
*
17+
* @var non-empty-string
18+
*/
19+
private const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
20+
(\\.[\\w]+) # classes
21+
|
22+
\\[(\\w+) # attributes
23+
|
24+
(\\:( # pseudo classes
25+
link|visited|active
26+
|hover|focus
27+
|lang
28+
|target
29+
|enabled|disabled|checked|indeterminate
30+
|root
31+
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
32+
|first-child|last-child|first-of-type|last-of-type
33+
|only-child|only-of-type
34+
|empty|contains
35+
))
36+
/ix';
37+
38+
/**
39+
* regexp for specificity calculations
40+
*
41+
* @var non-empty-string
42+
*/
43+
private const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
44+
((^|[\\s\\+\\>\\~]+)[\\w]+ # elements
45+
|
46+
\\:{1,2}( # pseudo-elements
47+
after|before|first-letter|first-line|selection
48+
))
49+
/ix';
50+
51+
/**
52+
* @var array<string, int<0, max>>
53+
*/
54+
private static $cache = [];
55+
56+
/**
57+
* Calculates the specificity of the given CSS selector.
58+
*
59+
* @return int<0, max>
60+
*
61+
* @internal
62+
*/
63+
public static function calculate(string $selector): int
64+
{
65+
if (!isset(self::$cache[$selector])) {
66+
$a = 0;
67+
/// @todo should exclude \# as well as "#"
68+
$aMatches = null;
69+
$b = \substr_count($selector, '#');
70+
$c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $aMatches);
71+
$d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $aMatches);
72+
self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
73+
}
74+
75+
return self::$cache[$selector];
76+
}
77+
78+
/**
79+
* Clears the cache in order to lower memory usage.
80+
*/
81+
public static function clearCache(): void
82+
{
83+
self::$cache = [];
84+
}
85+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sabberworm\CSS\Tests\Unit\Property\Selector;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
9+
10+
/**
11+
* @covers \Sabberworm\CSS\Property\Selector\SpecificityCalculator
12+
*/
13+
final class SpecificityCalculatorTest extends TestCase
14+
{
15+
protected function tearDown(): void
16+
{
17+
SpecificityCalculator::clearCache();
18+
}
19+
20+
/**
21+
* @return array<string, array{0: non-empty-string, 1: int<0, max>}>
22+
*/
23+
public static function provideSelectorsAndSpecificities(): array
24+
{
25+
return [
26+
'element' => ['a', 1],
27+
'element and descendant with pseudo-selector' => ['ol li::before', 3],
28+
'class' => ['.highlighted', 10],
29+
'element with class' => ['li.green', 11],
30+
'class with pseudo-selector' => ['.help:hover', 20],
31+
'ID' => ['#file', 100],
32+
'ID and descendant class' => ['#test .help', 110],
33+
];
34+
}
35+
36+
/**
37+
* @test
38+
*
39+
* @param non-empty-string $selector
40+
* @param int<0, max> $expectedSpecificity
41+
*
42+
* @dataProvider provideSelectorsAndSpecificities
43+
*/
44+
public function calculateReturnsSpecificityForProvidedSelector(
45+
string $selector,
46+
int $expectedSpecificity
47+
): void {
48+
self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector));
49+
}
50+
51+
/**
52+
* @test
53+
*
54+
* @param non-empty-string $selector
55+
* @param int<0, max> $expectedSpecificity
56+
*
57+
* @dataProvider provideSelectorsAndSpecificities
58+
*/
59+
public function calculateAfterClearingCacheReturnsSpecificityForProvidedSelector(
60+
string $selector,
61+
int $expectedSpecificity
62+
): void {
63+
SpecificityCalculator::clearCache();
64+
65+
self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector));
66+
}
67+
68+
/**
69+
* @test
70+
*/
71+
public function calculateCalledTwoTimesReturnsSameSpecificityForProvidedSelector(): void
72+
{
73+
$selector = '#test .help';
74+
75+
$firstResult = SpecificityCalculator::calculate($selector);
76+
$secondResult = SpecificityCalculator::calculate($selector);
77+
78+
self::assertSame($firstResult, $secondResult);
79+
}
80+
81+
/**
82+
* @test
83+
*/
84+
public function calculateCalledReturnsSameSpecificityForProvidedSelectorBeforeAndAfterClearingCache(): void
85+
{
86+
$selector = '#test .help';
87+
88+
$firstResult = SpecificityCalculator::calculate($selector);
89+
SpecificityCalculator::clearCache();
90+
$secondResult = SpecificityCalculator::calculate($selector);
91+
92+
self::assertSame($firstResult, $secondResult);
93+
}
94+
}

0 commit comments

Comments
 (0)