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

MyIntervals / PHP-CSS-Parser / 21341708609

25 Jan 2026 11:47PM UTC coverage: 67.035% (-4.3%) from 71.315%
21341708609

Pull #1471

github

web-flow
Merge d9a996e01 into 416f6a7fe
Pull Request #1471: [TASK] Add `SelectorComponent` interface and classes

16 of 130 new or added lines in 3 files covered. (12.31%)

1397 of 2084 relevant lines covered (67.03%)

28.94 hits per line

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

97.73
/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
19✔
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);
19✔
67

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

71
    final public function __construct(string $selector)
119✔
72
    {
73
        $this->setSelector($selector);
119✔
74
    }
119✔
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
88✔
84
    {
85
        $selectorParts = [];
88✔
86

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

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

118
        $selector = '';
74✔
119
        foreach ($selectorParts as $part) {
74✔
120
            $partSelector = $part->getValue();
74✔
121
            if (\in_array($partSelector, ['>', '+', '~'], true)) {
74✔
NEW
122
                $partSelector = ' ' . $partSelector . ' ';
×
123
            }
124
            $selector .= $partSelector;
74✔
125
        }
126

127
        return new static($selector);
74✔
128
    }
129

130
    public function getSelector(): string
31✔
131
    {
132
        return $this->selector;
31✔
133
    }
134

135
    public function setSelector(string $selector): void
119✔
136
    {
137
        $selector = \trim($selector);
119✔
138

139
        $hasAttribute = \strpos($selector, '[') !== false;
119✔
140

141
        // Whitespace can't be adjusted within an attribute selector, as it would change its meaning
142
        $this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector;
119✔
143
    }
119✔
144

145
    /**
146
     * @return int<0, max>
147
     */
148
    public function getSpecificity(): int
32✔
149
    {
150
        return SpecificityCalculator::calculate($this->selector);
32✔
151
    }
152

153
    public function render(OutputFormat $outputFormat): string
4✔
154
    {
155
        return $this->getSelector();
4✔
156
    }
157

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