diff --git a/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html b/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html
index 88b5936b271..f6003338801 100644
--- a/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html
+++ b/Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html
@@ -30,8 +30,6 @@
ctx.fillRect(0, 300, 500, 100);
// Calc
- // FIXME: The CSS parser currently chokes on calc expressions, which will
- // leave the fillStyle as it was (green).
ctx.fillStyle = "rgb(calc(infinity), 0, 0)";
ctx.fillRect(0, 400, 500, 100);
diff --git a/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png b/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png
index 2ed1dbb35ca..0430aac4309 100644
Binary files a/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png and b/Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png differ
diff --git a/Tests/LibWeb/Text/expected/canvas/fillstyle.txt b/Tests/LibWeb/Text/expected/canvas/fillstyle.txt
index b342d5b0d5b..df993731f86 100644
--- a/Tests/LibWeb/Text/expected/canvas/fillstyle.txt
+++ b/Tests/LibWeb/Text/expected/canvas/fillstyle.txt
@@ -2,4 +2,4 @@
2. "#ff0000ff"
3. "#0000ffff"
4. "#00ff00ff"
-5. "#00ff00ff"
+5. "#ff0000ff"
diff --git a/Tests/LibWeb/Text/input/canvas/fillstyle.html b/Tests/LibWeb/Text/input/canvas/fillstyle.html
index 87a3e851d45..1b2830effdc 100644
--- a/Tests/LibWeb/Text/input/canvas/fillstyle.html
+++ b/Tests/LibWeb/Text/input/canvas/fillstyle.html
@@ -33,10 +33,8 @@
return context.fillStyle;
});
- // 5. Percentages
+ // 5. Calc, with out-of-range values
testPart(() => {
- // FIXME: The CSS parser currently chokes on calc expressions, which will
- // leave the fillStyle as it was (green).
context.fillStyle = "rgb(calc(infinity), 0, 0)";
return context.fillStyle;
});
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index 4afd145c82f..23f669384af 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -112,7 +112,12 @@ set(SOURCES
CSS/StyleValues/CounterDefinitionsStyleValue.cpp
CSS/StyleValues/CounterStyleValue.cpp
CSS/StyleValues/CSSColorValue.cpp
+ CSS/StyleValues/CSSHSL.cpp
+ CSS/StyleValues/CSSHWB.cpp
CSS/StyleValues/CSSKeywordValue.cpp
+ CSS/StyleValues/CSSOKLab.cpp
+ CSS/StyleValues/CSSOKLCH.cpp
+ CSS/StyleValues/CSSRGB.cpp
CSS/StyleValues/DisplayStyleValue.cpp
CSS/StyleValues/EasingStyleValue.cpp
CSS/StyleValues/EdgeStyleValue.cpp
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
index 186b3c6b48e..b0b233fc393 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
+++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2018-2022, Andreas Kling
* Copyright (c) 2020-2021, the SerenityOS developers.
- * Copyright (c) 2021-2024, Sam Atkins
+ * Copyright (c) 2021-2024, Sam Atkins
* Copyright (c) 2021, Tobias Christiansen
* Copyright (c) 2022, MacDue
* Copyright (c) 2024, Shannon Booth
@@ -43,7 +43,12 @@
#include
#include
#include
+#include
+#include
#include
+#include
+#include
+#include
#include
#include
#include
@@ -2622,474 +2627,454 @@ RefPtr Parser::parse_rect_value(TokenStream& toke
return RectStyleValue::create(EdgeRect { params[0], params[1], params[2], params[3] });
}
-Optional Parser::parse_rgb_color(Vector const& component_values)
+// https://www.w3.org/TR/css-color-4/#typedef-hue
+RefPtr Parser::parse_hue_value(TokenStream& tokens)
{
- u8 r_val = 0;
- u8 g_val = 0;
- u8 b_val = 0;
+ // = |
+ if (auto number = parse_number_value(tokens))
+ return number;
+ if (auto angle = parse_angle_value(tokens))
+ return angle;
- auto tokens = TokenStream { component_values };
-
- tokens.skip_whitespace();
- auto const& red = tokens.next_token();
-
- if (!red.is(Token::Type::Number)
- && !red.is(Token::Type::Percentage))
- return {};
-
- tokens.skip_whitespace();
- bool legacy_syntax = tokens.peek_token().is(Token::Type::Comma);
- if (legacy_syntax) {
- // Legacy syntax.
- tokens.next_token();
- tokens.skip_whitespace();
-
- auto const& green = tokens.next_token();
- tokens.skip_whitespace();
-
- tokens.next_token();
- tokens.skip_whitespace();
-
- auto const& blue = tokens.next_token();
-
- if (red.is(Token::Type::Percentage)) {
- // Percentage components.
- if (!green.is(Token::Type::Percentage) || !blue.is(Token::Type::Percentage))
- return {};
-
- r_val = lround(clamp(red.token().percentage() * 2.55, 0, 255));
- g_val = lround(clamp(green.token().percentage() * 2.55, 0, 255));
- b_val = lround(clamp(blue.token().percentage() * 2.55, 0, 255));
- } else {
- // Number components.
- if (!green.is(Token::Type::Number) || !blue.is(Token::Type::Number))
- return {};
-
- r_val = clamp(llroundf(red.token().number_value()), 0, 255);
- g_val = clamp(llroundf(green.token().number_value()), 0, 255);
- b_val = clamp(llroundf(blue.token().number_value()), 0, 255);
- }
- } else {
- // Modern syntax.
-
- if (red.is(Token::Type::Number)) {
- r_val = clamp(llroundf(red.token().number_value()), 0, 255);
- } else {
- r_val = lround(clamp(red.token().percentage() * 2.55, 0, 255));
- }
-
- auto const& green = tokens.next_token();
- if (green.is(Token::Type::Number)) {
- g_val = clamp(llroundf(green.token().number_value()), 0, 255);
- } else if (green.is(Token::Type::Percentage)) {
- g_val = lround(clamp(green.token().percentage() * 2.55, 0, 255));
- } else {
- return {};
- }
-
- tokens.skip_whitespace();
- auto const& blue = tokens.next_token();
- if (blue.is(Token::Type::Number)) {
- b_val = clamp(llroundf(blue.token().number_value()), 0, 255);
- } else if (blue.is(Token::Type::Percentage)) {
- b_val = lround(clamp(blue.token().percentage() * 2.55, 0, 255));
- } else {
- return {};
- }
- }
-
- u8 alpha_val = 255;
- tokens.skip_whitespace();
- if (tokens.has_next_token()) {
- auto const& separator = tokens.next_token();
- bool is_comma = separator.is(Token::Type::Comma);
- bool is_slash = separator.is_delim('/');
- if (legacy_syntax ? !is_comma : !is_slash)
- return {};
-
- tokens.skip_whitespace();
- auto const& alpha = tokens.next_token();
-
- if (alpha.is(Token::Type::Number))
- alpha_val = clamp(lround(alpha.token().number_value() * 255.0), 0, 255);
- else if (alpha.is(Token::Type::Percentage))
- alpha_val = clamp(lround(alpha.token().percentage() * 2.55), 0, 255);
- else
- return {};
-
- tokens.skip_whitespace();
- if (tokens.has_next_token())
- return {}; // should have consumed all arguments.
- }
-
- return Color(r_val, g_val, b_val, alpha_val);
+ return nullptr;
}
-Optional Parser::parse_hsl_color(Vector const& component_values)
+RefPtr Parser::parse_solidus_and_alpha_value(TokenStream& tokens)
{
- float h_val = 0.0;
- float s_val = 0.0;
- float l_val = 0.0;
+ // [ / [ | none] ]?
+ // Common to the modern-syntax color functions.
+ // TODO: Parse `none`
- auto tokens = TokenStream { component_values };
-
- tokens.skip_whitespace();
- auto const& hue = tokens.next_token();
-
- if (!hue.is(Token::Type::Number)
- && !hue.is(Token::Type::Dimension))
- return {};
-
- if (hue.is(Token::Type::Number)) {
- h_val = fmod(hue.token().number_value(), 360.0);
- } else {
- auto numeric_value = hue.token().dimension_value();
- auto unit_string = hue.token().dimension_unit();
- auto angle_type = Angle::unit_from_name(unit_string);
-
- if (!angle_type.has_value())
- return {};
-
- auto angle = Angle { numeric_value, angle_type.release_value() };
-
- h_val = fmod(angle.to_degrees(), 360.0);
- }
-
- tokens.skip_whitespace();
- bool legacy_syntax = tokens.peek_token().is(Token::Type::Comma);
- if (legacy_syntax) {
- // legacy syntax.
- tokens.next_token();
- tokens.skip_whitespace();
-
- auto const& saturation = tokens.next_token();
- if (!saturation.is(Token::Type::Percentage))
- return {};
- s_val = max(saturation.token().percentage() / 100.0, 0);
-
- tokens.skip_whitespace();
- tokens.next_token();
- tokens.skip_whitespace();
-
- auto const& lightness = tokens.next_token();
- if (!lightness.is(Token::Type::Percentage))
- return {};
- l_val = lightness.token().percentage() / 100.0;
- } else {
- // Modern syntax.
-
- auto const& saturation = tokens.next_token();
- if (saturation.is(Token::Type::Number)) {
- s_val = saturation.token().number_value() / 100.0;
- } else if (saturation.is(Token::Type::Percentage)) {
- s_val = saturation.token().percentage() / 100.0;
- } else {
- return {};
- }
- s_val = max(s_val, 0);
-
- tokens.skip_whitespace();
- auto const& lightness = tokens.next_token();
- if (lightness.is(Token::Type::Number)) {
- l_val = lightness.token().number_value() / 100.0;
- } else if (lightness.is(Token::Type::Percentage)) {
- l_val = lightness.token().percentage() / 100.0;
- } else {
- return {};
- }
- }
-
- float alpha_val = 1.0;
- tokens.skip_whitespace();
- if (tokens.has_next_token()) {
- auto const& separator = tokens.next_token();
- bool is_comma = separator.is(Token::Type::Comma);
- bool is_slash = separator.is_delim('/');
- if (legacy_syntax ? !is_comma : !is_slash)
- return {};
-
- tokens.skip_whitespace();
- auto const& alpha = tokens.next_token();
-
- if (alpha.is(Token::Type::Number))
- alpha_val = alpha.token().number_value();
- else if (alpha.is(Token::Type::Percentage))
- alpha_val = alpha.token().percentage() / 100.0;
- else
- return {};
-
- tokens.skip_whitespace();
- if (tokens.has_next_token())
- return {}; // should have consumed all arguments.
- }
-
- return Color::from_hsla(h_val, s_val, l_val, alpha_val);
-}
-
-Optional Parser::parse_hwb_color(Vector const& component_values)
-{
- float h_val = 0.0;
- float w_val = 0.0;
- float b_val = 0.0;
-
- auto tokens = TokenStream { component_values };
-
- tokens.skip_whitespace();
- auto const& hue = tokens.next_token();
-
- if (!hue.is(Token::Type::Number)
- && !hue.is(Token::Type::Dimension))
- return {};
-
- if (hue.is(Token::Type::Number)) {
- h_val = fmod(hue.token().number_value(), 360.0);
- } else {
- auto numeric_value = hue.token().dimension_value();
- auto unit_string = hue.token().dimension_unit();
- auto angle_type = Angle::unit_from_name(unit_string);
-
- if (!angle_type.has_value())
- return {};
-
- auto angle = Angle { numeric_value, angle_type.release_value() };
-
- h_val = fmod(angle.to_degrees(), 360);
- }
-
- tokens.skip_whitespace();
- auto const& whiteness = tokens.next_token();
- if (whiteness.is(Token::Type::Number)) {
- w_val = whiteness.token().number_value() / 100.0;
- } else if (whiteness.is(Token::Type::Percentage)) {
- w_val = whiteness.token().percentage() / 100.0;
- } else {
- return {};
- }
-
- tokens.skip_whitespace();
- auto const& blackness = tokens.next_token();
- if (blackness.is(Token::Type::Number)) {
- b_val = blackness.token().number_value() / 100.0;
- } else if (blackness.is(Token::Type::Percentage)) {
- b_val = blackness.token().percentage() / 100.0;
- } else {
- return {};
- }
-
- float alpha_val = 1.0;
- tokens.skip_whitespace();
- if (tokens.has_next_token()) {
- auto const& separator = tokens.next_token();
- if (!separator.is_delim('/'))
- return {};
-
- tokens.skip_whitespace();
- auto const& alpha = tokens.next_token();
-
- if (alpha.is(Token::Type::Number))
- alpha_val = alpha.token().number_value();
- else if (alpha.is(Token::Type::Percentage))
- alpha_val = alpha.token().percentage() / 100.0;
- else
- return {};
-
- tokens.skip_whitespace();
- if (tokens.has_next_token())
- return {}; // should have consumed all arguments.
- }
-
- if (w_val + b_val >= 1.0f) {
- u8 gray = clamp(llroundf((w_val / (w_val + b_val)) * 255), 0, 255);
- return Color(gray, gray, gray, clamp(llroundf(alpha_val * 255), 0, 255));
- }
-
- float value = 1 - b_val;
- float saturation = 1 - (w_val / value);
- return Color::from_hsv(h_val, saturation, value).with_opacity(alpha_val);
-}
-
-Optional Parser::parse_oklab_color(Vector const& component_values)
-{
- float L_val = 0.0;
- float a_val = 0.0;
- float b_val = 0.0;
-
- auto tokens = TokenStream { component_values };
-
- tokens.skip_whitespace();
- auto const& lightness = tokens.next_token();
- if (lightness.is(Token::Type::Number)) {
- L_val = lightness.token().number_value();
- } else if (lightness.is(Token::Type::Percentage)) {
- L_val = lightness.token().percentage() / 100.0;
- } else {
- return {};
- }
- L_val = clamp(L_val, 0.0, 1.0);
-
- tokens.skip_whitespace();
- auto const& a = tokens.next_token();
- if (a.is(Token::Type::Number)) {
- a_val = a.token().number_value();
- } else if (a.is(Token::Type::Percentage)) {
- a_val = a.token().percentage() / 100.0 * 0.4;
- } else {
- return {};
- }
-
- tokens.skip_whitespace();
- auto const& b = tokens.next_token();
- if (b.is(Token::Type::Number)) {
- b_val = b.token().number_value();
- } else if (a.is(Token::Type::Percentage)) {
- b_val = b.token().percentage() / 100.0 * 0.4;
- } else {
- return {};
- }
-
- float alpha_val = 1.0;
- tokens.skip_whitespace();
- if (tokens.has_next_token()) {
- auto const& separator = tokens.next_token();
- if (!separator.is_delim('/'))
- return {};
-
- tokens.skip_whitespace();
- auto const& alpha = tokens.next_token();
-
- if (alpha.is(Token::Type::Number))
- alpha_val = alpha.token().number_value();
- else if (alpha.is(Token::Type::Percentage))
- alpha_val = alpha.token().percentage() / 100.0;
- else
- return {};
-
- tokens.skip_whitespace();
- if (tokens.has_next_token())
- return {}; // should have consumed all arguments.
- }
-
- return Color::from_oklab(L_val, a_val, b_val, alpha_val);
-}
-
-Optional Parser::parse_oklch_color(Vector const& component_values)
-{
- float L_val = 0.0;
- float c_val = 0.0;
- float h_val = 0.0;
-
- auto tokens = TokenStream { component_values };
-
- tokens.skip_whitespace();
- auto const& lightness = tokens.next_token();
- if (lightness.is(Token::Type::Number)) {
- L_val = lightness.token().number_value();
- } else if (lightness.is(Token::Type::Percentage)) {
- L_val = lightness.token().percentage() / 100.0;
- } else {
- return {};
- }
- L_val = clamp(L_val, 0.0, 1.0);
-
- tokens.skip_whitespace();
- auto const& chroma = tokens.next_token();
- if (chroma.is(Token::Type::Number)) {
- c_val = chroma.token().number_value();
- } else if (chroma.is(Token::Type::Percentage)) {
- c_val = chroma.token().percentage() / 100.0 * 0.4;
- } else {
- return {};
- }
- c_val = max(c_val, 0.0);
-
- tokens.skip_whitespace();
- auto const& hue = tokens.next_token();
-
- if (!hue.is(Token::Type::Number)
- && !hue.is(Token::Type::Dimension))
- return {};
-
- if (hue.is(Token::Type::Number)) {
- h_val = static_cast(hue.token().number_value()) * AK::Pi / 180;
- } else {
- auto numeric_value = hue.token().dimension_value();
- auto unit_string = hue.token().dimension_unit();
- auto angle_type = Angle::unit_from_name(unit_string);
-
- if (!angle_type.has_value())
- return {};
-
- auto angle = Angle { numeric_value, angle_type.release_value() };
-
- h_val = angle.to_radians();
- }
-
- float alpha_val = 1.0;
- tokens.skip_whitespace();
- if (tokens.has_next_token()) {
- auto const& separator = tokens.next_token();
- if (!separator.is_delim('/'))
- return {};
-
- tokens.skip_whitespace();
- auto const& alpha = tokens.next_token();
-
- if (alpha.is(Token::Type::Number))
- alpha_val = alpha.token().number_value();
- else if (alpha.is(Token::Type::Percentage))
- alpha_val = alpha.token().percentage() / 100.0;
- else
- return {};
-
- tokens.skip_whitespace();
- if (tokens.has_next_token())
- return {}; // should have consumed all arguments.
- }
-
- return Color::from_oklab(L_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val);
-}
-
-Optional Parser::parse_color(TokenStream& tokens)
-{
auto transaction = tokens.begin_transaction();
- auto commit_if_valid = [&](Optional color) {
- if (color.has_value())
- transaction.commit();
- return color;
- };
+ tokens.skip_whitespace();
+ if (!tokens.next_token().is_delim('/'))
+ return {};
+ tokens.skip_whitespace();
+ auto alpha = parse_number_percentage_value(tokens);
+ if (!alpha)
+ return {};
+ tokens.skip_whitespace();
+ transaction.commit();
+ return alpha;
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-rgb
+RefPtr Parser::parse_rgb_color_value(TokenStream& outer_tokens)
+{
+ // rgb() = [ | ]
+ // rgba() = [ | ]
+ // = rgb( #{3} , ? ) |
+ // rgb( #{3} , ? )
+ // = rgba( #{3} , ? ) |
+ // rgba( #{3} , ? )
+ // = rgb(
+ // [ | | none]{3}
+ // [ / [ | none] ]? )
+ // = rgba(
+ // [ | | none]{3}
+ // [ / [ | none] ]? )
+ // TODO: Handle none values
+
+ auto transaction = outer_tokens.begin_transaction();
+ outer_tokens.skip_whitespace();
+
+ auto& function_token = outer_tokens.next_token();
+ if (!function_token.is_function("rgb"sv) && !function_token.is_function("rgba"sv))
+ return {};
+
+ RefPtr red;
+ RefPtr green;
+ RefPtr blue;
+ RefPtr alpha;
+
+ auto inner_tokens = TokenStream { function_token.function().values() };
+ inner_tokens.skip_whitespace();
+
+ red = parse_number_percentage_value(inner_tokens);
+ if (!red)
+ return {};
+
+ inner_tokens.skip_whitespace();
+ bool legacy_syntax = inner_tokens.peek_token().is(Token::Type::Comma);
+ if (legacy_syntax) {
+ // Legacy syntax
+ // #{3} , ?
+ // | #{3} , ?
+ // So, r/g/b can be numbers or percentages, as long as they're all the same type.
+
+ inner_tokens.next_token(); // comma
+ inner_tokens.skip_whitespace();
+
+ green = parse_number_percentage_value(inner_tokens);
+ if (!green)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (!inner_tokens.next_token().is(Token::Type::Comma))
+ return {};
+ inner_tokens.skip_whitespace();
+
+ blue = parse_number_percentage_value(inner_tokens);
+ if (!blue)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ // Try and read comma and alpha
+ if (!inner_tokens.next_token().is(Token::Type::Comma))
+ return {};
+ inner_tokens.skip_whitespace();
+
+ alpha = parse_number_percentage_value(inner_tokens);
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token())
+ return {};
+ }
+
+ // Verify we're all percentages or all numbers
+ auto is_percentage = [](CSSStyleValue const& style_value) {
+ return style_value.is_percentage()
+ || (style_value.is_calculated() && style_value.as_calculated().resolves_to_percentage());
+ };
+ bool red_is_percentage = is_percentage(*red);
+ bool green_is_percentage = is_percentage(*green);
+ bool blue_is_percentage = is_percentage(*blue);
+ if (red_is_percentage != green_is_percentage || red_is_percentage != blue_is_percentage)
+ return {};
+
+ } else {
+ // Modern syntax
+ // [ | | none]{3} [ / [ | none] ]?
+
+ green = parse_number_percentage_value(inner_tokens);
+ if (!green)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ blue = parse_number_percentage_value(inner_tokens);
+ if (!blue)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ alpha = parse_solidus_and_alpha_value(inner_tokens);
+ if (!alpha || inner_tokens.has_next_token())
+ return {};
+ }
+ }
+
+ if (!alpha)
+ alpha = NumberStyleValue::create(1);
+
+ transaction.commit();
+ return CSSRGB::create(red.release_nonnull(), green.release_nonnull(), blue.release_nonnull(), alpha.release_nonnull());
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-hsl
+RefPtr Parser::parse_hsl_color_value(TokenStream& outer_tokens)
+{
+ // hsl() = [ | ]
+ // hsla() = [ | ]
+ // = hsl(
+ // [ | none]
+ // [ | | none]
+ // [ | | none]
+ // [ / [ | none] ]? )
+ // = hsla(
+ // [ | none]
+ // [ | | none]
+ // [ | | none]
+ // [ / [ | none] ]? )
+ // = hsl( , , , ? )
+ // = hsla( , , , ? )
+ // TODO: Handle none values
+
+ auto transaction = outer_tokens.begin_transaction();
+ outer_tokens.skip_whitespace();
+
+ auto& function_token = outer_tokens.next_token();
+ if (!function_token.is_function("hsl"sv) && !function_token.is_function("hsla"sv))
+ return {};
+
+ RefPtr h;
+ RefPtr s;
+ RefPtr l;
+ RefPtr alpha;
+
+ auto inner_tokens = TokenStream { function_token.function().values() };
+ inner_tokens.skip_whitespace();
+
+ h = parse_hue_value(inner_tokens);
+ if (!h)
+ return {};
+
+ inner_tokens.skip_whitespace();
+ bool legacy_syntax = inner_tokens.peek_token().is(Token::Type::Comma);
+ if (legacy_syntax) {
+ // Legacy syntax
+ // , , , ?
+ (void)inner_tokens.next_token(); // comma
+ inner_tokens.skip_whitespace();
+
+ s = parse_percentage_value(inner_tokens);
+ if (!s)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (!inner_tokens.next_token().is(Token::Type::Comma))
+ return {};
+ inner_tokens.skip_whitespace();
+
+ l = parse_percentage_value(inner_tokens);
+ if (!l)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ // Try and read comma and alpha
+ if (!inner_tokens.next_token().is(Token::Type::Comma))
+ return {};
+ inner_tokens.skip_whitespace();
+
+ alpha = parse_number_percentage_value(inner_tokens);
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token())
+ return {};
+ }
+ } else {
+ // Modern syntax
+ // [ | none]
+ // [ | | none]
+ // [ | | none]
+ // [ / [ | none] ]?
+
+ s = parse_number_percentage_value(inner_tokens);
+ if (!s)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ l = parse_number_percentage_value(inner_tokens);
+ if (!l)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ alpha = parse_solidus_and_alpha_value(inner_tokens);
+ if (!alpha || inner_tokens.has_next_token())
+ return {};
+ }
+ }
+
+ if (!alpha)
+ alpha = NumberStyleValue::create(1);
+
+ transaction.commit();
+ return CSSHSL::create(h.release_nonnull(), s.release_nonnull(), l.release_nonnull(), alpha.release_nonnull());
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-hwb
+RefPtr Parser::parse_hwb_color_value(TokenStream& outer_tokens)
+{
+ // hwb() = hwb(
+ // [ | none]
+ // [ | | none]
+ // [ | | none]
+ // [ / [ | none] ]? )
+
+ auto transaction = outer_tokens.begin_transaction();
+ outer_tokens.skip_whitespace();
+
+ auto& function_token = outer_tokens.next_token();
+ if (!function_token.is_function("hwb"sv))
+ return {};
+
+ RefPtr h;
+ RefPtr w;
+ RefPtr b;
+ RefPtr alpha;
+
+ auto inner_tokens = TokenStream { function_token.function().values() };
+ inner_tokens.skip_whitespace();
+
+ h = parse_hue_value(inner_tokens);
+ if (!h)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ w = parse_number_percentage_value(inner_tokens);
+ if (!w)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ b = parse_number_percentage_value(inner_tokens);
+ if (!b)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ alpha = parse_solidus_and_alpha_value(inner_tokens);
+ if (!alpha || inner_tokens.has_next_token())
+ return {};
+ }
+
+ if (!alpha)
+ alpha = NumberStyleValue::create(1);
+
+ transaction.commit();
+ return CSSHWB::create(h.release_nonnull(), w.release_nonnull(), b.release_nonnull(), alpha.release_nonnull());
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-oklab
+RefPtr Parser::parse_oklab_color_value(TokenStream& outer_tokens)
+{
+ // oklab() = oklab( [ | | none]
+ // [ | | none]
+ // [ | | none]
+ // [ / [ | none] ]? )
+
+ auto transaction = outer_tokens.begin_transaction();
+ outer_tokens.skip_whitespace();
+
+ auto& function_token = outer_tokens.next_token();
+ if (!function_token.is_function("oklab"sv))
+ return {};
+
+ RefPtr l;
+ RefPtr a;
+ RefPtr b;
+ RefPtr alpha;
+
+ auto inner_tokens = TokenStream { function_token.function().values() };
+ inner_tokens.skip_whitespace();
+
+ l = parse_number_percentage_value(inner_tokens);
+ if (!l)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ a = parse_number_percentage_value(inner_tokens);
+ if (!a)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ b = parse_number_percentage_value(inner_tokens);
+ if (!b)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ alpha = parse_solidus_and_alpha_value(inner_tokens);
+ if (!alpha || inner_tokens.has_next_token())
+ return {};
+ }
+
+ if (!alpha)
+ alpha = NumberStyleValue::create(1);
+
+ transaction.commit();
+ return CSSOKLab::create(l.release_nonnull(), a.release_nonnull(), b.release_nonnull(), alpha.release_nonnull());
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-oklch
+RefPtr Parser::parse_oklch_color_value(TokenStream& outer_tokens)
+{
+ // oklch() = oklch( [ | | none]
+ // [ | | none]
+ // [ | none]
+ // [ / [ | none] ]? )
+
+ auto transaction = outer_tokens.begin_transaction();
+ outer_tokens.skip_whitespace();
+
+ auto& function_token = outer_tokens.next_token();
+ if (!function_token.is_function("oklch"sv))
+ return {};
+
+ RefPtr l;
+ RefPtr c;
+ RefPtr h;
+ RefPtr alpha;
+
+ auto inner_tokens = TokenStream { function_token.function().values() };
+ inner_tokens.skip_whitespace();
+
+ l = parse_number_percentage_value(inner_tokens);
+ if (!l)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ c = parse_number_percentage_value(inner_tokens);
+ if (!c)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ h = parse_hue_value(inner_tokens);
+ if (!h)
+ return {};
+ inner_tokens.skip_whitespace();
+
+ if (inner_tokens.has_next_token()) {
+ alpha = parse_solidus_and_alpha_value(inner_tokens);
+ if (!alpha || inner_tokens.has_next_token())
+ return {};
+ }
+
+ if (!alpha)
+ alpha = NumberStyleValue::create(1);
+
+ transaction.commit();
+ return CSSOKLCH::create(l.release_nonnull(), c.release_nonnull(), h.release_nonnull(), alpha.release_nonnull());
+}
+
+// https://www.w3.org/TR/css-color-4/#color-syntax
+RefPtr Parser::parse_color_value(TokenStream& tokens)
+{
+
+ // Keywords: | | currentColor
+ {
+ auto transaction = tokens.begin_transaction();
+ if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) {
+ transaction.commit();
+ return keyword;
+ }
+ }
+
+ // Functions
+ if (auto rgb = parse_rgb_color_value(tokens))
+ return rgb;
+ if (auto hsl = parse_hsl_color_value(tokens))
+ return hsl;
+ if (auto hwb = parse_hwb_color_value(tokens))
+ return hwb;
+ if (auto oklab = parse_oklab_color_value(tokens))
+ return oklab;
+ if (auto oklch = parse_oklch_color_value(tokens))
+ return oklch;
+
+ auto transaction = tokens.begin_transaction();
tokens.skip_whitespace();
auto component_value = tokens.next_token();
- // https://www.w3.org/TR/css-color-4/
if (component_value.is(Token::Type::Ident)) {
auto ident = component_value.token().ident();
auto color = Color::from_string(ident);
if (color.has_value()) {
transaction.commit();
- return color;
+ return CSSColorValue::create_from_color(color.release_value());
}
// Otherwise, fall through to the hashless-hex-color case
- } else if (component_value.is(Token::Type::Hash)) {
+ }
+
+ if (component_value.is(Token::Type::Hash)) {
auto color = Color::from_string(MUST(String::formatted("#{}", component_value.token().hash_value())));
- return commit_if_valid(color);
- } else if (component_value.is_function()) {
- auto const& function = component_value.function();
- auto const& values = function.values();
- auto const function_name = function.name();
-
- if (function_name.equals_ignoring_ascii_case("rgb"sv) || function_name.equals_ignoring_ascii_case("rgba"sv))
- return commit_if_valid(parse_rgb_color(values));
- if (function_name.equals_ignoring_ascii_case("hsl"sv) || function_name.equals_ignoring_ascii_case("hsla"sv))
- return commit_if_valid(parse_hsl_color(values));
- if (function_name.equals_ignoring_ascii_case("hwb"sv))
- return commit_if_valid(parse_hwb_color(values));
- if (function_name.equals_ignoring_ascii_case("oklab"sv))
- return commit_if_valid(parse_oklab_color(values));
- if (function_name.equals_ignoring_ascii_case("oklch"sv))
- return commit_if_valid(parse_oklch_color(values));
-
+ if (color.has_value()) {
+ transaction.commit();
+ return CSSColorValue::create_from_color(color.release_value());
+ }
return {};
}
@@ -3149,26 +3134,16 @@ Optional Parser::parse_color(TokenStream& tokens)
}
// 6. Return the concatenation of "#" (U+0023) and serialization.
- return commit_if_valid(Color::from_string(MUST(String::formatted("#{}", serialization))));
+ auto color = Color::from_string(MUST(String::formatted("#{}", serialization)));
+ if (color.has_value()) {
+ transaction.commit();
+ return CSSColorValue::create_from_color(color.release_value());
+ }
}
return {};
}
-RefPtr Parser::parse_color_value(TokenStream& tokens)
-{
- if (auto color = parse_color(tokens); color.has_value())
- return CSSColorValue::create_from_color(color.value());
-
- auto transaction = tokens.begin_transaction();
- if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) {
- transaction.commit();
- return keyword;
- }
-
- return nullptr;
-}
-
// https://drafts.csswg.org/css-lists-3/#counter-functions
RefPtr Parser::parse_counter_value(TokenStream& tokens)
{
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h
index 12957da0535..90810db2f1f 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h
+++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
- * Copyright (c) 2021-2024, Sam Atkins
+ * Copyright (c) 2021-2024, Sam Atkins
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -244,12 +244,6 @@ private:
Optional parse_time(TokenStream&);
Optional parse_time_percentage(TokenStream&);
- Optional parse_rgb_color(Vector const&);
- Optional parse_hsl_color(Vector const&);
- Optional parse_hwb_color(Vector const&);
- Optional parse_oklab_color(Vector const&);
- Optional parse_oklch_color(Vector const&);
- Optional parse_color(TokenStream&);
Optional parse_source_size_value(TokenStream&);
Optional parse_ratio(TokenStream&);
Optional parse_unicode_range(TokenStream&);
@@ -289,6 +283,13 @@ private:
OwnPtr parse_math_function(PropertyID, Function const&);
OwnPtr parse_a_calc_function_node(Function const&);
RefPtr parse_keyword_value(TokenStream&);
+ RefPtr parse_hue_value(TokenStream&);
+ RefPtr parse_solidus_and_alpha_value(TokenStream&);
+ RefPtr parse_rgb_color_value(TokenStream&);
+ RefPtr parse_hsl_color_value(TokenStream&);
+ RefPtr parse_hwb_color_value(TokenStream&);
+ RefPtr parse_oklab_color_value(TokenStream&);
+ RefPtr parse_oklch_color_value(TokenStream&);
RefPtr parse_color_value(TokenStream&);
RefPtr parse_counter_value(TokenStream&);
enum class AllowReversed {
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
index 9a163739f4c..85ff393c95c 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
@@ -1432,8 +1432,12 @@ static NonnullRefPtr interpolate_value(DOM::Element& elemen
switch (from.type()) {
case CSSStyleValue::Type::Angle:
return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta)));
- case CSSStyleValue::Type::Color:
- return CSSColorValue::create_from_color(interpolate_color(from.as_color().color(), to.as_color().color(), delta));
+ case CSSStyleValue::Type::Color: {
+ Optional layout_node;
+ if (auto node = element.layout_node())
+ layout_node = *node;
+ return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta));
+ }
case CSSStyleValue::Type::Integer:
return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta));
case CSSStyleValue::Type::Length: {
diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp
index ca99b9beb8d..dd156ae8581 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp
+++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp
@@ -9,32 +9,116 @@
#include "CSSColorValue.h"
#include
+#include
+#include
+#include
+#include
+#include
namespace Web::CSS {
ValueComparingNonnullRefPtr CSSColorValue::create_from_color(Color color)
{
+ auto make_rgb_color = [](Color const& color) {
+ return CSSRGB::create(
+ NumberStyleValue::create(color.red()),
+ NumberStyleValue::create(color.green()),
+ NumberStyleValue::create(color.blue()),
+ NumberStyleValue::create(color.alpha() / 255.0));
+ };
+
if (color.value() == 0) {
- static auto transparent = adopt_ref(*new (nothrow) CSSColorValue(color));
+ static auto transparent = make_rgb_color(color);
return transparent;
}
if (color == Color::from_rgb(0x000000)) {
- static auto black = adopt_ref(*new (nothrow) CSSColorValue(color));
+ static auto black = make_rgb_color(color);
return black;
}
if (color == Color::from_rgb(0xffffff)) {
- static auto white = adopt_ref(*new (nothrow) CSSColorValue(color));
+ static auto white = make_rgb_color(color);
return white;
}
- return adopt_ref(*new (nothrow) CSSColorValue(color));
+ return make_rgb_color(color);
}
-String CSSColorValue::to_string() const
+Optional CSSColorValue::resolve_hue(CSSStyleValue const& style_value)
{
- return serialize_a_srgb_value(m_color);
+ // | | none
+ auto normalized = [](double number) {
+ return fmod(number, 360.0);
+ };
+
+ if (style_value.is_number())
+ return normalized(style_value.as_number().number());
+
+ if (style_value.is_angle())
+ return normalized(style_value.as_angle().angle().to_degrees());
+
+ if (style_value.is_calculated() && style_value.as_calculated().resolves_to_angle())
+ return normalized(style_value.as_calculated().resolve_angle().value().to_degrees());
+
+ if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+ return 0;
+
+ return {};
+}
+
+Optional CSSColorValue::resolve_with_reference_value(CSSStyleValue const& style_value, float one_hundred_percent_value)
+{
+ // | | none
+ auto normalize_percentage = [one_hundred_percent_value](Percentage const& percentage) {
+ return static_cast(percentage.as_fraction()) * one_hundred_percent_value;
+ };
+
+ if (style_value.is_percentage())
+ return normalize_percentage(style_value.as_percentage().percentage());
+
+ if (style_value.is_number())
+ return style_value.as_number().number();
+
+ if (style_value.is_calculated()) {
+ auto const& calculated = style_value.as_calculated();
+ if (calculated.resolves_to_number())
+ return calculated.resolve_number().value();
+ if (calculated.resolves_to_percentage())
+ return normalize_percentage(calculated.resolve_percentage().value());
+ }
+
+ if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+ return 0;
+
+ return {};
+}
+
+Optional CSSColorValue::resolve_alpha(CSSStyleValue const& style_value)
+{
+ // | | none
+ auto normalized = [](double number) {
+ return clamp(number, 0.0, 1.0);
+ };
+
+ if (style_value.is_number())
+ return normalized(style_value.as_number().number());
+
+ if (style_value.is_percentage())
+ return normalized(style_value.as_percentage().percentage().as_fraction());
+
+ if (style_value.is_calculated()) {
+ auto const& calculated = style_value.as_calculated();
+ if (calculated.resolves_to_number())
+ return normalized(calculated.resolve_number().value());
+ if (calculated.resolves_to_percentage())
+ return normalized(calculated.resolve_percentage().value().as_fraction());
+ }
+
+ if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+ return 0;
+
+ return {};
}
}
diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h
index 72add309faa..fc72fa29dd6 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h
+++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h
@@ -15,26 +15,35 @@
namespace Web::CSS {
// https://drafts.css-houdini.org/css-typed-om-1/#csscolorvalue
-class CSSColorValue : public StyleValueWithDefaultOperators {
+class CSSColorValue : public CSSStyleValue {
public:
static ValueComparingNonnullRefPtr create_from_color(Color color);
virtual ~CSSColorValue() override = default;
- Color color() const { return m_color; }
- virtual String to_string() const override;
virtual bool has_color() const override { return true; }
- virtual Color to_color(Optional) const override { return m_color; }
- bool properties_equal(CSSColorValue const& other) const { return m_color == other.m_color; }
+ enum class ColorType {
+ RGB,
+ HSL,
+ HWB,
+ OKLab,
+ OKLCH,
+ };
+ ColorType color_type() const { return m_color_type; }
-private:
- explicit CSSColorValue(Color color)
- : StyleValueWithDefaultOperators(Type::Color)
- , m_color(color)
+protected:
+ explicit CSSColorValue(ColorType color_type)
+ : CSSStyleValue(Type::Color)
+ , m_color_type(color_type)
{
}
- Color m_color;
+ static Optional resolve_hue(CSSStyleValue const&);
+ static Optional resolve_with_reference_value(CSSStyleValue const&, float one_hundred_percent_value);
+ static Optional resolve_alpha(CSSStyleValue const&);
+
+private:
+ ColorType m_color_type;
};
}
diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp
new file mode 100644
index 00000000000..a36ee11bb45
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2024, Sam Atkins
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSHSL.h"
+#include
+#include
+
+namespace Web::CSS {
+
+Color CSSHSL::to_color(Optional) const
+{
+ auto const h_val = resolve_hue(m_properties.h).value_or(0);
+ auto const s_val = resolve_with_reference_value(m_properties.s, 100.0).value_or(0);
+ auto const l_val = resolve_with_reference_value(m_properties.l, 100.0).value_or(0);
+ auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1);
+
+ return Color::from_hsla(h_val, s_val / 100.0f, l_val / 100.0f, alpha_val);
+}
+
+bool CSSHSL::equals(CSSStyleValue const& other) const
+{
+ if (type() != other.type())
+ return false;
+ auto const& other_color = other.as_color();
+ if (color_type() != other_color.color_type())
+ return false;
+ auto const& other_hsl = verify_cast(other_color);
+ return m_properties == other_hsl.m_properties;
+}
+
+// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
+String CSSHSL::to_string() const
+{
+ // FIXME: Do this properly, taking unresolved calculated values into account.
+ return serialize_a_srgb_value(to_color({}));
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h
new file mode 100644
index 00000000000..edf0603da42
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024, Sam Atkins
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace Web::CSS {
+
+// https://drafts.css-houdini.org/css-typed-om-1/#csshsl
+class CSSHSL final : public CSSColorValue {
+public:
+ static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr h, ValueComparingNonnullRefPtr s, ValueComparingNonnullRefPtr l, ValueComparingRefPtr alpha = {})
+ {
+ // alpha defaults to 1
+ if (!alpha)
+ return adopt_ref(*new (nothrow) CSSHSL(move(h), move(s), move(l), NumberStyleValue::create(1)));
+
+ return adopt_ref(*new (nothrow) CSSHSL(move(h), move(s), move(l), alpha.release_nonnull()));
+ }
+ virtual ~CSSHSL() override = default;
+
+ CSSStyleValue const& h() const { return *m_properties.h; }
+ CSSStyleValue const& s() const { return *m_properties.s; }
+ CSSStyleValue const& l() const { return *m_properties.l; }
+ CSSStyleValue const& alpha() const { return *m_properties.alpha; }
+
+ virtual Color to_color(Optional