Add <hex-color> to CSSParser (#44624)

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

The syntax for <color> is defined as (https://www.w3.org/TR/css-color-4/#color-syntax):

```
<color> = <color-base> | currentColor | <system-color>

<color-base> = <hex-color> | <color-function> | <named-color> | transparent
<color-function> = <rgb()> | <rgba()> | <hsl()> | <hsla()> | <hwb()> | <lab()> | <lch()> | <oklab()> | <oklch()> | <color()>
```

This diff implements in particular:
```
<color-base> = <hex-color>
```

We parse over a `<hash-token>` to extract a 3, 4, 6 or 8 digit long hexadecimal and parse it into a CSSColor struct representation:

```
struct Color {
    uint8_t r // 0-255
    uint8_t g // 0-255
    uint8_t b // 0-255
    uint8_t a // 0.0 - 1.0
}
```
Changelog: [Internal]

Reviewed By: NickGerleman

Differential Revision: D57287309

fbshipit-source-id: 35ce82e52fdc8fc86425e03d7883ae09b429b9e4
This commit is contained in:
Jorge Cabiedes Acosta
2024-05-20 07:29:51 -07:00
committed by Facebook GitHub Bot
parent 7349dabae2
commit 417ff5fec9
4 changed files with 173 additions and 1 deletions
@@ -27,6 +27,7 @@ enum class CSSValueType : uint8_t {
Percentage,
Ratio,
Angle,
Color,
};
/**
@@ -81,4 +82,11 @@ struct CSSAngle {
float degrees{};
};
struct CSSColor {
uint8_t r{};
uint8_t g{};
uint8_t b{};
uint8_t a{};
};
} // namespace facebook::react
@@ -76,7 +76,12 @@ class CSSValueParser {
return *percentage;
}
}
// <color>
if constexpr (hasType<CSSColor>()) {
if (auto colorValue = consumeColorToken(token)) {
return *colorValue;
}
}
return CSSValue{};
});
// TODO: support function component values and simple blocks
@@ -224,6 +229,93 @@ class CSSValueParser {
value != -std::numeric_limits<float>::infinity();
}
enum class HexColorType {
Long,
Short,
};
constexpr std::optional<CSSValue> consumeColorToken(
const CSSPreservedToken& token) {
// https://www.w3.org/TR/css-color-4/#hex-color
std::string_view hexColorValue = token.stringValue();
if (isValidHexColor(hexColorValue)) {
if (hexColorValue.length() == 3) {
return CSSValue::color(
hexToNumeric(hexColorValue.substr(0, 1), HexColorType::Short),
hexToNumeric(hexColorValue.substr(1, 1), HexColorType::Short),
hexToNumeric(hexColorValue.substr(2, 1), HexColorType::Short),
255u);
} else if (hexColorValue.length() == 4) {
return CSSValue::color(
hexToNumeric(hexColorValue.substr(0, 1), HexColorType::Short),
hexToNumeric(hexColorValue.substr(1, 1), HexColorType::Short),
hexToNumeric(hexColorValue.substr(2, 1), HexColorType::Short),
hexToNumeric(hexColorValue.substr(3, 1), HexColorType::Short));
} else if (hexColorValue.length() == 6) {
return CSSValue::color(
hexToNumeric(hexColorValue.substr(0, 2), HexColorType::Long),
hexToNumeric(hexColorValue.substr(2, 2), HexColorType::Long),
hexToNumeric(hexColorValue.substr(4, 2), HexColorType::Long),
255u);
} else if (hexColorValue.length() == 8) {
return CSSValue::color(
hexToNumeric(hexColorValue.substr(0, 2), HexColorType::Long),
hexToNumeric(hexColorValue.substr(2, 2), HexColorType::Long),
hexToNumeric(hexColorValue.substr(4, 2), HexColorType::Long),
hexToNumeric(hexColorValue.substr(6, 2), HexColorType::Long));
}
}
return {};
}
constexpr char toLower(char c) {
if (c >= 'A' && c <= 'Z') {
return static_cast<char>(c + 32);
}
return c;
}
constexpr uint8_t hexToNumeric(std::string_view hex, HexColorType hexType) {
int result = 0;
for (char c : hex) {
int value = 0;
if (c >= '0' && c <= '9') {
value = c - '0';
} else {
value = toLower(c) - 'a' + 10;
}
result *= 16;
result += value;
}
if (hexType == HexColorType::Short) {
return result * 16 + result;
} else {
return result;
}
}
constexpr bool isValidHexColor(std::string_view hex) {
// The syntax of a <hex-color> is a <hash-token> token whose value consists
// of 3, 4, 6, or 8 hexadecimal digits.
if (hex.size() != 3 && hex.size() != 4 && hex.size() != 6 &&
hex.size() != 8) {
return false;
}
for (auto c : hex) {
if (!isHexDigit(c)) {
return false;
}
}
return true;
}
constexpr bool isHexDigit(char c) {
return (c >= '0' && c <= '9') || (toLower(c) >= 'a' && toLower(c) <= 'f');
}
CSSSyntaxParser parser_;
};
@@ -109,6 +109,13 @@ class CSSValueVariant {
return CSSValueVariant(CSSValueType::Angle, CSSAngle{degrees});
}
static constexpr CSSValueVariant
color(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
requires(canRepresent<CSSColor>())
{
return CSSValueVariant(CSSValueType::Color, CSSColor{r, g, b, a});
}
constexpr CSSValueType type() const {
return type_;
}
@@ -155,6 +162,12 @@ class CSSValueVariant {
return getIf<CSSValueType::Angle, CSSAngle>();
}
constexpr CSSColor getColor() const
requires(canRepresent<CSSColor>())
{
return getIf<CSSValueType::Color, CSSColor>();
}
constexpr bool hasValue() const
requires(canRepresent<CSSWideKeyword>())
{
@@ -185,6 +198,8 @@ class CSSValueVariant {
return getRatio() == other.getRatio();
case CSSValueType::Angle:
return getAngle() == other.getAngle();
case CSSValueType::Color:
return getColor() == other.getColor();
}
return false;
@@ -320,4 +320,61 @@ TEST(CSSValueParser, parse_length_prop_constexpr) {
EXPECT_EQ(pxValue.getLength().unit, CSSLengthUnit::Px);
}
TEST(CSSValueParser, hex_color_values) {
auto emptyValue = parseCSSValue<CSSWideKeyword, CSSColor>("");
EXPECT_EQ(emptyValue.type(), CSSValueType::CSSWideKeyword);
EXPECT_EQ(emptyValue.getCSSWideKeyword(), CSSWideKeyword::Unset);
auto hex3DigitColorValue = parseCSSValue<CSSWideKeyword, CSSColor>("#fff");
EXPECT_EQ(hex3DigitColorValue.type(), CSSValueType::Color);
EXPECT_EQ(hex3DigitColorValue.getColor().r, 255);
EXPECT_EQ(hex3DigitColorValue.getColor().g, 255);
EXPECT_EQ(hex3DigitColorValue.getColor().b, 255);
EXPECT_EQ(hex3DigitColorValue.getColor().a, 255);
auto hex4DigitColorValue = parseCSSValue<CSSWideKeyword, CSSColor>("#ffff");
EXPECT_EQ(hex4DigitColorValue.type(), CSSValueType::Color);
EXPECT_EQ(hex4DigitColorValue.getColor().r, 255);
EXPECT_EQ(hex4DigitColorValue.getColor().g, 255);
EXPECT_EQ(hex4DigitColorValue.getColor().b, 255);
EXPECT_EQ(hex4DigitColorValue.getColor().a, 255);
auto hex6DigitColorValue = parseCSSValue<CSSWideKeyword, CSSColor>("#ffffff");
EXPECT_EQ(hex6DigitColorValue.type(), CSSValueType::Color);
EXPECT_EQ(hex6DigitColorValue.getColor().r, 255);
EXPECT_EQ(hex6DigitColorValue.getColor().g, 255);
EXPECT_EQ(hex6DigitColorValue.getColor().b, 255);
EXPECT_EQ(hex6DigitColorValue.getColor().a, 255);
auto hex8DigitColorValue =
parseCSSValue<CSSWideKeyword, CSSColor>("#ffffffff");
EXPECT_EQ(hex8DigitColorValue.type(), CSSValueType::Color);
EXPECT_EQ(hex8DigitColorValue.getColor().r, 255);
EXPECT_EQ(hex8DigitColorValue.getColor().g, 255);
EXPECT_EQ(hex8DigitColorValue.getColor().b, 255);
EXPECT_EQ(hex8DigitColorValue.getColor().a, 255);
auto hexMixedCaseColorValue =
parseCSSValue<CSSWideKeyword, CSSColor>("#FFCc99");
EXPECT_EQ(hexMixedCaseColorValue.type(), CSSValueType::Color);
EXPECT_EQ(hexMixedCaseColorValue.getColor().r, 255);
EXPECT_EQ(hexMixedCaseColorValue.getColor().g, 204);
EXPECT_EQ(hexMixedCaseColorValue.getColor().b, 153);
EXPECT_EQ(hexMixedCaseColorValue.getColor().a, 255);
auto hexDigitOnlyColorValue = parseCSSValue<CSSWideKeyword, CSSColor>("#369");
EXPECT_EQ(hexDigitOnlyColorValue.type(), CSSValueType::Color);
EXPECT_EQ(hexDigitOnlyColorValue.getColor().r, 51);
EXPECT_EQ(hexDigitOnlyColorValue.getColor().g, 102);
EXPECT_EQ(hexDigitOnlyColorValue.getColor().b, 153);
EXPECT_EQ(hexDigitOnlyColorValue.getColor().a, 255);
auto hexAlphaTestValue = parseCSSValue<CSSWideKeyword, CSSColor>("#FFFFFFCC");
EXPECT_EQ(hexAlphaTestValue.type(), CSSValueType::Color);
EXPECT_EQ(hexAlphaTestValue.getColor().r, 255);
EXPECT_EQ(hexAlphaTestValue.getColor().g, 255);
EXPECT_EQ(hexAlphaTestValue.getColor().b, 255);
EXPECT_EQ(hexAlphaTestValue.getColor().a, 204);
}
} // namespace facebook::react