From 8803dca0bb21c7bb101cd9d3bca550b63b7b47a7 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Tue, 21 Jan 2025 20:20:04 -0800 Subject: [PATCH] Support parsing rgb() and rgba() functions into CSSColor (#48827) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48827 This teaches the CSSColor parser how to handle these functions. There are a surprising amount of edge cases here, including many syntactic options added by the CSS Color Module 4 spec, and some technically invalid examples supported by normalize-color, sometimes working in Chrome. I used the combination of the spec, and existing functionality and tests for `normalize-color`, with the end result supporting a superset of the functionality of both, while being a bit more permissive than either. I still need to add support for the other color functions, and will probably want to share code here, but for now, just implemented everything for the rgb values as a start. Changelog: [Internal] Reviewed By: javache Differential Revision: D68362477 fbshipit-source-id: 62973ba2f8361b6a43c7cf9a96029147f84582d2 --- .../ReactCommon/react/renderer/css/CSSColor.h | 7 + .../react/renderer/css/CSSColorFunction.h | 155 ++++++++++++++++++ .../react/renderer/css/CSSHexColor.h | 2 +- .../react/renderer/css/CSSSyntaxParser.h | 2 +- .../react/renderer/css/tests/CSSColorTest.cpp | 141 ++++++++++++++++ 5 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSColor.h b/packages/react-native/ReactCommon/react/renderer/css/CSSColor.h index 5d8324f6da2..b5d36b241d4 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSColor.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSColor.h @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -39,6 +40,12 @@ struct CSSDataTypeParser { return {}; } } + + static constexpr auto consumeFunctionBlock( + const CSSFunctionBlock& func, + CSSSyntaxParser& parser) -> std::optional { + return parseCSSColorFunction(func.name, parser); + } }; static_assert(CSSDataType); diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h b/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h new file mode 100644 index 00000000000..dc9acbb3a04 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSColorFunction.h @@ -0,0 +1,155 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace detail { + +constexpr uint8_t clamp255Component(float f) { + // Implementations should honor the precision of the channel as authored or + // calculated wherever possible. If this is not possible, the channel should + // be rounded towards +∞. + // https://www.w3.org/TR/css-color-4/#rgb-functions + auto i = static_cast(f); + auto ceiled = f > i ? i + 1 : i; + return static_cast(std::clamp(ceiled, 0, 255)); +} + +/** + * Parses an rgb() or rgba() function and returns a CSSColor if it is valid. + * Some invalid syntax (like mixing commas and whitespace) are allowed for + * backwards compatibility with normalize-color. + * https://www.w3.org/TR/css-color-4/#funcdef-rgb + */ +template +constexpr std::optional parseRgbFunction(CSSSyntaxParser& parser) { + auto firstValue = parseNextCSSValue(parser); + if (std::holds_alternative(firstValue)) { + return {}; + } + + float redNumber = 0; + float greenNumber = 0; + float blueNumber = 0; + + if (std::holds_alternative(firstValue)) { + redNumber = std::get(firstValue).value; + + auto green = parseNextCSSValue( + parser, CSSComponentValueDelimiter::CommaOrWhitespace); + if (!std::holds_alternative(green)) { + return {}; + } + greenNumber = std::get(green).value; + + auto blue = parseNextCSSValue( + parser, CSSComponentValueDelimiter::CommaOrWhitespace); + if (!std::holds_alternative(blue)) { + return {}; + } + blueNumber = std::get(blue).value; + } else { + redNumber = std::get(firstValue).value * 2.55f; + + auto green = parseNextCSSValue( + parser, CSSComponentValueDelimiter::CommaOrWhitespace); + if (!std::holds_alternative(green)) { + return {}; + } + greenNumber = std::get(green).value * 2.55f; + + auto blue = parseNextCSSValue( + parser, CSSComponentValueDelimiter::CommaOrWhitespace); + if (!std::holds_alternative(blue)) { + return {}; + } + blueNumber = std::get(blue).value * 2.55f; + } + + auto alphaValue = peekNextCSSValue( + parser, CSSComponentValueDelimiter::CommaOrWhitespace); + if (!std::holds_alternative(alphaValue)) { + parser.consumeComponentValue(CSSComponentValueDelimiter::CommaOrWhitespace); + } else { + alphaValue = peekNextCSSValue( + parser, CSSComponentValueDelimiter::Solidus); + if (!std::holds_alternative(alphaValue)) { + parser.consumeComponentValue(CSSComponentValueDelimiter::Solidus); + } + } + + float alphaNumber = std::holds_alternative(alphaValue) ? 1.0f + : std::holds_alternative(alphaValue) + ? std::get(alphaValue).value + : std::get(alphaValue).value / 100.0f; + + return CSSColor{ + .r = clamp255Component(redNumber), + .g = clamp255Component(greenNumber), + .b = clamp255Component(blueNumber), + .a = clamp255Component(alphaNumber * 255.0f), + }; +} +} // namespace detail + +/** + * Parses a CSS value from function name and contents and + * returns a CSSColor if it is valid. + * https://www.w3.org/TR/css-color-4/#typedef-color-function + */ +template +constexpr std::optional parseCSSColorFunction( + std::string_view colorFunction, + CSSSyntaxParser& parser) { + switch (fnv1aLowercase(colorFunction)) { + // CSS Color Module Level 4 treats the alpha variants of functions as the + // same as non-alpha variants (alpha is optional for both). + case fnv1a("rgb"): + case fnv1a("rgba"): + return detail::parseRgbFunction(parser); + break; + case fnv1a("hsl"): + case fnv1a("hsla"): + // TODO + break; + case fnv1a("hwb"): + case fnv1a("hwba"): + // TODO + break; + case fnv1a("lab"): + break; + case fnv1a("lch"): + break; + case fnv1a("oklab"): + break; + case fnv1a("oklch"): + break; + case fnv1a("color"): + // TODO T213000437: Support `color()` functions and wide-gamut colors. + break; + default: + return {}; + } + + return {}; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSHexColor.h b/packages/react-native/ReactCommon/react/renderer/css/CSSHexColor.h index ca53b90e83a..58607f67d4a 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSHexColor.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSHexColor.h @@ -68,7 +68,7 @@ constexpr bool isValidHexColor(std::string_view hex) { } // namespace detail /** - * Parses a CSS value from hash stoken string value and returns a + * Parses a CSS value from hash token string value and returns a * CSSColor if it is valid. * https://www.w3.org/TR/css-color-4/#hex-color */ diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h b/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h index e0631c248eb..b278c2683c9 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSSyntaxParser.h @@ -199,7 +199,7 @@ class CSSSyntaxParser { } private: - CSSSyntaxParser(CSSSyntaxParser& parser, CSSTokenType terminator) + constexpr CSSSyntaxParser(CSSSyntaxParser& parser, CSSTokenType terminator) : tokenizer_{parser.tokenizer_}, currentToken_{parser.currentToken_}, terminator_{terminator} {} diff --git a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp index 3286ce59a56..3935b400669 100644 --- a/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/css/tests/CSSColorTest.cpp @@ -99,4 +99,145 @@ TEST(CSSColor, named_colors) { EXPECT_EQ(std::get(transparentColor).a, 0); } +TEST(CSSColor, rgb_rgba_values) { + auto simpleValue = parseCSSProperty("rgb(255, 255, 255)"); + EXPECT_TRUE(std::holds_alternative(simpleValue)); + EXPECT_EQ(std::get(simpleValue).r, 255); + EXPECT_EQ(std::get(simpleValue).g, 255); + EXPECT_EQ(std::get(simpleValue).b, 255); + EXPECT_EQ(std::get(simpleValue).a, 255); + + auto capsValue = parseCSSProperty("RGB(255, 255, 255)"); + EXPECT_TRUE(std::holds_alternative(capsValue)); + EXPECT_EQ(std::get(capsValue).r, 255); + EXPECT_EQ(std::get(capsValue).g, 255); + EXPECT_EQ(std::get(capsValue).b, 255); + EXPECT_EQ(std::get(capsValue).a, 255); + + auto modernSyntaxValue = parseCSSProperty("rgb(255 255 255)"); + EXPECT_TRUE(std::holds_alternative(modernSyntaxValue)); + EXPECT_EQ(std::get(modernSyntaxValue).r, 255); + EXPECT_EQ(std::get(modernSyntaxValue).g, 255); + EXPECT_EQ(std::get(modernSyntaxValue).b, 255); + EXPECT_EQ(std::get(modernSyntaxValue).a, 255); + + auto mixedDelimeterValue = parseCSSProperty("rgb(255,255 255)"); + EXPECT_TRUE(std::holds_alternative(mixedDelimeterValue)); + EXPECT_EQ(std::get(mixedDelimeterValue).r, 255); + EXPECT_EQ(std::get(mixedDelimeterValue).g, 255); + EXPECT_EQ(std::get(mixedDelimeterValue).b, 255); + EXPECT_EQ(std::get(mixedDelimeterValue).a, 255); + + auto mixedSpacingValue = parseCSSProperty("rgb( 5 4,3)"); + EXPECT_TRUE(std::holds_alternative(mixedSpacingValue)); + EXPECT_EQ(std::get(mixedSpacingValue).r, 5); + EXPECT_EQ(std::get(mixedSpacingValue).g, 4); + EXPECT_EQ(std::get(mixedSpacingValue).b, 3); + EXPECT_EQ(std::get(mixedSpacingValue).a, 255); + + auto clampedValue = parseCSSProperty("rgb(-50, 500, 0)"); + EXPECT_TRUE(std::holds_alternative(clampedValue)); + EXPECT_EQ(std::get(clampedValue).r, 0); + EXPECT_EQ(std::get(clampedValue).g, 255); + EXPECT_EQ(std::get(clampedValue).b, 0); + EXPECT_EQ(std::get(clampedValue).a, 255); + + auto fractionalValue = parseCSSProperty("rgb(0.5, 0.5, 0.5)"); + EXPECT_TRUE(std::holds_alternative(fractionalValue)); + EXPECT_EQ(std::get(fractionalValue).r, 1); + EXPECT_EQ(std::get(fractionalValue).g, 1); + EXPECT_EQ(std::get(fractionalValue).b, 1); + EXPECT_EQ(std::get(fractionalValue).a, 255); + + auto percentageValue = parseCSSProperty("rgb(50%, 50%, 50%)"); + EXPECT_TRUE(std::holds_alternative(percentageValue)); + EXPECT_EQ(std::get(percentageValue).r, 128); + EXPECT_EQ(std::get(percentageValue).g, 128); + EXPECT_EQ(std::get(percentageValue).b, 128); + + auto mixedNumberPercentageValue = + parseCSSProperty("rgb(50%, 0.5, 50%)"); + EXPECT_TRUE( + std::holds_alternative(mixedNumberPercentageValue)); + + auto rgbWithNumberAlphaValue = + parseCSSProperty("rgb(255 255 255 0.5)"); + EXPECT_TRUE(std::holds_alternative(rgbWithNumberAlphaValue)); + EXPECT_EQ(std::get(rgbWithNumberAlphaValue).r, 255); + EXPECT_EQ(std::get(rgbWithNumberAlphaValue).g, 255); + EXPECT_EQ(std::get(rgbWithNumberAlphaValue).b, 255); + EXPECT_EQ(std::get(rgbWithNumberAlphaValue).a, 128); + + auto rgbWithPercentageAlphaValue = + parseCSSProperty("rgb(255 255 255, 50%)"); + EXPECT_TRUE(std::holds_alternative(rgbWithPercentageAlphaValue)); + EXPECT_EQ(std::get(rgbWithPercentageAlphaValue).r, 255); + EXPECT_EQ(std::get(rgbWithPercentageAlphaValue).g, 255); + EXPECT_EQ(std::get(rgbWithPercentageAlphaValue).b, 255); + EXPECT_EQ(std::get(rgbWithPercentageAlphaValue).a, 128); + + auto rgbWithSolidusAlphaValue = + parseCSSProperty("rgb(255 255 255 / 0.5)"); + EXPECT_TRUE(std::holds_alternative(rgbWithSolidusAlphaValue)); + EXPECT_EQ(std::get(rgbWithSolidusAlphaValue).r, 255); + EXPECT_EQ(std::get(rgbWithSolidusAlphaValue).g, 255); + EXPECT_EQ(std::get(rgbWithSolidusAlphaValue).b, 255); + EXPECT_EQ(std::get(rgbWithSolidusAlphaValue).a, 128); + + auto rgbaWithSolidusAlphaValue = + parseCSSProperty("rgba(255 255 255 / 0.5)"); + EXPECT_TRUE(std::holds_alternative(rgbaWithSolidusAlphaValue)); + EXPECT_EQ(std::get(rgbaWithSolidusAlphaValue).r, 255); + EXPECT_EQ(std::get(rgbaWithSolidusAlphaValue).g, 255); + EXPECT_EQ(std::get(rgbaWithSolidusAlphaValue).b, 255); + EXPECT_EQ(std::get(rgbaWithSolidusAlphaValue).a, 128); + + auto rgbaWithPercentageSolidusAlphaValue = + parseCSSProperty("rgba(255 255 255 / 50%)"); + EXPECT_TRUE( + std::holds_alternative(rgbaWithPercentageSolidusAlphaValue)); + EXPECT_EQ(std::get(rgbaWithPercentageSolidusAlphaValue).r, 255); + EXPECT_EQ(std::get(rgbaWithPercentageSolidusAlphaValue).g, 255); + EXPECT_EQ(std::get(rgbaWithPercentageSolidusAlphaValue).b, 255); + EXPECT_EQ(std::get(rgbaWithPercentageSolidusAlphaValue).a, 128); + + auto rgbaWithoutAlphaValue = parseCSSProperty("rgba(255 255 255)"); + EXPECT_TRUE(std::holds_alternative(rgbaWithoutAlphaValue)); + EXPECT_EQ(std::get(rgbaWithoutAlphaValue).r, 255); + EXPECT_EQ(std::get(rgbaWithoutAlphaValue).g, 255); + EXPECT_EQ(std::get(rgbaWithoutAlphaValue).b, 255); + EXPECT_EQ(std::get(rgbaWithoutAlphaValue).a, 255); + + auto surroundingWhitespaceValue = + parseCSSProperty(" rgb(255, 1, 2) "); + EXPECT_TRUE(std::holds_alternative(surroundingWhitespaceValue)); + EXPECT_EQ(std::get(surroundingWhitespaceValue).r, 255); + EXPECT_EQ(std::get(surroundingWhitespaceValue).g, 1); + EXPECT_EQ(std::get(surroundingWhitespaceValue).b, 2); + EXPECT_EQ(std::get(surroundingWhitespaceValue).a, 255); + + auto valueWithSingleComponent = parseCSSProperty("rgb(255)"); + EXPECT_TRUE(std::holds_alternative(valueWithSingleComponent)); + + auto valueWithTooFewComponents = parseCSSProperty("rgb(255, 255)"); + EXPECT_TRUE( + std::holds_alternative(valueWithTooFewComponents)); + + auto valueWithTooManyComponents = + parseCSSProperty("rgb(255, 255, 255, 255, 255)"); + EXPECT_TRUE( + std::holds_alternative(valueWithTooManyComponents)); + + auto valueStartingWithComma = parseCSSProperty("rgb(, 1, 2)"); + EXPECT_TRUE(std::holds_alternative(valueStartingWithComma)); + + auto valueEndingWithComma = parseCSSProperty("rgb(1, 2, )"); + EXPECT_TRUE(std::holds_alternative(valueEndingWithComma)); +} + +TEST(CSSColor, constexpr_values) { + [[maybe_unused]] constexpr auto simpleValue = + parseCSSProperty("rgb(255, 255, 255)"); +} + } // namespace facebook::react