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

MyIntervals / PHP-CSS-Parser / 16146983935

08 Jul 2025 03:04PM UTC coverage: 58.527% (+0.6%) from 57.935%
16146983935

Pull #1292

github

web-flow
Merge 0ac639b0e into 6dec68e38
Pull Request #1292: [BUGFIX] Allow comma-separated arguments in selectors

28 of 29 new or added lines in 1 file covered. (96.55%)

1 existing line in 1 file now uncovered.

1081 of 1847 relevant lines covered (58.53%)

17.31 hits per line

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

75.25
/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
39✔
39
    {
40
        $comments = [];
39✔
41
        $result = new DeclarationBlock($parserState->currentLine());
39✔
42
        try {
43
            $selectors = [];
39✔
44
            $selectorParts = [];
39✔
45
            $stringWrapperCharacter = null;
39✔
46
            $functionNestingLevel = 0;
39✔
47
            $consumedNextCharacter = false;
39✔
48
            do {
49
                if (!$consumedNextCharacter) {
39✔
50
                    $selectorParts[] = $parserState->consume(1);
39✔
51
                }
52
                $selectorParts[] = $parserState->consumeUntil(
39✔
53
                    ['{', '}', '\'', '"', '(', ')', ','],
39✔
54
                    false,
39✔
55
                    false,
39✔
56
                    $comments
57
                );
58
                $nextCharacter = $parserState->peek();
39✔
59
                $consumedNextCharacter = false;
39✔
60
                switch ($nextCharacter) {
39✔
61
                    case '\'':
39✔
62
                        // The fallthrough is intentional.
63
                    case '"':
39✔
64
                        if (!\is_string($stringWrapperCharacter)) {
×
65
                            $stringWrapperCharacter = $nextCharacter;
×
66
                        } elseif ($stringWrapperCharacter === $nextCharacter) {
×
67
                            if (\substr(\end($selectorParts), -1) !== '\\') {
×
68
                                $stringWrapperCharacter = null;
×
69
                            }
70
                        }
71
                        break;
×
72
                    case '(':
39✔
73
                        if (!isset($stringWrapperCharacter)) {
18✔
74
                            ++$functionNestingLevel;
18✔
75
                        }
76
                        break;
18✔
77
                    case ')':
39✔
78
                        if (!isset($stringWrapperCharacter)) {
18✔
79
                            if ($functionNestingLevel <= 0) {
18✔
NEW
80
                                throw new UnexpectedTokenException('anything but', ')');
×
81
                            }
82
                            --$functionNestingLevel;
18✔
83
                        }
84
                        break;
18✔
85
                    case ',':
39✔
86
                        if (!isset($stringWrapperCharacter) && $functionNestingLevel === 0) {
26✔
87
                            $selectors[] = \implode('', $selectorParts);
25✔
88
                            $selectorParts = [];
25✔
89
                            $parserState->consume(1);
25✔
90
                            $consumedNextCharacter = true;
25✔
91
                        }
92
                        break;
26✔
93
                }
94
            } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
39✔
95
            $selectors[] = \implode('', $selectorParts); // add final or only selector
39✔
96
            $result->setSelectors($selectors, $list);
39✔
97
            if ($parserState->comes('{')) {
36✔
98
                $parserState->consume(1);
36✔
99
            }
100
        } catch (UnexpectedTokenException $e) {
3✔
101
            if ($parserState->getSettings()->usesLenientParsing()) {
3✔
102
                if (!$parserState->comes('}')) {
3✔
103
                    $parserState->consumeUntil('}', false, true);
1✔
104
                }
105
                return null;
3✔
106
            } else {
107
                throw $e;
×
108
            }
109
        }
110
        $result->setComments($comments);
36✔
111
        RuleSet::parseRuleSet($parserState, $result);
36✔
112
        return $result;
36✔
113
    }
114

115
    /**
116
     * @param array<Selector|string>|string $selectors
117
     *
118
     * @throws UnexpectedTokenException
119
     */
120
    public function setSelectors($selectors, ?CSSList $list = null): void
40✔
121
    {
122
        if (\is_array($selectors)) {
40✔
123
            $this->selectors = $selectors;
40✔
124
        } else {
UNCOV
125
            $this->selectors = \explode(',', $selectors);
×
126
        }
127
        foreach ($this->selectors as $key => $selector) {
40✔
128
            if (!($selector instanceof Selector)) {
40✔
129
                if ($list === null || !($list instanceof KeyFrame)) {
39✔
130
                    if (!Selector::isValid($selector)) {
39✔
131
                        throw new UnexpectedTokenException(
3✔
132
                            "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
3✔
133
                            $selector,
134
                            'custom'
3✔
135
                        );
136
                    }
137
                    $this->selectors[$key] = new Selector($selector);
36✔
138
                } else {
139
                    if (!KeyframeSelector::isValid($selector)) {
×
140
                        throw new UnexpectedTokenException(
×
141
                            "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
×
142
                            $selector,
143
                            'custom'
×
144
                        );
145
                    }
146
                    $this->selectors[$key] = new KeyframeSelector($selector);
×
147
                }
148
            }
149
        }
150
    }
37✔
151

152
    /**
153
     * Remove one of the selectors of the block.
154
     *
155
     * @param Selector|string $selectorToRemove
156
     */
157
    public function removeSelector($selectorToRemove): bool
×
158
    {
159
        if ($selectorToRemove instanceof Selector) {
×
160
            $selectorToRemove = $selectorToRemove->getSelector();
×
161
        }
162
        foreach ($this->selectors as $key => $selector) {
×
163
            if ($selector->getSelector() === $selectorToRemove) {
×
164
                unset($this->selectors[$key]);
×
165
                return true;
×
166
            }
167
        }
168
        return false;
×
169
    }
170

171
    /**
172
     * @return array<Selector>
173
     */
174
    public function getSelectors(): array
30✔
175
    {
176
        return $this->selectors;
30✔
177
    }
178

179
    /**
180
     * @return non-empty-string
181
     *
182
     * @throws OutputException
183
     */
184
    public function render(OutputFormat $outputFormat): string
6✔
185
    {
186
        $formatter = $outputFormat->getFormatter();
6✔
187
        $result = $formatter->comments($this);
6✔
188
        if (\count($this->selectors) === 0) {
6✔
189
            // If all the selectors have been removed, this declaration block becomes invalid
190
            throw new OutputException(
×
191
                'Attempt to print declaration block with missing selector',
×
192
                $this->getLineNumber()
×
193
            );
194
        }
195
        $result .= $outputFormat->getContentBeforeDeclarationBlock();
6✔
196
        $result .= $formatter->implode(
6✔
197
            $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(),
6✔
198
            $this->selectors
6✔
199
        );
200
        $result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
6✔
201
        $result .= $formatter->spaceBeforeOpeningBrace() . '{';
6✔
202
        $result .= $this->renderRules($outputFormat);
6✔
203
        $result .= '}';
6✔
204
        $result .= $outputFormat->getContentAfterDeclarationBlock();
6✔
205

206
        return $result;
6✔
207
    }
208
}
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