Skip to content

Commit

Permalink
Support parsing hsl() and hsla() functions
Browse files Browse the repository at this point in the history
Summary:
Adds support for hsl() and hsla() funtions. This supports more modern syntax options than normalize-color, like number components, optional alpha, and fills in missing support for angle units. The underlying math was lifted pretty much directly from normalize-color though.

An aside, std::remainder for these is not guaranteed to be constexpr, but Clang is still okay with rgb function parsing to be contexpr because we never try to evaluate non-constexpr function?

Changelog: [Internal]

Differential Revision: D68473990
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Jan 22, 2025
1 parent 29679bb commit ab54cc4
Show file tree
Hide file tree
Showing 2 changed files with 305 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
#pragma once

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <optional>
#include <string_view>
#include <tuple>

#include <react/renderer/css/CSSAngle.h>
#include <react/renderer/css/CSSNumber.h>
Expand Down Expand Up @@ -43,6 +45,66 @@ constexpr std::optional<float> normNumberComponent(
return {};
}

constexpr uint8_t clampAlpha(std::optional<float> alpha) {
return alpha.has_value() ? clamp255Component(*alpha * 255.0f)
: static_cast<uint8_t>(255u);
}

constexpr std::optional<float> normHueComponent(
const std::variant<std::monostate, CSSNumber, CSSAngle>& component) {
if (std::holds_alternative<CSSNumber>(component)) {
return std::get<CSSNumber>(component).value;
} else if (std::holds_alternative<CSSAngle>(component)) {
return std::get<CSSAngle>(component).degrees;
}

return {};
}

inline float normHue(float hue) {
auto rem = std::remainder(hue, 360.0f);
return (rem < 0 ? rem + 360 : rem) / 360.0f;
}

constexpr float hueToTgb(float p, float q, float t) {
if (t < 0.0f) {
t += 1.0f;
}
if (t > 1.0f) {
t -= 1.0f;
}
if (t < 1.0f / 6.0f) {
return p + (q - p) * 6 * t;
}
if (t < 1.0f / 2.0f) {
return q;
}
if (t < 2.0f / 3.0f) {
return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
}
return p;
}

inline std::tuple<uint8_t, uint8_t, uint8_t>
hslToRgb(float h, float s, float l) {
h = normHue(h);
s = std::clamp(s / 100.0f, 0.0f, 1.0f);
l = std::clamp(l / 100.0f, 0.0f, 1.0f);

auto q = l < 0.5f ? l * (1.0f + s) : l + s - l * s;
auto p = 2.0f * l - q;

auto r = hueToTgb(p, q, h + 1.0f / 3.0f);
auto g = hueToTgb(p, q, h);
auto b = hueToTgb(p, q, h - 1.0f / 3.0f);

return {
static_cast<uint8_t>(std::round(r * 255.0f)),
static_cast<uint8_t>(std::round(g * 255.0f)),
static_cast<uint8_t>(std::round(b * 255.0f)),
};
}

template <typename... ComponentT>
requires(
(std::is_same_v<CSSNumber, ComponentT> ||
Expand Down Expand Up @@ -124,8 +186,7 @@ constexpr std::optional<CSSColor> parseLegacyRgbFunction(
.r = clamp255Component(*red),
.g = clamp255Component(*green),
.b = clamp255Component(*blue),
.a = alpha.has_value() ? clamp255Component(*alpha * 255.0f)
: static_cast<uint8_t>(255u),
.a = clampAlpha(alpha),
};
}

Expand Down Expand Up @@ -168,8 +229,7 @@ constexpr std::optional<CSSColor> parseModernRgbFunction(
.r = clamp255Component(*red),
.g = clamp255Component(*green),
.b = clamp255Component(*blue),
.a = alpha.has_value() ? clamp255Component(*alpha * 255.0f)
: static_cast<uint8_t>(255u),
.a = clampAlpha(alpha),
};
}

Expand All @@ -185,6 +245,97 @@ constexpr std::optional<CSSColor> parseRgbFunction(CSSSyntaxParser& parser) {
return parseModernRgbFunction<CSSColor>(parser);
}
}

/**
* Parses a legacy syntax hsl() or hsla() function and returns a CSSColor if it
* is valid.
* https://www.w3.org/TR/css-color-4/#typedef-legacy-hsl-syntax
*/
template <typename CSSColor>
inline std::optional<CSSColor> parseLegacyHslFunction(CSSSyntaxParser& parser) {
auto h = normHueComponent(parseNextCSSValue<CSSNumber, CSSAngle>(parser));
if (!h.has_value()) {
return {};
}

auto s = normComponent(
parseNextCSSValue<CSSPercentage>(parser, CSSDelimiter::Comma), 100.0f);
if (!s.has_value()) {
return {};
}

auto l = normComponent(
parseNextCSSValue<CSSPercentage>(parser, CSSDelimiter::Comma), 100.0f);
if (!l.has_value()) {
return {};
}
auto a = normComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(parser, CSSDelimiter::Comma),
1.0f);

auto [r, g, b] = hslToRgb(*h, *s, *l);

return CSSColor{
.r = r,
.g = g,
.b = b,
.a = clampAlpha(a),
};
}

/**
* Parses a modern syntax hsl() or hsla() function and returns a CSSColor if
* it is valid. https://www.w3.org/TR/css-color-4/#typedef-modern-hsl-syntax
*/
template <typename CSSColor>
inline std::optional<CSSColor> parseModernHslFunction(CSSSyntaxParser& parser) {
auto h = normHueComponent(parseNextCSSValue<CSSNumber, CSSAngle>(parser));
if (!h.has_value()) {
return {};
}

auto s = normComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::Whitespace),
100.0f);
if (!s.has_value()) {
return {};
}

auto l = normComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::Whitespace),
100.0f);
if (!l.has_value()) {
return {};
}
auto a = normComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::SolidusOrWhitespace),
1.0f);

auto [r, g, b] = hslToRgb(*h, *s, *l);

return CSSColor{
.r = r,
.g = g,
.b = b,
.a = clampAlpha(a),
};
}

/**
* Parses an hsl() or hsla() function and returns a CSSColor if it is valid.
* https://www.w3.org/TR/css-color-4/#funcdef-hsl
*/
template <typename CSSColor>
inline std::optional<CSSColor> parseHslFunction(CSSSyntaxParser& parser) {
if (isLegacyColorFunction<CSSNumber, CSSAngle>(parser)) {
return parseLegacyHslFunction<CSSColor>(parser);
} else {
return parseModernHslFunction<CSSColor>(parser);
}
}
} // namespace detail

/**
Expand All @@ -205,7 +356,7 @@ constexpr std::optional<CSSColor> parseCSSColorFunction(
break;
case fnv1a("hsl"):
case fnv1a("hsla"):
// TODO
return detail::parseHslFunction<CSSColor>(parser);
break;
case fnv1a("hwb"):
case fnv1a("hwba"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,155 @@ TEST(CSSColor, rgb_rgba_values) {
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueEndingWithComma));
}

TEST(CSSColor, hsl_hsla_values) {
auto simpleValue = parseCSSProperty<CSSColor>("hsl(180, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(simpleValue));
EXPECT_EQ(std::get<CSSColor>(simpleValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(simpleValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(simpleValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(simpleValue).a, 255);

auto modernSyntaxValue = parseCSSProperty<CSSColor>("hsl(180 50% 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(modernSyntaxValue));
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxValue).a, 255);

auto degreesValue = parseCSSProperty<CSSColor>("hsl(180deg, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(degreesValue));
EXPECT_EQ(std::get<CSSColor>(degreesValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(degreesValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(degreesValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(degreesValue).a, 255);

auto turnValue = parseCSSProperty<CSSColor>("hsl(0.5turn, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(turnValue));
EXPECT_EQ(std::get<CSSColor>(turnValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(turnValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(turnValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(turnValue).a, 255);

auto legacySyntaxAlphaValue =
parseCSSProperty<CSSColor>("hsl(70, 190%, 75%, 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(legacySyntaxAlphaValue));
EXPECT_EQ(std::get<CSSColor>(legacySyntaxAlphaValue).r, 234);
EXPECT_EQ(std::get<CSSColor>(legacySyntaxAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(legacySyntaxAlphaValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(legacySyntaxAlphaValue).a, 128);

auto modernSyntaxAlphaValue =
parseCSSProperty<CSSColor>("hsl(70 190% 75% 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(modernSyntaxAlphaValue));
EXPECT_EQ(std::get<CSSColor>(modernSyntaxAlphaValue).r, 234);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxAlphaValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxAlphaValue).a, 128);

auto modernSyntaxWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("hsl(70 190% 75% 0.5)");
EXPECT_TRUE(
std::holds_alternative<CSSColor>(modernSyntaxWithSolidusAlphaValue));
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithSolidusAlphaValue).r, 234);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithSolidusAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithSolidusAlphaValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithSolidusAlphaValue).a, 128);

auto percentageAlphaValue =
parseCSSProperty<CSSColor>("hsl(70 190% 75% 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(percentageAlphaValue));
EXPECT_EQ(std::get<CSSColor>(percentageAlphaValue).r, 234);
EXPECT_EQ(std::get<CSSColor>(percentageAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(percentageAlphaValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(percentageAlphaValue).a, 128);

auto hslaWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("hsla(70 190% 75% / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(hslaWithSolidusAlphaValue));
EXPECT_EQ(std::get<CSSColor>(hslaWithSolidusAlphaValue).r, 234);
EXPECT_EQ(std::get<CSSColor>(hslaWithSolidusAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(hslaWithSolidusAlphaValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(hslaWithSolidusAlphaValue).a, 128);

auto rgbLegacySyntaxWithSolidusAlphaValue =
parseCSSProperty<CSSColor>("hsl(1, 4, 5 / 0.5)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(
rgbLegacySyntaxWithSolidusAlphaValue));

auto hslaWithoutAlphaValue = parseCSSProperty<CSSColor>("hsla(70 190% 75%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(hslaWithoutAlphaValue));
EXPECT_EQ(std::get<CSSColor>(hslaWithoutAlphaValue).r, 234);
EXPECT_EQ(std::get<CSSColor>(hslaWithoutAlphaValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(hslaWithoutAlphaValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(hslaWithoutAlphaValue).a, 255);

auto surroundingWhitespaceValue =
parseCSSProperty<CSSColor>(" hsl(180, 50%, 50%) ");
EXPECT_TRUE(std::holds_alternative<CSSColor>(surroundingWhitespaceValue));
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(surroundingWhitespaceValue).a, 255);

auto modernSyntaxWithNumberComponent =
parseCSSProperty<CSSColor>("hsl(180 50 50%)");
EXPECT_TRUE(
std::holds_alternative<CSSColor>(modernSyntaxWithNumberComponent));
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithNumberComponent).r, 64);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithNumberComponent).g, 191);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithNumberComponent).b, 191);
EXPECT_EQ(std::get<CSSColor>(modernSyntaxWithNumberComponent).a, 255);

auto legacySyntaxWithNumberComponent =
parseCSSProperty<CSSColor>("hsl(180, 50, 50%)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(legacySyntaxWithNumberComponent));

auto clampedComponentValue =
parseCSSProperty<CSSColor>("hsl(360, -100%, 120%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(clampedComponentValue));
EXPECT_EQ(std::get<CSSColor>(clampedComponentValue).r, 255);
EXPECT_EQ(std::get<CSSColor>(clampedComponentValue).g, 255);
EXPECT_EQ(std::get<CSSColor>(clampedComponentValue).b, 255);
EXPECT_EQ(std::get<CSSColor>(clampedComponentValue).a, 255);

auto manyDegreesValue = parseCSSProperty<CSSColor>("hsl(540deg, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(manyDegreesValue));
EXPECT_EQ(std::get<CSSColor>(manyDegreesValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(manyDegreesValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(manyDegreesValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(manyDegreesValue).a, 255);

auto negativeDegreesValue =
parseCSSProperty<CSSColor>("hsl(-180deg, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(negativeDegreesValue));
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).r, 64);
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).g, 191);
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).b, 191);
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).a, 255);

auto valueWithSingleComponent = parseCSSProperty<CSSColor>("hsl(180deg)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueWithSingleComponent));

auto valueWithTooFewComponents =
parseCSSProperty<CSSColor>("hsl(180deg, 50%)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(valueWithTooFewComponents));

auto valueWithTooManyComponents =
parseCSSProperty<CSSColor>("hsl(70 190% 75% 0.5 0.5)");
EXPECT_TRUE(
std::holds_alternative<std::monostate>(valueWithTooManyComponents));

auto valueStartingWithComma =
parseCSSProperty<CSSColor>("hsl(,540deg, 50%, 50%)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueStartingWithComma));

auto valueEndingWithComma =
parseCSSProperty<CSSColor>("hsl(540deg, 50%, 50%,)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueEndingWithComma));
}

TEST(CSSColor, constexpr_values) {
[[maybe_unused]] constexpr auto emptyValue = parseCSSProperty<CSSColor>("");

Expand Down

0 comments on commit ab54cc4

Please sign in to comment.