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

MyIntervals / PHP-CSS-Parser / 17733672509

15 Sep 2025 12:51PM UTC coverage: 59.658%. Remained the same
17733672509

push

github

web-flow
[BUGFIX] Use safe preg functions in `Value` (#1379)

Also use typesafe comparisons in the affected line.

Part of #1168

0 of 1 new or added line in 1 file covered. (0.0%)

1118 of 1874 relevant lines covered (59.66%)

25.34 hits per line

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

74.23
/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

15
use function Safe\preg_match;
16

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

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

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

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

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

134
        return $result;
12✔
135
    }
136

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

181
        return $value;
12✔
182
    }
183

184
    /**
185
     * @throws UnexpectedEOFException
186
     * @throws UnexpectedTokenException
187
     */
188
    private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
×
189
    {
190
        $function = $parserState->consumeUntil('(', false, true);
×
191
        $arguments = Value::parseValue($parserState, [',', '=']);
×
192
        return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
×
193
    }
194

195
    /**
196
     * @throws UnexpectedEOFException
197
     * @throws UnexpectedTokenException
198
     */
199
    private static function parseUnicodeRangeValue(ParserState $parserState): string
×
200
    {
201
        $codepointMaxLength = 6; // Code points outside BMP can use up to six digits
×
202
        $range = '';
×
203
        $parserState->consume('U+');
×
204
        do {
205
            if ($parserState->comes('-')) {
×
206
                $codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them
×
207
            }
208
            $range .= $parserState->consume(1);
×
209
        } while (
NEW
210
            (\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
×
211
        );
212

213
        return "U+{$range}";
×
214
    }
215
}
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