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
This commit is contained in:
Nick Gerleman
2025-01-21 20:20:04 -08:00
committed by Facebook GitHub Bot
parent fe3e3a54a3
commit 8803dca0bb
5 changed files with 305 additions and 2 deletions
@@ -9,6 +9,7 @@
#include <optional>
#include <react/renderer/css/CSSColorFunction.h>
#include <react/renderer/css/CSSDataType.h>
#include <react/renderer/css/CSSHexColor.h>
#include <react/renderer/css/CSSNamedColor.h>
@@ -39,6 +40,12 @@ struct CSSDataTypeParser<CSSColor> {
return {};
}
}
static constexpr auto consumeFunctionBlock(
const CSSFunctionBlock& func,
CSSSyntaxParser& parser) -> std::optional<CSSColor> {
return parseCSSColorFunction<CSSColor>(func.name, parser);
}
};
static_assert(CSSDataType<CSSColor>);
@@ -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 <algorithm>
#include <cstdint>
#include <optional>
#include <string_view>
#include <react/renderer/css/CSSAngle.h>
#include <react/renderer/css/CSSNumber.h>
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSSyntaxParser.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/utils/fnv1a.h>
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<int32_t>(f);
auto ceiled = f > i ? i + 1 : i;
return static_cast<uint8_t>(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 <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, CSSComponentValueDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSNumber>(green)) {
return {};
}
greenNumber = std::get<CSSNumber>(green).value;
auto blue = parseNextCSSValue<CSSNumber>(
parser, CSSComponentValueDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSNumber>(blue)) {
return {};
}
blueNumber = std::get<CSSNumber>(blue).value;
} else {
redNumber = std::get<CSSPercentage>(firstValue).value * 2.55f;
auto green = parseNextCSSValue<CSSPercentage>(
parser, CSSComponentValueDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSPercentage>(green)) {
return {};
}
greenNumber = std::get<CSSPercentage>(green).value * 2.55f;
auto blue = parseNextCSSValue<CSSPercentage>(
parser, CSSComponentValueDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<CSSPercentage>(blue)) {
return {};
}
blueNumber = std::get<CSSPercentage>(blue).value * 2.55f;
}
auto alphaValue = peekNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSComponentValueDelimiter::CommaOrWhitespace);
if (!std::holds_alternative<std::monostate>(alphaValue)) {
parser.consumeComponentValue(CSSComponentValueDelimiter::CommaOrWhitespace);
} else {
alphaValue = peekNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSComponentValueDelimiter::Solidus);
if (!std::holds_alternative<std::monostate>(alphaValue)) {
parser.consumeComponentValue(CSSComponentValueDelimiter::Solidus);
}
}
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
/**
* Parses a CSS <color-function> 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 <typename CSSColor>
constexpr std::optional<CSSColor> 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<CSSColor>(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
@@ -68,7 +68,7 @@ constexpr bool isValidHexColor(std::string_view hex) {
} // namespace detail
/**
* Parses a CSS <hex-color> value from hash stoken string value and returns a
* Parses a CSS <hex-color> value from hash token string value and returns a
* CSSColor if it is valid.
* https://www.w3.org/TR/css-color-4/#hex-color
*/
@@ -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} {}
@@ -99,4 +99,145 @@ TEST(CSSColor, named_colors) {
EXPECT_EQ(std::get<CSSColor>(transparentColor).a, 0);
}
TEST(CSSColor, rgb_rgba_values) {
auto simpleValue = parseCSSProperty<CSSColor>("rgb(255, 255, 255)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(simpleValue));
EXPECT_EQ(std::get<CSSColor>(simpleValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(simpleValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(simpleValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(simpleValue).a, 255);
auto capsValue = parseCSSProperty<CSSColor>("RGB(255, 255, 255)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(capsValue));
EXPECT_EQ(std::get<CSSColor>(capsValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(capsValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(capsValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(capsValue).a, 255);
auto modernSyntaxValue = parseCSSProperty<CSSColor>("rgb(255 255 255)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(modernSyntaxValue));
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).b, 255);
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);
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);
EXPECT_EQ(std::get<CSSColor>(mixedSpacingValue).b, 3);
EXPECT_EQ(std::get<CSSColor>(mixedSpacingValue).a, 255);
auto clampedValue = parseCSSProperty<CSSColor>("rgb(-50, 500, 0)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(clampedValue));
EXPECT_EQ(std::get<CSSColor>(clampedValue).r, 0);
EXPECT_EQ(std::get<CSSColor>(clampedValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(clampedValue).b, 0);
EXPECT_EQ(std::get<CSSColor>(clampedValue).a, 255);
auto fractionalValue = parseCSSProperty<CSSColor>("rgb(0.5, 0.5, 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(fractionalValue));
EXPECT_EQ(std::get<CSSColor>(fractionalValue).r, 1);
EXPECT_EQ(std::get<CSSColor>(fractionalValue).g, 1);
EXPECT_EQ(std::get<CSSColor>(fractionalValue).b, 1);
EXPECT_EQ(std::get<CSSColor>(fractionalValue).a, 255);
auto percentageValue = parseCSSProperty<CSSColor>("rgb(50%, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(percentageValue));
EXPECT_EQ(std::get<CSSColor>(percentageValue).r, 128);
EXPECT_EQ(std::get<CSSColor>(percentageValue).g, 128);
EXPECT_EQ(std::get<CSSColor>(percentageValue).b, 128);
auto mixedNumberPercentageValue =
parseCSSProperty<CSSColor>("rgb(50%, 0.5, 50%)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(mixedNumberPercentageValue));
auto rgbWithNumberAlphaValue =
parseCSSProperty<CSSColor>("rgb(255 255 255 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(rgbWithNumberAlphaValue));
EXPECT_EQ(std::get<CSSColor>(rgbWithNumberAlphaValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithNumberAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithNumberAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithNumberAlphaValue).a, 128);
auto rgbWithPercentageAlphaValue =
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);
EXPECT_EQ(std::get<CSSColor>(rgbWithPercentageAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithPercentageAlphaValue).a, 128);
auto rgbWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("rgb(255 255 255 / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(rgbWithSolidusAlphaValue));
EXPECT_EQ(std::get<CSSColor>(rgbWithSolidusAlphaValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithSolidusAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithSolidusAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbWithSolidusAlphaValue).a, 128);
auto rgbaWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("rgba(255 255 255 / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(rgbaWithSolidusAlphaValue));
EXPECT_EQ(std::get<CSSColor>(rgbaWithSolidusAlphaValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithSolidusAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithSolidusAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithSolidusAlphaValue).a, 128);
auto rgbaWithPercentageSolidusAlphaValue =
parseCSSProperty<CSSColor>("rgba(255 255 255 / 50%)");
EXPECT_TRUE(
std::holds_alternative<CSSColor>(rgbaWithPercentageSolidusAlphaValue));
EXPECT_EQ(std::get<CSSColor>(rgbaWithPercentageSolidusAlphaValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithPercentageSolidusAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithPercentageSolidusAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithPercentageSolidusAlphaValue).a, 128);
auto rgbaWithoutAlphaValue = parseCSSProperty<CSSColor>("rgba(255 255 255)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(rgbaWithoutAlphaValue));
EXPECT_EQ(std::get<CSSColor>(rgbaWithoutAlphaValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithoutAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithoutAlphaValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(rgbaWithoutAlphaValue).a, 255);
auto surroundingWhitespaceValue =
parseCSSProperty<CSSColor>(" rgb(255, 1, 2) ");
EXPECT_TRUE(std::holds_alternative<CSSColor>(surroundingWhitespaceValue));
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).g, 1);
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).b, 2);
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).a, 255);
auto valueWithSingleComponent = parseCSSProperty<CSSColor>("rgb(255)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueWithSingleComponent));
auto valueWithTooFewComponents = parseCSSProperty<CSSColor>("rgb(255, 255)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(valueWithTooFewComponents));
auto valueWithTooManyComponents =
parseCSSProperty<CSSColor>("rgb(255, 255, 255, 255, 255)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(valueWithTooManyComponents));
auto valueStartingWithComma = parseCSSProperty<CSSColor>("rgb(, 1, 2)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueStartingWithComma));
auto valueEndingWithComma = parseCSSProperty<CSSColor>("rgb(1, 2, )");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueEndingWithComma));
}
TEST(CSSColor, constexpr_values) {
[[maybe_unused]] constexpr auto simpleValue =
parseCSSProperty<CSSColor>("rgb(255, 255, 255)");
}
} // namespace facebook::react