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

MyIntervals / PHP-CSS-Parser / 21338071049

25 Jan 2026 07:14PM UTC coverage: 71.315% (+0.6%) from 70.738%
21338071049

Pull #1478

github

web-flow
Merge a293ba132 into 416f6a7fe
Pull Request #1478: [TASK] Add `SelectorComponent` interface

1432 of 2008 relevant lines covered (71.31%)

30.64 hits per line

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

74.75
/src/Value/Value.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\Value;
6

7
use Sabberworm\CSS\CSSElement;
8
use Sabberworm\CSS\Parsing\ParserState;
9
use Sabberworm\CSS\Parsing\SourceException;
10
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
11
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
12
use Sabberworm\CSS\Position\Position;
13
use Sabberworm\CSS\Position\Positionable;
14
use Sabberworm\CSS\ShortClassNameProvider;
15

16
use function Safe\preg_match;
17

18
/**
19
 * Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
20
 * abstract subclass `ValueList`.
21
 */
22
abstract class Value implements CSSElement, Positionable
23
{
24
    use Position;
25
    use ShortClassNameProvider;
26

27
    /**
28
     * @param int<1, max>|null $lineNumber
29
     */
30
    public function __construct(?int $lineNumber = null)
74✔
31
    {
32
        $this->setPosition($lineNumber);
74✔
33
    }
74✔
34

35
    /**
36
     * @param array<non-empty-string> $listDelimiters
37
     *
38
     * @return Value|string
39
     *
40
     * @throws UnexpectedTokenException
41
     * @throws UnexpectedEOFException
42
     *
43
     * @internal since V8.8.0
44
     */
45
    public static function parseValue(ParserState $parserState, array $listDelimiters = [])
12✔
46
    {
47
        /** @var list<Value|string> $stack */
48
        $stack = [];
12✔
49
        $parserState->consumeWhiteSpace();
12✔
50
        //Build a list of delimiters and parsed values
51
        while (
52
        !($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!')
12✔
53
            || $parserState->comes(')')
12✔
54
            || $parserState->isEnd())
12✔
55
        ) {
56
            if (\count($stack) > 0) {
12✔
57
                $foundDelimiter = false;
11✔
58
                foreach ($listDelimiters as $delimiter) {
11✔
59
                    if ($parserState->comes($delimiter)) {
11✔
60
                        \array_push($stack, $parserState->consume($delimiter));
11✔
61
                        $parserState->consumeWhiteSpace();
11✔
62
                        $foundDelimiter = true;
11✔
63
                        break;
11✔
64
                    }
65
                }
66
                if (!$foundDelimiter) {
11✔
67
                    //Whitespace was the list delimiter
68
                    \array_push($stack, ' ');
10✔
69
                }
70
            }
71
            \array_push($stack, self::parsePrimitiveValue($parserState));
12✔
72
            $parserState->consumeWhiteSpace();
12✔
73
        }
74
        // Convert the list to list objects
75
        foreach ($listDelimiters as $delimiter) {
12✔
76
            $stackSize = \count($stack);
12✔
77
            if ($stackSize === 1) {
12✔
78
                return $stack[0];
12✔
79
            }
80
            $newStack = [];
11✔
81
            for ($offset = 0; $offset < $stackSize; ++$offset) {
11✔
82
                if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) {
11✔
83
                    $newStack[] = $stack[$offset];
11✔
84
                    continue;
11✔
85
                }
86
                $length = 2; //Number of elements to be joined
11✔
87
                for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) {
11✔
88
                    if ($delimiter !== $stack[$i]) {
10✔
89
                        break;
1✔
90
                    }
91
                }
92
                $list = new RuleValueList($delimiter, $parserState->currentLine());
11✔
93
                for ($i = $offset; $i - $offset < $length * 2; $i += 2) {
11✔
94
                    $list->addListComponent($stack[$i]);
11✔
95
                }
96
                $newStack[] = $list;
11✔
97
                $offset += $length * 2 - 2;
11✔
98
            }
99
            $stack = $newStack;
11✔
100
        }
101
        if (!isset($stack[0])) {
11✔
102
            throw new UnexpectedTokenException(
×
103
                " {$parserState->peek()} ",
×
104
                $parserState->peek(1, -1) . $parserState->peek(2),
×
105
                'literal',
×
106
                $parserState->currentLine()
×
107
            );
108
        }
109
        return $stack[0];
11✔
110
    }
111

112
    /**
113
     * @return CSSFunction|string
114
     *
115
     * @throws UnexpectedEOFException
116
     * @throws UnexpectedTokenException
117
     *
118
     * @internal since V8.8.0
119
     */
120
    public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false)
12✔
121
    {
122
        $anchor = $parserState->anchor();
12✔
123
        $result = $parserState->parseIdentifier($ignoreCase);
12✔
124

125
        if ($parserState->comes('(')) {
12✔
126
            $anchor->backtrack();
12✔
127
            if ($parserState->streql('url', $result)) {
12✔
128
                $result = URL::parse($parserState);
×
129
            } elseif ($parserState->streql('calc', $result)) {
12✔
130
                $result = CalcFunction::parse($parserState);
1✔
131
            } else {
132
                $result = CSSFunction::parse($parserState, $ignoreCase);
11✔
133
            }
134
        }
135

136
        return $result;
12✔
137
    }
138

139
    /**
140
     * @return CSSFunction|CSSString|LineName|Size|URL|string
141
     *
142
     * @throws UnexpectedEOFException
143
     * @throws UnexpectedTokenException
144
     * @throws SourceException
145
     *
146
     * @internal since V8.8.0
147
     */
148
    public static function parsePrimitiveValue(ParserState $parserState)
12✔
149
    {
150
        $value = null;
12✔
151
        $parserState->consumeWhiteSpace();
12✔
152
        if (
153
            \is_numeric($parserState->peek())
12✔
154
            || ($parserState->comes('-.')
12✔
155
                && \is_numeric($parserState->peek(1, 2)))
12✔
156
            || (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1)))
12✔
157
        ) {
158
            $value = Size::parse($parserState);
12✔
159
        } elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) {
12✔
160
            $value = Color::parse($parserState);
×
161
        } elseif ($parserState->comes("'") || $parserState->comes('"')) {
12✔
162
            $value = CSSString::parse($parserState);
×
163
        } elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) {
12✔
164
            $value = self::parseMicrosoftFilter($parserState);
×
165
        } elseif ($parserState->comes('[')) {
12✔
166
            $value = LineName::parse($parserState);
×
167
        } elseif ($parserState->comes('U+')) {
12✔
168
            $value = self::parseUnicodeRangeValue($parserState);
×
169
        } else {
170
            $nextCharacter = $parserState->peek(1);
12✔
171
            try {
172
                $value = self::parseIdentifierOrFunction($parserState);
12✔
173
            } catch (UnexpectedTokenException $e) {
9✔
174
                if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) {
9✔
175
                    $value = $parserState->consume(1);
9✔
176
                } else {
177
                    throw $e;
×
178
                }
179
            }
180
        }
181
        $parserState->consumeWhiteSpace();
12✔
182

183
        return $value;
12✔
184
    }
185

186
    /**
187
     * @return array<string, bool|int|float|string|array<mixed>|null>
188
     *
189
     * @internal
190
     */
191
    public function getArrayRepresentation(): array
1✔
192
    {
193
        return [
194
            'class' => $this->getShortClassName(),
1✔
195
        ];
196
    }
197

198
    /**
199
     * @throws UnexpectedEOFException
200
     * @throws UnexpectedTokenException
201
     */
202
    private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
×
203
    {
204
        $function = $parserState->consumeUntil('(', false, true);
×
205
        $arguments = Value::parseValue($parserState, [',', '=']);
×
206
        return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
×
207
    }
208

209
    /**
210
     * @throws UnexpectedEOFException
211
     * @throws UnexpectedTokenException
212
     */
213
    private static function parseUnicodeRangeValue(ParserState $parserState): string
×
214
    {
215
        $codepointMaxLength = 6; // Code points outside BMP can use up to six digits
×
216
        $range = '';
×
217
        $parserState->consume('U+');
×
218
        do {
219
            if ($parserState->comes('-')) {
×
220
                $codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them
×
221
            }
222
            $range .= $parserState->consume(1);
×
223
        } while (
224
            (\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
×
225
        );
226

227
        return "U+{$range}";
×
228
    }
229
}
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