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

MyIntervals / PHP-CSS-Parser / 21341247349

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

Pull #1471

github

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

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

1 existing line in 1 file now uncovered.

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
                    \array_pop($selectorParts);
1✔
93
                    break;
1✔
94
                } else {
95
                    throw $e;
13✔
96
                }
97
            }
98
            try {
99
                $selectorParts[] = Combinator::parse($parserState, $comments);
75✔
100
            } catch (UnexpectedTokenException $e) {
74✔
101
                break;
74✔
102
            }
103
        }
104

105
        if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) {
75✔
106
            throw new UnexpectedTokenException(
1✔
107
                '`,`, `{`, `}` or EOF',
1✔
108
                $parserState->peek(5),
1✔
109
                'literal',
1✔
110
                $parserState->currentLine()
1✔
111
            );
112

113
        }
114

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

124
        return new static($selector);
74✔
125
    }
126

127
    public function getSelector(): string
31✔
128
    {
129
        return $this->selector;
31✔
130
    }
131

132
    public function setSelector(string $selector): void
119✔
133
    {
134
        $selector = \trim($selector);
119✔
135

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

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

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

150
    public function render(OutputFormat $outputFormat): string
4✔
151
    {
152
        return $this->getSelector();
4✔
153
    }
154

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