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

MyIntervals / PHP-CSS-Parser / 16189025711

10 Jul 2025 07:35AM UTC coverage: 58.615% (+0.2%) from 58.397%
16189025711

push

github

web-flow
[BUGFIX] Allow comma in quoted string in selector (#1323)

Split by commas during parsing, not after.

16 of 16 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1075 of 1834 relevant lines covered (58.62%)

19.71 hits per line

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

79.55
/src/RuleSet/DeclarationBlock.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\RuleSet;
6

7
use Sabberworm\CSS\CSSList\CSSList;
8
use Sabberworm\CSS\CSSList\KeyFrame;
9
use Sabberworm\CSS\OutputFormat;
10
use Sabberworm\CSS\Parsing\OutputException;
11
use Sabberworm\CSS\Parsing\ParserState;
12
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
13
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
14
use Sabberworm\CSS\Property\KeyframeSelector;
15
use Sabberworm\CSS\Property\Selector;
16

17
/**
18
 * This class represents a `RuleSet` constrained by a `Selector`.
19
 *
20
 * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
21
 * matching elements.
22
 *
23
 * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
24
 */
25
class DeclarationBlock extends RuleSet
26
{
27
    /**
28
     * @var array<Selector|string>
29
     */
30
    private $selectors = [];
31

32
    /**
33
     * @throws UnexpectedTokenException
34
     * @throws UnexpectedEOFException
35
     *
36
     * @internal since V8.8.0
37
     */
38
    public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock
141✔
39
    {
40
        $comments = [];
141✔
41
        $result = new DeclarationBlock($parserState->currentLine());
141✔
42
        try {
43
            $selectors = [];
141✔
44
            $selectorParts = [];
141✔
45
            $stringWrapperCharacter = null;
141✔
46
            $consumedNextCharacter = false;
141✔
47
            static $stopCharacters = ['{', '}', '\'', '"', ','];
141✔
48
            do {
49
                if (!$consumedNextCharacter) {
141✔
50
                    $selectorParts[] = $parserState->consume(1);
141✔
51
                }
52
                $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
141✔
53
                $nextCharacter = $parserState->peek();
141✔
54
                $consumedNextCharacter = false;
141✔
55
                switch ($nextCharacter) {
141✔
56
                    case '\'':
141✔
57
                        // The fallthrough is intentional.
58
                    case '"':
141✔
59
                        if (!\is_string($stringWrapperCharacter)) {
42✔
60
                            $stringWrapperCharacter = $nextCharacter;
42✔
61
                        } elseif ($stringWrapperCharacter === $nextCharacter) {
42✔
62
                            if (\substr(\end($selectorParts), -1) !== '\\') {
42✔
63
                                $stringWrapperCharacter = null;
42✔
64
                            }
65
                        }
66
                        break;
42✔
67
                    case ',':
141✔
68
                        if (!\is_string($stringWrapperCharacter)) {
123✔
69
                            $selectors[] = \implode('', $selectorParts);
121✔
70
                            $selectorParts = [];
121✔
71
                            $parserState->consume(1);
121✔
72
                            $consumedNextCharacter = true;
121✔
73
                        }
74
                        break;
123✔
75
                }
76
            } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
141✔
77
            $selectors[] = \implode('', $selectorParts); // add final or only selector
141✔
78
            $result->setSelectors($selectors, $list);
141✔
79
            if ($parserState->comes('{')) {
138✔
80
                $parserState->consume(1);
138✔
81
            }
82
        } catch (UnexpectedTokenException $e) {
3✔
83
            if ($parserState->getSettings()->usesLenientParsing()) {
3✔
84
                if (!$parserState->comes('}')) {
3✔
85
                    $parserState->consumeUntil('}', false, true);
1✔
86
                }
87
                return null;
3✔
88
            } else {
89
                throw $e;
×
90
            }
91
        }
92
        $result->setComments($comments);
138✔
93
        RuleSet::parseRuleSet($parserState, $result);
138✔
94
        return $result;
138✔
95
    }
96

97
    /**
98
     * @param array<Selector|string>|string $selectors
99
     *
100
     * @throws UnexpectedTokenException
101
     */
102
    public function setSelectors($selectors, ?CSSList $list = null): void
142✔
103
    {
104
        if (\is_array($selectors)) {
142✔
105
            $this->selectors = $selectors;
142✔
106
        } else {
UNCOV
107
            $this->selectors = \explode(',', $selectors);
×
108
        }
109
        foreach ($this->selectors as $key => $selector) {
142✔
110
            if (!($selector instanceof Selector)) {
142✔
111
                if ($list === null || !($list instanceof KeyFrame)) {
141✔
112
                    if (!Selector::isValid($selector)) {
141✔
113
                        throw new UnexpectedTokenException(
3✔
114
                            "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
3✔
115
                            $selector,
116
                            'custom'
3✔
117
                        );
118
                    }
119
                    $this->selectors[$key] = new Selector($selector);
138✔
120
                } else {
121
                    if (!KeyframeSelector::isValid($selector)) {
×
122
                        throw new UnexpectedTokenException(
×
123
                            "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
×
124
                            $selector,
125
                            'custom'
×
126
                        );
127
                    }
128
                    $this->selectors[$key] = new KeyframeSelector($selector);
×
129
                }
130
            }
131
        }
132
    }
139✔
133

134
    /**
135
     * Remove one of the selectors of the block.
136
     *
137
     * @param Selector|string $selectorToRemove
138
     */
139
    public function removeSelector($selectorToRemove): bool
×
140
    {
141
        if ($selectorToRemove instanceof Selector) {
×
142
            $selectorToRemove = $selectorToRemove->getSelector();
×
143
        }
144
        foreach ($this->selectors as $key => $selector) {
×
145
            if ($selector->getSelector() === $selectorToRemove) {
×
146
                unset($this->selectors[$key]);
×
147
                return true;
×
148
            }
149
        }
150
        return false;
×
151
    }
152

153
    /**
154
     * @return array<Selector>
155
     */
156
    public function getSelectors(): array
132✔
157
    {
158
        return $this->selectors;
132✔
159
    }
160

161
    /**
162
     * @return non-empty-string
163
     *
164
     * @throws OutputException
165
     */
166
    public function render(OutputFormat $outputFormat): string
6✔
167
    {
168
        $formatter = $outputFormat->getFormatter();
6✔
169
        $result = $formatter->comments($this);
6✔
170
        if (\count($this->selectors) === 0) {
6✔
171
            // If all the selectors have been removed, this declaration block becomes invalid
172
            throw new OutputException(
×
173
                'Attempt to print declaration block with missing selector',
×
174
                $this->getLineNumber()
×
175
            );
176
        }
177
        $result .= $outputFormat->getContentBeforeDeclarationBlock();
6✔
178
        $result .= $formatter->implode(
6✔
179
            $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(),
6✔
180
            $this->selectors
6✔
181
        );
182
        $result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
6✔
183
        $result .= $formatter->spaceBeforeOpeningBrace() . '{';
6✔
184
        $result .= $this->renderRules($outputFormat);
6✔
185
        $result .= '}';
6✔
186
        $result .= $outputFormat->getContentAfterDeclarationBlock();
6✔
187

188
        return $result;
6✔
189
    }
190
}
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

© 2025 Coveralls, Inc