More spec compliant rgb function parsing (#48839)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48839

In the last diff I mixed and matched `<legacy-rgb-syntax>` and `<modern-rgb-syntax>` a bit to keep compatiblity with `normalze-color`.

Spec noncompliant values have only been allowed since https://github.com/facebook/react-native/pull/34600 with the main issue being that legacy syntax rgb functions are allowed to use the `/` based alpha syntax, and commas can be mixed with whitespace. This seems like an exceedingly rare real-world scenario (there are currently zero usages of slash syntax in RKJSModules validated by `rgb\([^\)]*/`), so I'm going to instead just follow the spec for more sanity.

Another bit that I missed was that modern RGB functions allow individual components to be `<percentage>` or `<number>` compared to legacy functions which only allow the full function to accept one or the other (`normalize-color` doesn't support `<percentage>` at all), so I fixed that as well.

I started sharing a little bit more of the logic here, to make things more readable when adding more functions.

Changelog: [Internal]

Reviewed By: javache

Differential Revision: D68468275

fbshipit-source-id: f1dfab51b91a3f64436c2559daa3d1e8891db889
This commit is contained in:
Nick Gerleman
2025-01-23 16:19:48 -08:00
committed by Facebook GitHub Bot
parent 24393c7dde
commit 5b3d8d3410
4 changed files with 178 additions and 81 deletions
@@ -17,6 +17,7 @@
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSSyntaxParser.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/utils/PackTraits.h>
#include <react/utils/fnv1a.h>
namespace facebook::react {
@@ -33,71 +34,156 @@ constexpr uint8_t clamp255Component(float f) {
return static_cast<uint8_t>(std::clamp(ceiled, 0, 255));
}
constexpr std::optional<float> normalizeNumberComponent(
const std::variant<std::monostate, CSSNumber>& component) {
if (std::holds_alternative<CSSNumber>(component)) {
return std::get<CSSNumber>(component).value;
}
return {};
}
template <typename... ComponentT>
requires(
(std::is_same_v<CSSNumber, ComponentT> ||
std::is_same_v<CSSPercentage, ComponentT>) &&
...)
constexpr std::optional<float> normalizeComponent(
const std::variant<std::monostate, ComponentT...>& component,
float baseValue) {
if constexpr (traits::containsType<CSSPercentage, ComponentT...>()) {
if (std::holds_alternative<CSSPercentage>(component)) {
return std::get<CSSPercentage>(component).value / 100.0f * baseValue;
}
}
if constexpr (traits::containsType<CSSNumber, ComponentT...>()) {
if (std::holds_alternative<CSSNumber>(component)) {
return std::get<CSSNumber>(component).value;
}
}
return {};
}
template <CSSDataType... FirstComponentAllowedTypesT>
constexpr bool isLegacyColorFunction(CSSSyntaxParser& parser) {
auto lookahead = parser;
auto next = parseNextCSSValue<FirstComponentAllowedTypesT...>(lookahead);
if (std::holds_alternative<std::monostate>(next)) {
return false;
}
return lookahead.consumeComponentValue<bool>(
CSSDelimiter::OptionalWhitespace, [](CSSPreservedToken token) {
return token.type() == CSSTokenType::Comma;
});
}
/**
* Parses a legacy syntax rgb() or rgba() function and returns a CSSColor if it
* is valid.
* https://www.w3.org/TR/css-color-4/#typedef-legacy-rgb-syntax
*/
template <typename CSSColor>
constexpr std::optional<CSSColor> parseLegacyRgbFunction(
CSSSyntaxParser& parser) {
auto rawRed = parseNextCSSValue<CSSNumber, CSSPercentage>(parser);
bool usesNumber = std::holds_alternative<CSSNumber>(rawRed);
auto red = normalizeComponent(rawRed, 255.0f);
if (!red.has_value()) {
return {};
}
auto green = usesNumber
? normalizeNumberComponent(
parseNextCSSValue<CSSNumber>(parser, CSSDelimiter::Comma))
: normalizeComponent(
parseNextCSSValue<CSSPercentage>(parser, CSSDelimiter::Comma),
255.0f);
if (!green.has_value()) {
return {};
}
auto blue = usesNumber
? normalizeNumberComponent(
parseNextCSSValue<CSSNumber>(parser, CSSDelimiter::Comma))
: normalizeComponent(
parseNextCSSValue<CSSPercentage>(parser, CSSDelimiter::Comma),
255.0f);
if (!blue.has_value()) {
return {};
}
auto alpha = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(parser, CSSDelimiter::Comma),
1.0f);
return CSSColor{
.r = clamp255Component(*red),
.g = clamp255Component(*green),
.b = clamp255Component(*blue),
.a = alpha.has_value() ? clamp255Component(*alpha * 255.0f)
: static_cast<uint8_t>(255u),
};
}
/**
* Parses a modern syntax rgb() or rgba() function and returns a CSSColor if it
* is valid.
* https://www.w3.org/TR/css-color-4/#typedef-modern-rgb-syntax
*/
template <typename CSSColor>
constexpr std::optional<CSSColor> parseModernRgbFunction(
CSSSyntaxParser& parser) {
auto red = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(parser), 255.0f);
if (!red.has_value()) {
return {};
}
auto green = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::Whitespace),
255.0f);
if (!green.has_value()) {
return {};
}
auto blue = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::Whitespace),
255.0f);
if (!blue.has_value()) {
return {};
}
auto alpha = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::SolidusOrWhitespace),
1.0f);
return CSSColor{
.r = clamp255Component(*red),
.g = clamp255Component(*green),
.b = clamp255Component(*blue),
.a = alpha.has_value() ? clamp255Component(*alpha * 255.0f)
: static_cast<uint8_t>(255u),
};
}
/**
* 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 <typename CSSColor>
constexpr std::optional<CSSColor> parseRgbFunction(CSSSyntaxParser& parser) {
auto firstValue = parseNextCSSValue<CSSNumber, CSSPercentage>(parser);
if (std::holds_alternative<std::monostate>(firstValue)) {
return {};
}
float redNumber = 0;
float greenNumber = 0;
float blueNumber = 0;
if (std::holds_alternative<CSSNumber>(firstValue)) {
redNumber = std::get<CSSNumber>(firstValue).value;
auto green =
parseNextCSSValue<CSSNumber>(parser, CSSDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSNumber>(green)) {
return {};
}
greenNumber = std::get<CSSNumber>(green).value;
auto blue =
parseNextCSSValue<CSSNumber>(parser, CSSDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSNumber>(blue)) {
return {};
}
blueNumber = std::get<CSSNumber>(blue).value;
if (isLegacyColorFunction<CSSNumber, CSSPercentage>(parser)) {
return parseLegacyRgbFunction<CSSColor>(parser);
} else {
redNumber = std::get<CSSPercentage>(firstValue).value * 2.55f;
auto green = parseNextCSSValue<CSSPercentage>(
parser, CSSDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSPercentage>(green)) {
return {};
}
greenNumber = std::get<CSSPercentage>(green).value * 2.55f;
auto blue = parseNextCSSValue<CSSPercentage>(
parser, CSSDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSPercentage>(blue)) {
return {};
}
blueNumber = std::get<CSSPercentage>(blue).value * 2.55f;
return parseModernRgbFunction<CSSColor>(parser);
}
auto alphaValue = parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::CommaOrWhitespaceOrSolidus);
float alphaNumber = std::holds_alternative<std::monostate>(alphaValue) ? 1.0f
: std::holds_alternative<CSSNumber>(alphaValue)
? std::get<CSSNumber>(alphaValue).value
: std::get<CSSPercentage>(alphaValue).value / 100.0f;
return CSSColor{
.r = clamp255Component(redNumber),
.g = clamp255Component(greenNumber),
.b = clamp255Component(blueNumber),
.a = clamp255Component(alphaNumber * 255.0f),
};
}
} // namespace detail
@@ -93,9 +93,9 @@ enum class CSSDelimiter {
Whitespace,
OptionalWhitespace,
Solidus,
SolidusOrWhitespace,
Comma,
CommaOrWhitespace,
CommaOrWhitespaceOrSolidus,
None,
};
@@ -313,10 +313,9 @@ struct CSSComponentValueVisitorDispatcher {
return true;
}
return false;
case CSSDelimiter::CommaOrWhitespaceOrSolidus:
if (parser.peek().type() == CSSTokenType::Comma ||
(parser.peek().type() == CSSTokenType::Delim &&
parser.peek().stringValue() == "/")) {
case CSSDelimiter::SolidusOrWhitespace:
if (parser.peek().type() == CSSTokenType::Delim &&
parser.peek().stringValue() == "/") {
parser.consumeToken();
parser.consumeWhitespace();
return true;
@@ -122,13 +122,9 @@ TEST(CSSColor, rgb_rgba_values) {
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).a, 255);
auto mixedDelimeterValue = parseCSSProperty<CSSColor>("rgb(255,255 255)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(mixedDelimeterValue));
EXPECT_EQ(std::get<CSSColor>(mixedDelimeterValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(mixedDelimeterValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(mixedDelimeterValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(mixedDelimeterValue).a, 255);
EXPECT_TRUE(std::holds_alternative<std::monostate>(mixedDelimeterValue));
auto mixedSpacingValue = parseCSSProperty<CSSColor>("rgb( 5 4,3)");
auto mixedSpacingValue = parseCSSProperty<CSSColor>("rgb( 5 4 3)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(mixedSpacingValue));
EXPECT_EQ(std::get<CSSColor>(mixedSpacingValue).r, 5);
EXPECT_EQ(std::get<CSSColor>(mixedSpacingValue).g, 4);
@@ -155,10 +151,19 @@ TEST(CSSColor, rgb_rgba_values) {
EXPECT_EQ(std::get<CSSColor>(percentageValue).g, 128);
EXPECT_EQ(std::get<CSSColor>(percentageValue).b, 128);
auto mixedNumberPercentageValue =
auto mixedLegacyNumberPercentageValue =
parseCSSProperty<CSSColor>("rgb(50%, 0.5, 50%)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(mixedNumberPercentageValue));
std::holds_alternative<std::monostate>(mixedLegacyNumberPercentageValue));
auto mixedModernNumberPercentageValue =
parseCSSProperty<CSSColor>("rgb(50% 0.5 50%)");
EXPECT_TRUE(
std::holds_alternative<CSSColor>(mixedModernNumberPercentageValue));
EXPECT_EQ(std::get<CSSColor>(mixedModernNumberPercentageValue).r, 128);
EXPECT_EQ(std::get<CSSColor>(mixedModernNumberPercentageValue).g, 1);
EXPECT_EQ(std::get<CSSColor>(mixedModernNumberPercentageValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(mixedModernNumberPercentageValue).a, 255);
auto rgbWithNumberAlphaValue =
parseCSSProperty<CSSColor>("rgb(255 255 255 0.5)");
@@ -169,7 +174,7 @@ TEST(CSSColor, rgb_rgba_values) {
EXPECT_EQ(std::get<CSSColor>(rgbWithNumberAlphaValue).a, 128);
auto rgbWithPercentageAlphaValue =
parseCSSProperty<CSSColor>("rgb(255 255 255, 50%)");
parseCSSProperty<CSSColor>("rgb(255 255 255 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(rgbWithPercentageAlphaValue));
EXPECT_EQ(std::get<CSSColor>(rgbWithPercentageAlphaValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithPercentageAlphaValue).g, 255);
@@ -184,6 +189,11 @@ TEST(CSSColor, rgb_rgba_values) {
EXPECT_EQ(std::get<CSSColor>(rgbWithSolidusAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithSolidusAlphaValue).a, 128);
auto rgbLegacySyntaxWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("rgb(1, 4, 5 /0.5)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(
rgbLegacySyntaxWithSolidusAlphaValue));
auto rgbaWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("rgba(255 255 255 / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(rgbaWithSolidusAlphaValue));
@@ -236,7 +246,12 @@ TEST(CSSColor, rgb_rgba_values) {
}
TEST(CSSColor, constexpr_values) {
[[maybe_unused]] constexpr auto simpleValue =
[[maybe_unused]] constexpr auto emptyValue = parseCSSProperty<CSSColor>("");
[[maybe_unused]] constexpr auto hexColorValue =
parseCSSProperty<CSSColor>("#fff");
[[maybe_unused]] constexpr auto rgbFunctionValue =
parseCSSProperty<CSSColor>("rgb(255, 255, 255)");
}
@@ -533,8 +533,8 @@ TEST(CSSSyntaxParser, required_whitespace_not_present) {
EXPECT_EQ(delimValue2, "/");
}
TEST(CSSSyntaxParser, comma_or_whitespace_or_solidus) {
CSSSyntaxParser parser{"foo, bar / baz potato%"};
TEST(CSSSyntaxParser, solidus_or_whitespace) {
CSSSyntaxParser parser{"foo bar / baz potato, papaya"};
auto identValue1 = parser.consumeComponentValue<std::string_view>(
CSSDelimiter::OptionalWhitespace, [](const CSSPreservedToken& token) {
@@ -546,8 +546,7 @@ TEST(CSSSyntaxParser, comma_or_whitespace_or_solidus) {
EXPECT_EQ(identValue1, "foo");
auto identValue2 = parser.consumeComponentValue<std::string_view>(
CSSDelimiter::CommaOrWhitespaceOrSolidus,
[](const CSSPreservedToken& token) {
CSSDelimiter::SolidusOrWhitespace, [](const CSSPreservedToken& token) {
EXPECT_EQ(token.type(), CSSTokenType::Ident);
EXPECT_EQ(token.stringValue(), "bar");
return token.stringValue();
@@ -556,8 +555,7 @@ TEST(CSSSyntaxParser, comma_or_whitespace_or_solidus) {
EXPECT_EQ(identValue2, "bar");
auto identValue3 = parser.consumeComponentValue<std::string_view>(
CSSDelimiter::CommaOrWhitespaceOrSolidus,
[](const CSSPreservedToken& token) {
CSSDelimiter::SolidusOrWhitespace, [](const CSSPreservedToken& token) {
EXPECT_EQ(token.type(), CSSTokenType::Ident);
EXPECT_EQ(token.stringValue(), "baz");
return token.stringValue();
@@ -566,8 +564,7 @@ TEST(CSSSyntaxParser, comma_or_whitespace_or_solidus) {
EXPECT_EQ(identValue3, "baz");
auto identValue4 = parser.consumeComponentValue<std::string_view>(
CSSDelimiter::CommaOrWhitespaceOrSolidus,
[](const CSSPreservedToken& token) {
CSSDelimiter::SolidusOrWhitespace, [](const CSSPreservedToken& token) {
EXPECT_EQ(token.type(), CSSTokenType::Ident);
EXPECT_EQ(token.stringValue(), "potato");
return token.stringValue();
@@ -576,7 +573,7 @@ TEST(CSSSyntaxParser, comma_or_whitespace_or_solidus) {
EXPECT_EQ(identValue4, "potato");
auto delimValue1 = parser.consumeComponentValue<bool>(
CSSDelimiter::CommaOrWhitespaceOrSolidus,
CSSDelimiter::SolidusOrWhitespace,
[](const CSSPreservedToken& token) { return true; });
EXPECT_FALSE(delimValue1);