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

MyIntervals / PHP-CSS-Parser / 12914116700

22 Jan 2025 05:52PM UTC coverage: 41.373% (+0.4%) from 40.95%
12914116700

push

github

web-flow
[FEATURE] Support `rgba` and `rgb` (etc.) being aliases (#797)

As of CSS Color Module Level 4, `rgba` is an alias of `rgb`;
likewise, `hsla` is an alias of `hsl`.

This change allows any of the above color functions to contain either three or
four arguments, with alpha assumed as the fourth, and allowed to be absent.

21 of 23 new or added lines in 1 file covered. (91.3%)

844 of 2040 relevant lines covered (41.37%)

5.82 hits per line

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

89.47
/src/Value/Color.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\Value;
6

7
use Sabberworm\CSS\OutputFormat;
8
use Sabberworm\CSS\Parsing\ParserState;
9
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
10
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
11

12
/**
13
 * `Color's can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are always stored as an array of
14
 * ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form.
15
 */
16
class Color extends CSSFunction
17
{
18
    /**
19
     * @param array<int, Value|string> $aColor
20
     * @param int $iLineNo
21
     */
22
    public function __construct(array $aColor, $iLineNo = 0)
20✔
23
    {
24
        parent::__construct(\implode('', \array_keys($aColor)), $aColor, ',', $iLineNo);
20✔
25
    }
20✔
26

27
    /**
28
     * @throws UnexpectedEOFException
29
     * @throws UnexpectedTokenException
30
     */
31
    public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): CSSFunction
52✔
32
    {
33
        $aColor = [];
52✔
34
        if ($oParserState->comes('#')) {
52✔
35
            $oParserState->consume('#');
25✔
36
            $sValue = $oParserState->parseIdentifier(false);
25✔
37
            if ($oParserState->strlen($sValue) === 3) {
24✔
38
                $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
8✔
39
            } elseif ($oParserState->strlen($sValue) === 4) {
18✔
40
                $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3]
3✔
41
                    . $sValue[3];
3✔
42
            }
43

44
            if ($oParserState->strlen($sValue) === 8) {
24✔
45
                $aColor = [
46
                    'r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
4✔
47
                    'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
4✔
48
                    'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
4✔
49
                    'a' => new Size(
4✔
50
                        \round(self::mapRange(\intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2),
4✔
51
                        null,
4✔
52
                        true,
4✔
53
                        $oParserState->currentLine()
4✔
54
                    ),
55
                ];
56
            } elseif ($oParserState->strlen($sValue) === 6) {
21✔
57
                $aColor = [
58
                    'r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
14✔
59
                    'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
14✔
60
                    'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
14✔
61
                ];
62
            } else {
63
                throw new UnexpectedTokenException(
9✔
64
                    'Invalid hex color value',
9✔
65
                    $sValue,
66
                    'custom',
9✔
67
                    $oParserState->currentLine()
24✔
68
                );
69
            }
70
        } else {
71
            $sColorMode = $oParserState->parseIdentifier(true);
31✔
72
            $oParserState->consumeWhiteSpace();
31✔
73
            $oParserState->consume('(');
31✔
74

75
            // CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`.
76
            // So, attempt to parse with the `a`, and allow for it not being there.
77
            switch ($sColorMode) {
31✔
78
                case 'rgb':
31✔
79
                    $colorModeForParsing = 'rgba';
11✔
80
                    $mayHaveOptionalAlpha = true;
11✔
81
                    break;
11✔
82
                case 'hsl':
23✔
83
                    $colorModeForParsing = 'hsla';
14✔
84
                    $mayHaveOptionalAlpha = true;
14✔
85
                    break;
14✔
86
                case 'rgba':
11✔
87
                    // This is handled identically to the following case.
88
                case 'hsla':
5✔
89
                    $colorModeForParsing = $sColorMode;
11✔
90
                    $mayHaveOptionalAlpha = true;
11✔
91
                    break;
11✔
92
                default:
NEW
93
                    $colorModeForParsing = $sColorMode;
×
NEW
94
                    $mayHaveOptionalAlpha = false;
×
95
            }
96

97
            $bContainsVar = false;
31✔
98
            $iLength = $oParserState->strlen($colorModeForParsing);
31✔
99
            for ($i = 0; $i < $iLength; ++$i) {
31✔
100
                $oParserState->consumeWhiteSpace();
31✔
101
                if ($oParserState->comes('var')) {
31✔
102
                    $aColor[$colorModeForParsing[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState);
2✔
103
                    $bContainsVar = true;
2✔
104
                } else {
105
                    $aColor[$colorModeForParsing[$i]] = Size::parse($oParserState, true);
31✔
106
                }
107

108
                // This must be done first, to consume comments as well, so that the `comes` test will work.
109
                $oParserState->consumeWhiteSpace();
31✔
110

111
                // With a `var` argument, the function can have fewer arguments.
112
                // And as of CSS Color Module Level 4, the alpha argument is optional.
113
                $canCloseNow =
114
                    $bContainsVar ||
31✔
115
                    ($mayHaveOptionalAlpha && $i >= $iLength - 2);
31✔
116
                if ($canCloseNow && $oParserState->comes(')')) {
31✔
117
                    break;
23✔
118
                }
119

120
                if ($i < ($iLength - 1)) {
31✔
121
                    $oParserState->consume(',');
31✔
122
                }
123
            }
124
            $oParserState->consume(')');
25✔
125

126
            if ($bContainsVar) {
23✔
127
                return new CSSFunction($sColorMode, \array_values($aColor), ',', $oParserState->currentLine());
2✔
128
            }
129
        }
130
        return new Color($aColor, $oParserState->currentLine());
36✔
131
    }
132

133
    private static function mapRange(float $fVal, float $fFromMin, float $fFromMax, float $fToMin, float $fToMax): float
2✔
134
    {
135
        $fFromRange = $fFromMax - $fFromMin;
2✔
136
        $fToRange = $fToMax - $fToMin;
2✔
137
        $fMultiplier = $fToRange / $fFromRange;
2✔
138
        $fNewVal = $fVal - $fFromMin;
2✔
139
        $fNewVal *= $fMultiplier;
2✔
140
        return $fNewVal + $fToMin;
2✔
141
    }
142

143
    /**
144
     * @return array<int, Value|string>
145
     */
146
    public function getColor()
×
147
    {
148
        return $this->aComponents;
×
149
    }
150

151
    /**
152
     * @param array<int, Value|string> $aColor
153
     */
154
    public function setColor(array $aColor): void
×
155
    {
156
        $this->setName(\implode('', \array_keys($aColor)));
×
157
        $this->aComponents = $aColor;
×
158
    }
×
159

160
    /**
161
     * @return string
162
     */
163
    public function getColorDescription()
×
164
    {
165
        return $this->getName();
×
166
    }
167

168
    public function __toString(): string
20✔
169
    {
170
        return $this->render(new OutputFormat());
20✔
171
    }
172

173
    public function render(OutputFormat $oOutputFormat): string
20✔
174
    {
175
        // Shorthand RGB color values
176
        if ($oOutputFormat->getRGBHashNotation() && \implode('', \array_keys($this->aComponents)) === 'rgb') {
20✔
177
            $sResult = \sprintf(
6✔
178
                '%02x%02x%02x',
6✔
179
                $this->aComponents['r']->getSize(),
6✔
180
                $this->aComponents['g']->getSize(),
6✔
181
                $this->aComponents['b']->getSize()
6✔
182
            );
183
            return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5])
6✔
184
                    ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult);
6✔
185
        }
186
        return parent::render($oOutputFormat);
14✔
187
    }
188
}
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