From ae6c0258a4993fa720d7b4827abd9bcdae4e4aa6 Mon Sep 17 00:00:00 2001 From: MacDue Date: Tue, 12 Jul 2022 00:15:05 +0100 Subject: [PATCH] LibWeb: Parse the linear-gradient() CSS function This should parse linear-gradient()s as defined in the W3 spec https://drafts.csswg.org/css-images/#linear-gradients. Note: This currently cannot parse multi-position color stops, these are shown on MDN and work in Firefox and Chrome, though do not seem to be defined in the spec. See: https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#gradient_with_multi-position_color_stops P.s. This also allows -webkit-linear-gradient for compatibility. --- .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 187 +++++++++++++++++- Userland/Libraries/LibWeb/CSS/Parser/Parser.h | 2 + Userland/Libraries/LibWeb/CSS/StyleValue.h | 1 + 3 files changed, 187 insertions(+), 3 deletions(-) diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 30dc5d0e07..dcf18049ef 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -3,6 +3,7 @@ * Copyright (c) 2020-2021, the SerenityOS developers. * Copyright (c) 2021-2022, Sam Atkins * Copyright (c) 2021, Tobias Christiansen + * Copyright (c) 2022, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ @@ -2355,6 +2356,187 @@ Optional Parser::parse_url_function(ComponentValue const& component_val return {}; } +RefPtr Parser::parse_linear_gradient_function(ComponentValue const& component_value) +{ + if (!component_value.is_function()) + return {}; + + auto function_name = component_value.function().name(); + if (!function_name.is_one_of_ignoring_case("linear-gradient"sv, "-webkit-linear-gradient"sv)) + return {}; + + // linear-gradient() = linear-gradient([ | to ]?, ) + + TokenStream tokens { component_value.function().values() }; + tokens.skip_whitespace(); + + if (!tokens.has_next_token()) + return {}; + + bool has_direction_param = true; + LinearGradientStyleValue::GradientDirection gradient_direction = SideOrCorner::Bottom; + + auto& first_param = tokens.peek_token(); + if (first_param.is(Token::Type::Dimension)) { + // + tokens.next_token(); + float angle_value = first_param.token().dimension_value(); + auto unit_string = first_param.token().dimension_unit(); + auto angle_type = Angle::unit_from_name(unit_string); + + if (!angle_type.has_value()) + return {}; + + gradient_direction = Angle { angle_value, angle_type.release_value() }; + } else if (first_param.is(Token::Type::Ident) && first_param.token().ident().equals_ignoring_case("to"sv)) { + // = [left | right] || [top | bottom] + tokens.next_token(); + tokens.skip_whitespace(); + + auto to_side = [](StringView value) -> Optional { + if (value.equals_ignoring_case("top"sv)) + return SideOrCorner::Top; + if (value.equals_ignoring_case("bottom"sv)) + return SideOrCorner::Bottom; + if (value.equals_ignoring_case("left"sv)) + return SideOrCorner::Left; + if (value.equals_ignoring_case("right"sv)) + return SideOrCorner::Right; + return {}; + }; + + if (!tokens.has_next_token()) + return {}; + + // [left | right] || [top | bottom] + auto& second_param = tokens.next_token(); + if (!second_param.is(Token::Type::Ident)) + return {}; + auto side_a = to_side(second_param.token().ident()); + tokens.skip_whitespace(); + Optional side_b; + if (tokens.has_next_token() && tokens.peek_token().is(Token::Type::Ident)) + side_b = to_side(tokens.next_token().token().ident()); + + if (side_a.has_value() && !side_b.has_value()) { + gradient_direction = *side_a; + } else if (side_a.has_value() && side_b.has_value()) { + // Covert two sides to a corner + if (to_underlying(*side_b) < to_underlying(*side_a)) + swap(side_a, side_b); + if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Left) + gradient_direction = SideOrCorner::TopLeft; + else if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Right) + gradient_direction = SideOrCorner::TopRight; + else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Left) + gradient_direction = SideOrCorner::BottomLeft; + else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Right) + gradient_direction = SideOrCorner::BottomRight; + else + return {}; + } else { + return {}; + } + } else { + has_direction_param = false; + } + + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return {}; + + if (has_direction_param && !tokens.next_token().is(Token::Type::Comma)) + return {}; + + // = + // , [ ? , ]# + + // FIXME: Support multi-position color stops + // https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#gradient_with_multi-position_color_stops + // These are shown on MDN... Though do not appear in the W3 spec(?) + + enum class ElementType { + Garbage, + ColorStop, + ColorHint + }; + + auto parse_color_stop_list_element = [&](ColorStopListElement& element) -> ElementType { + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return ElementType::Garbage; + auto& token = tokens.next_token(); + + Gfx::Color color; + Optional length; + auto dimension = parse_dimension(token); + if (dimension.has_value() && dimension->is_length_percentage()) { + // [ ] or [] + length = dimension->length_percentage(); + tokens.skip_whitespace(); + // + if (!tokens.has_next_token() || tokens.peek_token().is(Token::Type::Comma)) { + element.transition_hint = GradientColorHint { *length }; + return ElementType::ColorHint; + } + // + auto maybe_color = parse_color(tokens.next_token()); + if (!maybe_color.has_value()) + return ElementType::Garbage; + color = *maybe_color; + } else { + // [ ?] + auto maybe_color = parse_color(token); + if (!maybe_color.has_value()) + return ElementType::Garbage; + color = *maybe_color; + tokens.skip_whitespace(); + if (tokens.has_next_token() && !tokens.peek_token().is(Token::Type::Comma)) { + auto token = tokens.next_token(); + auto dimension = parse_dimension(token); + if (!dimension.has_value() || !dimension->is_length_percentage()) + return ElementType::Garbage; + length = dimension->length_percentage(); + } + } + + element.color_stop = GradientColorStop { color, length }; + return ElementType::ColorStop; + }; + + ColorStopListElement first_element {}; + if (parse_color_stop_list_element(first_element) != ElementType::ColorStop) + return {}; + + if (!tokens.has_next_token()) + return {}; + + Vector color_stops { first_element }; + while (tokens.has_next_token()) { + ColorStopListElement list_element {}; + tokens.skip_whitespace(); + if (!tokens.next_token().is(Token::Type::Comma)) + return {}; + auto element_type = parse_color_stop_list_element(list_element); + if (element_type == ElementType::ColorHint) { + // , + tokens.skip_whitespace(); + if (!tokens.next_token().is(Token::Type::Comma)) + return {}; + // Note: This fills in the color stop on the same list_element as the color hint (it does not overwrite it). + if (parse_color_stop_list_element(list_element) != ElementType::ColorStop) + return {}; + } else if (element_type == ElementType::ColorStop) { + // + } else { + return {}; + } + color_stops.append(list_element); + } + + return LinearGradientStyleValue::create(gradient_direction, move(color_stops)); +} + RefPtr Parser::convert_to_rule(NonnullRefPtr rule) { if (rule->is_at_rule()) { @@ -3253,9 +3435,8 @@ RefPtr Parser::parse_image_value(ComponentValue const& component_val auto url = parse_url_function(component_value, AllowedDataUrlType::Image); if (url.has_value()) return ImageStyleValue::create(url.value()); - // FIXME: Handle gradients. - - return {}; + // FIXME: Implement other kinds of gradient + return parse_linear_gradient_function(component_value); } template diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index 79aaab3460..a59c17155a 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -317,6 +317,8 @@ private: }; Optional parse_url_function(ComponentValue const&, AllowedDataUrlType = AllowedDataUrlType::None); + RefPtr parse_linear_gradient_function(ComponentValue const&); + ParseErrorOr> parse_css_value(PropertyID, TokenStream&); RefPtr parse_css_value(ComponentValue const&); RefPtr parse_builtin_value(ComponentValue const&); diff --git a/Userland/Libraries/LibWeb/CSS/StyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValue.h index d42a51723f..a9c987099e 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValue.h @@ -58,6 +58,7 @@ enum class FlexBasis { Auto, }; +// Note: The sides must be before the corners in this enum (as this order is used in parsing). enum class SideOrCorner { Top, Bottom,