mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
fe3e3a54a3
commit
8803dca0bb
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user