diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca3c21c..3cc0fd2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Please also have a look at our - Partial support for CSS Color Module Level 4: - `rgb` and `rgba`, and `hsl` and `hsla` are now aliases (#797} - Parse color functions that use the "modern" syntax (#800) + - Render RGB functions with "modern" syntax when required (#840) - Add a class diagram to the README (#482) - Add more tests (#449) diff --git a/src/Value/Color.php b/src/Value/Color.php index 5dd832a6..af6efa95 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -233,6 +233,10 @@ public function render(OutputFormat $outputFormat): string return $this->renderAsHex(); } + if ($this->shouldRenderInModernSyntax()) { + return $this->renderInModernSyntax($outputFormat); + } + return parent::render($outputFormat); } @@ -282,4 +286,87 @@ private function renderAsHex(): string return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result); } + + /** + * The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s. + * + * The "legacy" and "modern" monikers are part of the formal W3C syntax. + * See the following for more information: + * - {@link + * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax + * Description of the formal syntax for `rgb()` on MDN + * }; + * - {@link + * https://www.w3.org/TR/css-color-4/#rgb-functions + * The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft + * } (as of 13 February 2024, at time of writing). + */ + private function shouldRenderInModernSyntax(): bool + { + if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) { + return false; + } + + $hasPercentage = false; + $hasNumber = false; + foreach ($this->aComponents as $key => $value) { + if ($key === 'a') { + // Alpha can have units that don't match those of the RGB components in the "legacy" syntax. + // So it is not necessary to check it. It's also always last, hence `break` rather than `continue`. + break; + } + if (!($value instanceof Size)) { + // Unexpected, unknown, or modified via the API + return false; + } + $unit = $value->getUnit(); + // `switch` only does loose comparison + if ($unit === null) { + $hasNumber = true; + } elseif ($unit === '%') { + $hasPercentage = true; + } else { + // Invalid unit + return false; + } + } + + return $hasPercentage && $hasNumber; + } + + /** + * Some color functions, such as `rgb`, + * may have a mixture of `percentage`, `number`, or possibly other types in their arguments. + * + * Note that this excludes the alpha component, which is treated separately. + */ + private function colorFunctionMayHaveMixedValueTypes(string $function): bool + { + $functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba']; + + return \in_array($function, $functionsThatMayHaveMixedValueTypes, true); + } + + /** + * @return non-empty-string + */ + private function renderInModernSyntax(OutputFormat $outputFormat): string + { + // Maybe not yet without alpha, but will be... + $componentsWithoutAlpha = $this->aComponents; + \end($componentsWithoutAlpha); + if (\key($componentsWithoutAlpha) === 'a') { + $alpha = $this->aComponents['a']; + unset($componentsWithoutAlpha['a']); + } + + $arguments = $outputFormat->implode(' ', $componentsWithoutAlpha); + if (isset($alpha)) { + $separator = $outputFormat->spaceBeforeListArgumentSeparator('/') + . '/' . $outputFormat->spaceAfterListArgumentSeparator('/'); + $arguments = $outputFormat->implode($separator, [$arguments, $alpha]); + } + + return $this->getName() . '(' . $arguments . ')'; + } } diff --git a/tests/Unit/Value/ColorTest.php b/tests/Unit/Value/ColorTest.php index 2b533a7f..5c502f82 100644 --- a/tests/Unit/Value/ColorTest.php +++ b/tests/Unit/Value/ColorTest.php @@ -86,8 +86,6 @@ public static function provideValidColorAndExpectedRendering(): array 'rgb(0 119 0)', '#070', ], - // The "legacy" syntax currently used for rendering does not allow a mixture of percentages and numbers. - /* 'modern rgb with percentage R' => [ 'rgb(0% 119 0)', 'rgb(0% 119 0)', @@ -112,7 +110,6 @@ public static function provideValidColorAndExpectedRendering(): array 'rgb(0 60% 0%)', 'rgb(0 60% 0%)', ], - //*/ 'modern rgb with percentage components' => [ 'rgb(0% 60% 0%)', 'rgb(0%,60%,0%)', @@ -131,7 +128,6 @@ public static function provideValidColorAndExpectedRendering(): array 'rgb(0 119 0 / 50%)', 'rgba(0,119,0,50%)', ], - /* 'modern rgba with percentage R' => [ 'rgb(0% 119 0 / 0.5)', 'rgba(0% 119 0/.5)', @@ -144,7 +140,6 @@ public static function provideValidColorAndExpectedRendering(): array 'rgb(0 119 0% / 0.5)', 'rgba(0 119 0%/.5)', ], - //*/ 'modern rgba with percentage RGB' => [ 'rgb(0% 60% 0% / 0.5)', 'rgba(0%,60%,0%,.5)',