• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MyIntervals / PHP-CSS-Parser / 20517081930

26 Dec 2025 05:59AM UTC coverage: 68.833% (-0.4%) from 69.191%
20517081930

Pull #1446

github

web-flow
Merge 96889c35d into 0ae1fde22
Pull Request #1446: [FEATURE] Render selector

12 of 27 new or added lines in 1 file covered. (44.44%)

1345 of 1954 relevant lines covered (68.83%)

32.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.12
/src/Property/Selector.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\Property;
6

7
use Sabberworm\CSS\OutputFormat;
8
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
9
use Sabberworm\CSS\Renderable;
10

11
use function Safe\preg_match;
12
use function Safe\preg_replace;
13

14
/**
15
 * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
16
 * class.
17
 */
18
class Selector implements Renderable
19
{
20
    /**
21
     * @internal since 8.5.2
22
     */
23
    public const SELECTOR_VALIDATION_RX = '/
24
        ^(
25
            (?:
26
                # any sequence of valid unescaped characters, except quotes
27
                [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
28
                |
29
                # one or more escaped characters
30
                (?:\\\\.)++
31
                |
32
                # quoted text, like in `[id="example"]`
33
                (?:
34
                    # opening quote
35
                    ([\'"])
36
                    (?:
37
                        # sequence of characters except closing quote or backslash
38
                        (?:(?!\\g{-1}|\\\\).)++
39
                        |
40
                        # one or more escaped characters
41
                        (?:\\\\.)++
42
                    )*+ # zero or more times
43
                    # closing quote or end (unmatched quote is currently allowed)
44
                    (?:\\g{-1}|$)
45
                )
46
            )*+ # zero or more times
47
        )$
48
        /ux';
49

50
    /**
51
     * @var string
52
     */
53
    private $selector;
54

55
    /**
56
     * @internal since V8.8.0
57
     */
58
    public static function isValid(string $selector): bool
12✔
59
    {
60
        // Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
61
        $numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
12✔
62

63
        return $numberOfMatches === 1;
12✔
64
    }
65

66
    public function __construct(string $selector)
29✔
67
    {
68
        $this->setSelector($selector);
29✔
69
    }
29✔
70

71
    public function getSelector(): string
10✔
72
    {
73
        return $this->selector;
10✔
74
    }
75

76
    public function setSelector(string $selector): void
29✔
77
    {
78
        $selector = \trim($selector);
29✔
79

80
        $hasAttribute = \strpos($selector, '[') !== false;
29✔
81

82
        // Whitespace can't be adjusted within an attribute selector, as it would change its meaning
83
        $this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector;
29✔
84
    }
29✔
85

86
    /**
87
     * @return int<0, max>
88
     */
89
    public function getSpecificity(): int
18✔
90
    {
91
        return SpecificityCalculator::calculate($this->selector);
18✔
92
    }
93

94
    public function render(OutputFormat $outputFormat): string
4✔
95
    {
96
        $tidy = [
97
            '>',
4✔
98
            '~',
99
            '+',
100
        ];
101

102
        $selector = $this->getSelector();
4✔
103
        $attribute = false;
4✔
104
        $output = '';
4✔
105

106
        for ($i = 0; $i < \strlen($selector); ++$i) {
4✔
107
            $current = $selector[$i];
4✔
108

109
            if ($current === '[') {
4✔
NEW
110
                $attribute = true;
×
NEW
111
                $output .= $current;
×
NEW
112
                $previous = $current;
×
NEW
113
                continue;
×
114
            }
115

116
            if ($attribute) {
4✔
NEW
117
                if ($current === ']') {
×
NEW
118
                    $attribute = false;
×
119
                }
120

NEW
121
                $output .= $current;
×
NEW
122
                $previous = $current;
×
NEW
123
                continue;
×
124
            }
125

126
            if (\in_array($current, $tidy, true)) {
4✔
NEW
127
                if ($previous !== ' ') {
×
NEW
128
                    $output .= ' ';
×
129
                }
130

NEW
131
                $output .= $current;
×
132

NEW
133
                if ($selector[$i +1] !== ' ') {
×
NEW
134
                    $output .= ' ';
×
135
                }
136

NEW
137
                $current = ' ';
×
138
            } else {
139
                $output .= $current;
4✔
140
            }
141

142
            $previous = $current;
4✔
143
        }
144

145
        return $output;
4✔
146
    }
147
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc