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

MyIntervals / PHP-CSS-Parser / 21695434793

05 Feb 2026 01:40AM UTC coverage: 72.562% (-0.4%) from 72.97%
21695434793

Pull #1471

github

web-flow
Merge 4e2da6f23 into 2f9902851
Pull Request #1471: [TASK] Use `SelectorComponent` and `Combinator` in `Selector::parse()`

19 of 20 new or added lines in 1 file covered. (95.0%)

1518 of 2092 relevant lines covered (72.56%)

33.77 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\Property;
6

7
use Sabberworm\CSS\Comment\Comment;
8
use Sabberworm\CSS\OutputFormat;
9
use Sabberworm\CSS\Parsing\ParserState;
10
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
11
use Sabberworm\CSS\Property\Selector\Combinator;
12
use Sabberworm\CSS\Property\Selector\CompoundSelector;
13
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
14
use Sabberworm\CSS\Renderable;
15

16
use function Safe\preg_match;
17
use function Safe\preg_replace;
18

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

55
    /**
56
     * @var string
57
     */
58
    private $selector;
59

60
    /**
61
     * @internal since V8.8.0
62
     */
63
    public static function isValid(string $selector): bool
30✔
64
    {
65
        // Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
66
        $numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
30✔
67

68
        return $numberOfMatches === 1;
30✔
69
    }
70

71
    final public function __construct(string $selector)
133✔
72
    {
73
        $this->setSelector($selector);
133✔
74
    }
133✔
75

76
    /**
77
     * @param list<Comment> $comments
78
     *
79
     * @throws UnexpectedTokenException
80
     *
81
     * @internal
82
     */
83
    public static function parse(ParserState $parserState, array &$comments = []): self
102✔
84
    {
85
        // Whitespace is a descendent combinator, not allowed around a compound selector.
86
        // (It is allowed within, e.g. as part of a string or within a function like `:not()`.)
87
        // Gobble it up now to get a clean start.
88
        $parserState->consumeWhiteSpace($comments);
102✔
89

90
        $selectorParts = [];
102✔
91
        while (true) {
102✔
92
            try {
93
                $selectorParts[] = CompoundSelector::parse($parserState, $comments);
102✔
94
            } catch (UnexpectedTokenException $e) {
14✔
95
                if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') {
14✔
96
                    // The whitespace was not a descendent combinator, and was, in fact, arbitrary,
97
                    // after the end of the selector.  Discard it.
98
                    \array_pop($selectorParts);
1✔
99
                    break;
1✔
100
                } else {
101
                    throw $e;
13✔
102
                }
103
            }
104
            try {
105
                $selectorParts[] = Combinator::parse($parserState, $comments);
89✔
106
            } catch (UnexpectedTokenException $e) {
88✔
107
                // End of selector has been reached.
108
                break;
88✔
109
            }
110
        }
111

112
        // Check that the selector has been fully parsed:
113
        if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) {
89✔
114
            throw new UnexpectedTokenException(
1✔
115
                '`,`, `{`, `}` or EOF',
1✔
116
                $parserState->peek(5),
1✔
117
                'literal',
1✔
118
                $parserState->currentLine()
1✔
119
            );
120
        }
121

122
        $selectorString = '';
88✔
123
        foreach ($selectorParts as $selectorPart) {
88✔
124
            $selectorPartValue = $selectorPart->getValue();
88✔
125
            if (\in_array($selectorPartValue, ['>', '+', '~'], true)) {
88✔
NEW
126
                $selectorString .= ' ' . $selectorPartValue . ' ';
×
127
            } else {
128
                $selectorString .= $selectorPartValue;
88✔
129
            }
130
        }
131

132
        return new static($selectorString);
88✔
133
    }
134

135
    public function getSelector(): string
45✔
136
    {
137
        return $this->selector;
45✔
138
    }
139

140
    public function setSelector(string $selector): void
133✔
141
    {
142
        $selector = \trim($selector);
133✔
143

144
        $hasAttribute = \strpos($selector, '[') !== false;
133✔
145

146
        // Whitespace can't be adjusted within an attribute selector, as it would change its meaning
147
        $this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector;
133✔
148
    }
133✔
149

150
    /**
151
     * @return int<0, max>
152
     */
153
    public function getSpecificity(): int
32✔
154
    {
155
        return SpecificityCalculator::calculate($this->selector);
32✔
156
    }
157

158
    public function render(OutputFormat $outputFormat): string
4✔
159
    {
160
        return $this->getSelector();
4✔
161
    }
162

163
    /**
164
     * @return array<string, bool|int|float|string|array<mixed>|null>
165
     *
166
     * @internal
167
     */
168
    public function getArrayRepresentation(): array
2✔
169
    {
170
        throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
2✔
171
    }
172
}
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