From 199a931da11971676cfd434dcead115eb794f117 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 17 Aug 2023 15:27:38 +0100 Subject: [PATCH] LibWeb: Move gradient parsing code to a separate file --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../LibWeb/CSS/Parser/GradientParsing.cpp | 519 ++++++++++++++++++ .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 503 ----------------- 3 files changed, 520 insertions(+), 503 deletions(-) create mode 100644 Userland/Libraries/LibWeb/CSS/Parser/GradientParsing.cpp diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 1455e16e65..418845499f 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -53,6 +53,7 @@ set(SOURCES CSS/Parser/Declaration.cpp CSS/Parser/DeclarationOrAtRule.cpp CSS/Parser/Function.cpp + CSS/Parser/GradientParsing.cpp CSS/Parser/Helpers.cpp CSS/Parser/MediaParsing.cpp CSS/Parser/Parser.cpp diff --git a/Userland/Libraries/LibWeb/CSS/Parser/GradientParsing.cpp b/Userland/Libraries/LibWeb/CSS/Parser/GradientParsing.cpp new file mode 100644 index 0000000000..6f2e86b389 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/Parser/GradientParsing.cpp @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2018-2022, Andreas Kling + * Copyright (c) 2020-2021, the SerenityOS developers. + * Copyright (c) 2021-2023, Sam Atkins + * Copyright (c) 2021, Tobias Christiansen + * Copyright (c) 2022, MacDue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace Web::CSS::Parser { + +template +static Optional> parse_color_stop_list(auto& tokens, auto is_position, auto get_position, auto parse_color, auto parse_dimension) +{ + enum class ElementType { + Garbage, + ColorStop, + ColorHint + }; + + auto parse_color_stop_list_element = [&](TElement& element) -> ElementType { + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return ElementType::Garbage; + auto const& token = tokens.next_token(); + + RefPtr color; + Optional position; + Optional second_position; + auto dimension = parse_dimension(token); + if (dimension.has_value() && is_position(*dimension)) { + // [ ] or [] + position = get_position(*dimension); + tokens.skip_whitespace(); + // + if (!tokens.has_next_token() || tokens.peek_token().is(Token::Type::Comma)) { + element.transition_hint = typename TElement::ColorHint { *position }; + return ElementType::ColorHint; + } + // + auto maybe_color = parse_color(tokens.next_token()); + if (maybe_color.is_error() || maybe_color.value() == nullptr) + return ElementType::Garbage; + color = maybe_color.release_value(); + } else { + // [ ?] + auto maybe_color = parse_color(token); + if (maybe_color.is_error() || maybe_color.value() == nullptr) + return ElementType::Garbage; + color = maybe_color.release_value(); + tokens.skip_whitespace(); + // Allow up to [ ] (double-position color stops) + // Note: Double-position color stops only appear to be valid in this order. + for (auto stop_position : Array { &position, &second_position }) { + 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() || !is_position(*dimension)) + return ElementType::Garbage; + *stop_position = get_position(*dimension); + tokens.skip_whitespace(); + } + } + } + + element.color_stop = typename TElement::ColorStop { color, position, second_position }; + return ElementType::ColorStop; + }; + + TElement 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()) { + TElement 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 color_stops; +} + +static StringView consume_if_starts_with(StringView str, StringView start, auto found_callback) +{ + if (str.starts_with(start, CaseSensitivity::CaseInsensitive)) { + found_callback(); + return str.substring_view(start.length()); + } + return str; +} + +Optional> Parser::parse_linear_color_stop_list(TokenStream& tokens) +{ + // = + // , [ ? , ]# + return parse_color_stop_list( + tokens, + [](Dimension& dimension) { return dimension.is_length_percentage(); }, + [](Dimension& dimension) { return dimension.length_percentage(); }, + [&](auto& token) { return parse_color_value(token); }, + [&](auto& token) { return parse_dimension(token); }); +} + +Optional> Parser::parse_angular_color_stop_list(TokenStream& tokens) +{ + // = + // , [ ? , ]# + return parse_color_stop_list( + tokens, + [](Dimension& dimension) { return dimension.is_angle_percentage(); }, + [](Dimension& dimension) { return dimension.angle_percentage(); }, + [&](auto& token) { return parse_color_value(token); }, + [&](auto& token) { return parse_dimension(token); }); +} + +ErrorOr> Parser::parse_linear_gradient_function(ComponentValue const& component_value) +{ + using GradientType = LinearGradientStyleValue::GradientType; + + if (!component_value.is_function()) + return nullptr; + + GradientRepeating repeating_gradient = GradientRepeating::No; + GradientType gradient_type { GradientType::Standard }; + + auto function_name = component_value.function().name(); + + function_name = consume_if_starts_with(function_name, "-webkit-"sv, [&] { + gradient_type = GradientType::WebKit; + }); + + function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] { + repeating_gradient = GradientRepeating::Yes; + }); + + if (!function_name.equals_ignoring_ascii_case("linear-gradient"sv)) + return nullptr; + + // linear-gradient() = linear-gradient([ | to ]?, ) + + TokenStream tokens { component_value.function().values() }; + tokens.skip_whitespace(); + + if (!tokens.has_next_token()) + return nullptr; + + bool has_direction_param = true; + LinearGradientStyleValue::GradientDirection gradient_direction = gradient_type == GradientType::Standard + ? SideOrCorner::Bottom + : SideOrCorner::Top; + + auto to_side = [](StringView value) -> Optional { + if (value.equals_ignoring_ascii_case("top"sv)) + return SideOrCorner::Top; + if (value.equals_ignoring_ascii_case("bottom"sv)) + return SideOrCorner::Bottom; + if (value.equals_ignoring_ascii_case("left"sv)) + return SideOrCorner::Left; + if (value.equals_ignoring_ascii_case("right"sv)) + return SideOrCorner::Right; + return {}; + }; + + auto is_to_side_or_corner = [&](auto const& token) { + if (!token.is(Token::Type::Ident)) + return false; + if (gradient_type == GradientType::WebKit) + return to_side(token.token().ident()).has_value(); + return token.token().ident().equals_ignoring_ascii_case("to"sv); + }; + + auto const& 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 nullptr; + + gradient_direction = Angle { angle_value, angle_type.release_value() }; + } else if (is_to_side_or_corner(first_param)) { + // = [left | right] || [top | bottom] + + // Note: -webkit-linear-gradient does not include to the "to" prefix on the side or corner + if (gradient_type == GradientType::Standard) { + tokens.next_token(); + tokens.skip_whitespace(); + + if (!tokens.has_next_token()) + return nullptr; + } + + // [left | right] || [top | bottom] + auto const& first_side = tokens.next_token(); + if (!first_side.is(Token::Type::Ident)) + return nullptr; + + auto side_a = to_side(first_side.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()) { + // Convert 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 nullptr; + } else { + return nullptr; + } + } else { + has_direction_param = false; + } + + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return nullptr; + + if (has_direction_param && !tokens.next_token().is(Token::Type::Comma)) + return nullptr; + + auto color_stops = parse_linear_color_stop_list(tokens); + if (!color_stops.has_value()) + return nullptr; + + return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient); +} + +ErrorOr> Parser::parse_conic_gradient_function(ComponentValue const& component_value) +{ + if (!component_value.is_function()) + return nullptr; + + GradientRepeating repeating_gradient = GradientRepeating::No; + + auto function_name = component_value.function().name(); + + function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] { + repeating_gradient = GradientRepeating::Yes; + }); + + if (!function_name.equals_ignoring_ascii_case("conic-gradient"sv)) + return nullptr; + + TokenStream tokens { component_value.function().values() }; + tokens.skip_whitespace(); + + if (!tokens.has_next_token()) + return nullptr; + + Angle from_angle(0, Angle::Type::Deg); + PositionValue at_position = PositionValue::center(); + + // conic-gradient( [ [ from ]? [ at ]? ] || + // , ) + auto token = tokens.peek_token(); + bool got_from_angle = false; + bool got_color_interpolation_method = false; + bool got_at_position = false; + while (token.is(Token::Type::Ident)) { + auto consume_identifier = [&](auto identifier) { + auto token_string = token.token().ident(); + if (token_string.equals_ignoring_ascii_case(identifier)) { + (void)tokens.next_token(); + tokens.skip_whitespace(); + return true; + } + return false; + }; + + if (consume_identifier("from"sv)) { + // from + if (got_from_angle || got_at_position) + return nullptr; + if (!tokens.has_next_token()) + return nullptr; + + auto angle_token = tokens.next_token(); + if (!angle_token.is(Token::Type::Dimension)) + return nullptr; + float angle = angle_token.token().dimension_value(); + auto angle_unit = angle_token.token().dimension_unit(); + auto angle_type = Angle::unit_from_name(angle_unit); + if (!angle_type.has_value()) + return nullptr; + + from_angle = Angle(angle, *angle_type); + got_from_angle = true; + } else if (consume_identifier("at"sv)) { + // at + if (got_at_position) + return nullptr; + auto position = parse_position(tokens); + if (!position.has_value()) + return nullptr; + at_position = *position; + got_at_position = true; + } else if (consume_identifier("in"sv)) { + // + if (got_color_interpolation_method) + return nullptr; + dbgln("FIXME: Parse color interpolation method for conic-gradient()"); + got_color_interpolation_method = true; + } else { + break; + } + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return nullptr; + token = tokens.peek_token(); + } + + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return nullptr; + if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.next_token().is(Token::Type::Comma)) + return nullptr; + + auto color_stops = parse_angular_color_stop_list(tokens); + if (!color_stops.has_value()) + return nullptr; + + return ConicGradientStyleValue::create(from_angle, at_position, move(*color_stops), repeating_gradient); +} + +ErrorOr> Parser::parse_radial_gradient_function(ComponentValue const& component_value) +{ + using EndingShape = RadialGradientStyleValue::EndingShape; + using Extent = RadialGradientStyleValue::Extent; + using CircleSize = RadialGradientStyleValue::CircleSize; + using EllipseSize = RadialGradientStyleValue::EllipseSize; + using Size = RadialGradientStyleValue::Size; + + if (!component_value.is_function()) + return nullptr; + + auto repeating_gradient = GradientRepeating::No; + + auto function_name = component_value.function().name(); + + function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] { + repeating_gradient = GradientRepeating::Yes; + }); + + if (!function_name.equals_ignoring_ascii_case("radial-gradient"sv)) + return nullptr; + + TokenStream tokens { component_value.function().values() }; + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return nullptr; + + bool expect_comma = false; + + auto commit_value = [&](auto value, T&... transactions) { + (transactions.commit(), ...); + return value; + }; + + // radial-gradient( [ || ]? [ at ]? , ) + + Size size = Extent::FarthestCorner; + EndingShape ending_shape = EndingShape::Circle; + PositionValue at_position = PositionValue::center(); + + auto parse_ending_shape = [&]() -> Optional { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto& token = tokens.next_token(); + if (!token.is(Token::Type::Ident)) + return {}; + auto ident = token.token().ident(); + if (ident.equals_ignoring_ascii_case("circle"sv)) + return commit_value(EndingShape::Circle, transaction); + if (ident.equals_ignoring_ascii_case("ellipse"sv)) + return commit_value(EndingShape::Ellipse, transaction); + return {}; + }; + + auto parse_extent_keyword = [](StringView keyword) -> Optional { + if (keyword.equals_ignoring_ascii_case("closest-corner"sv)) + return Extent::ClosestCorner; + if (keyword.equals_ignoring_ascii_case("closest-side"sv)) + return Extent::ClosestSide; + if (keyword.equals_ignoring_ascii_case("farthest-corner"sv)) + return Extent::FarthestCorner; + if (keyword.equals_ignoring_ascii_case("farthest-side"sv)) + return Extent::FarthestSide; + return {}; + }; + + auto parse_size = [&]() -> Optional { + // = + // | + // | + // {2} + auto transaction_size = tokens.begin_transaction(); + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return {}; + auto& token = tokens.next_token(); + if (token.is(Token::Type::Ident)) { + auto extent = parse_extent_keyword(token.token().ident()); + if (!extent.has_value()) + return {}; + return commit_value(*extent, transaction_size); + } + auto first_dimension = parse_dimension(token); + if (!first_dimension.has_value()) + return {}; + if (!first_dimension->is_length_percentage()) + return {}; + auto transaction_second_dimension = tokens.begin_transaction(); + tokens.skip_whitespace(); + if (tokens.has_next_token()) { + auto& second_token = tokens.next_token(); + auto second_dimension = parse_dimension(second_token); + if (second_dimension.has_value() && second_dimension->is_length_percentage()) + return commit_value(EllipseSize { first_dimension->length_percentage(), second_dimension->length_percentage() }, + transaction_size, transaction_second_dimension); + } + if (first_dimension->is_length()) + return commit_value(CircleSize { first_dimension->length() }, transaction_size); + return {}; + }; + + { + // [ || ]? + auto maybe_ending_shape = parse_ending_shape(); + auto maybe_size = parse_size(); + if (!maybe_ending_shape.has_value() && maybe_size.has_value()) + maybe_ending_shape = parse_ending_shape(); + if (maybe_size.has_value()) { + size = *maybe_size; + expect_comma = true; + } + if (maybe_ending_shape.has_value()) { + expect_comma = true; + ending_shape = *maybe_ending_shape; + if (ending_shape == EndingShape::Circle && size.has()) + return nullptr; + if (ending_shape == EndingShape::Ellipse && size.has()) + return nullptr; + } else { + ending_shape = size.has() ? EndingShape::Circle : EndingShape::Ellipse; + } + } + + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return nullptr; + + auto& token = tokens.peek_token(); + if (token.is(Token::Type::Ident) && token.token().ident().equals_ignoring_ascii_case("at"sv)) { + (void)tokens.next_token(); + auto position = parse_position(tokens); + if (!position.has_value()) + return nullptr; + at_position = *position; + expect_comma = true; + } + + tokens.skip_whitespace(); + if (!tokens.has_next_token()) + return nullptr; + if (expect_comma && !tokens.next_token().is(Token::Type::Comma)) + return nullptr; + + // + auto color_stops = parse_linear_color_stop_list(tokens); + if (!color_stops.has_value()) + return nullptr; + + return RadialGradientStyleValue::create(ending_shape, size, at_position, move(*color_stops), repeating_gradient); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 2695e874b7..52e0879b74 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -42,7 +42,6 @@ #include #include #include -#include #include #include #include @@ -65,7 +64,6 @@ #include #include #include -#include #include #include #include @@ -74,7 +72,6 @@ #include #include #include -#include #include #include #include @@ -1198,506 +1195,6 @@ ErrorOr> Parser::parse_url_value(ComponentValue const& compon return URLStyleValue::create(*url); } -template -static Optional> parse_color_stop_list(auto& tokens, auto is_position, auto get_position, auto parse_color, auto parse_dimension) -{ - enum class ElementType { - Garbage, - ColorStop, - ColorHint - }; - - auto parse_color_stop_list_element = [&](TElement& element) -> ElementType { - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return ElementType::Garbage; - auto const& token = tokens.next_token(); - - RefPtr color; - Optional position; - Optional second_position; - auto dimension = parse_dimension(token); - if (dimension.has_value() && is_position(*dimension)) { - // [ ] or [] - position = get_position(*dimension); - tokens.skip_whitespace(); - // - if (!tokens.has_next_token() || tokens.peek_token().is(Token::Type::Comma)) { - element.transition_hint = typename TElement::ColorHint { *position }; - return ElementType::ColorHint; - } - // - auto maybe_color = parse_color(tokens.next_token()); - if (maybe_color.is_error() || maybe_color.value() == nullptr) - return ElementType::Garbage; - color = maybe_color.release_value(); - } else { - // [ ?] - auto maybe_color = parse_color(token); - if (maybe_color.is_error() || maybe_color.value() == nullptr) - return ElementType::Garbage; - color = maybe_color.release_value(); - tokens.skip_whitespace(); - // Allow up to [ ] (double-position color stops) - // Note: Double-position color stops only appear to be valid in this order. - for (auto stop_position : Array { &position, &second_position }) { - 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() || !is_position(*dimension)) - return ElementType::Garbage; - *stop_position = get_position(*dimension); - tokens.skip_whitespace(); - } - } - } - - element.color_stop = typename TElement::ColorStop { color, position, second_position }; - return ElementType::ColorStop; - }; - - TElement 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()) { - TElement 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 color_stops; -} - -Optional> Parser::parse_linear_color_stop_list(TokenStream& tokens) -{ - // = - // , [ ? , ]# - return parse_color_stop_list( - tokens, - [](Dimension& dimension) { return dimension.is_length_percentage(); }, - [](Dimension& dimension) { return dimension.length_percentage(); }, - [&](auto& token) { return parse_color_value(token); }, - [&](auto& token) { return parse_dimension(token); }); -} - -Optional> Parser::parse_angular_color_stop_list(TokenStream& tokens) -{ - // = - // , [ ? , ]# - return parse_color_stop_list( - tokens, - [](Dimension& dimension) { return dimension.is_angle_percentage(); }, - [](Dimension& dimension) { return dimension.angle_percentage(); }, - [&](auto& token) { return parse_color_value(token); }, - [&](auto& token) { return parse_dimension(token); }); -} - -static StringView consume_if_starts_with(StringView str, StringView start, auto found_callback) -{ - if (str.starts_with(start, CaseSensitivity::CaseInsensitive)) { - found_callback(); - return str.substring_view(start.length()); - } - return str; -} - -ErrorOr> Parser::parse_linear_gradient_function(ComponentValue const& component_value) -{ - using GradientType = LinearGradientStyleValue::GradientType; - - if (!component_value.is_function()) - return nullptr; - - GradientRepeating repeating_gradient = GradientRepeating::No; - GradientType gradient_type { GradientType::Standard }; - - auto function_name = component_value.function().name(); - - function_name = consume_if_starts_with(function_name, "-webkit-"sv, [&] { - gradient_type = GradientType::WebKit; - }); - - function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] { - repeating_gradient = GradientRepeating::Yes; - }); - - if (!function_name.equals_ignoring_ascii_case("linear-gradient"sv)) - return nullptr; - - // linear-gradient() = linear-gradient([ | to ]?, ) - - TokenStream tokens { component_value.function().values() }; - tokens.skip_whitespace(); - - if (!tokens.has_next_token()) - return nullptr; - - bool has_direction_param = true; - LinearGradientStyleValue::GradientDirection gradient_direction = gradient_type == GradientType::Standard - ? SideOrCorner::Bottom - : SideOrCorner::Top; - - auto to_side = [](StringView value) -> Optional { - if (value.equals_ignoring_ascii_case("top"sv)) - return SideOrCorner::Top; - if (value.equals_ignoring_ascii_case("bottom"sv)) - return SideOrCorner::Bottom; - if (value.equals_ignoring_ascii_case("left"sv)) - return SideOrCorner::Left; - if (value.equals_ignoring_ascii_case("right"sv)) - return SideOrCorner::Right; - return {}; - }; - - auto is_to_side_or_corner = [&](auto const& token) { - if (!token.is(Token::Type::Ident)) - return false; - if (gradient_type == GradientType::WebKit) - return to_side(token.token().ident()).has_value(); - return token.token().ident().equals_ignoring_ascii_case("to"sv); - }; - - auto const& 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 nullptr; - - gradient_direction = Angle { angle_value, angle_type.release_value() }; - } else if (is_to_side_or_corner(first_param)) { - // = [left | right] || [top | bottom] - - // Note: -webkit-linear-gradient does not include to the "to" prefix on the side or corner - if (gradient_type == GradientType::Standard) { - tokens.next_token(); - tokens.skip_whitespace(); - - if (!tokens.has_next_token()) - return nullptr; - } - - // [left | right] || [top | bottom] - auto const& first_side = tokens.next_token(); - if (!first_side.is(Token::Type::Ident)) - return nullptr; - - auto side_a = to_side(first_side.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()) { - // Convert 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 nullptr; - } else { - return nullptr; - } - } else { - has_direction_param = false; - } - - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return nullptr; - - if (has_direction_param && !tokens.next_token().is(Token::Type::Comma)) - return nullptr; - - auto color_stops = parse_linear_color_stop_list(tokens); - if (!color_stops.has_value()) - return nullptr; - - return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient); -} - -ErrorOr> Parser::parse_conic_gradient_function(ComponentValue const& component_value) -{ - if (!component_value.is_function()) - return nullptr; - - GradientRepeating repeating_gradient = GradientRepeating::No; - - auto function_name = component_value.function().name(); - - function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] { - repeating_gradient = GradientRepeating::Yes; - }); - - if (!function_name.equals_ignoring_ascii_case("conic-gradient"sv)) - return nullptr; - - TokenStream tokens { component_value.function().values() }; - tokens.skip_whitespace(); - - if (!tokens.has_next_token()) - return nullptr; - - Angle from_angle(0, Angle::Type::Deg); - PositionValue at_position = PositionValue::center(); - - // conic-gradient( [ [ from ]? [ at ]? ] || - // , ) - auto token = tokens.peek_token(); - bool got_from_angle = false; - bool got_color_interpolation_method = false; - bool got_at_position = false; - while (token.is(Token::Type::Ident)) { - auto consume_identifier = [&](auto identifier) { - auto token_string = token.token().ident(); - if (token_string.equals_ignoring_ascii_case(identifier)) { - (void)tokens.next_token(); - tokens.skip_whitespace(); - return true; - } - return false; - }; - - if (consume_identifier("from"sv)) { - // from - if (got_from_angle || got_at_position) - return nullptr; - if (!tokens.has_next_token()) - return nullptr; - - auto angle_token = tokens.next_token(); - if (!angle_token.is(Token::Type::Dimension)) - return nullptr; - float angle = angle_token.token().dimension_value(); - auto angle_unit = angle_token.token().dimension_unit(); - auto angle_type = Angle::unit_from_name(angle_unit); - if (!angle_type.has_value()) - return nullptr; - - from_angle = Angle(angle, *angle_type); - got_from_angle = true; - } else if (consume_identifier("at"sv)) { - // at - if (got_at_position) - return nullptr; - auto position = parse_position(tokens); - if (!position.has_value()) - return nullptr; - at_position = *position; - got_at_position = true; - } else if (consume_identifier("in"sv)) { - // - if (got_color_interpolation_method) - return nullptr; - dbgln("FIXME: Parse color interpolation method for conic-gradient()"); - got_color_interpolation_method = true; - } else { - break; - } - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return nullptr; - token = tokens.peek_token(); - } - - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return nullptr; - if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.next_token().is(Token::Type::Comma)) - return nullptr; - - auto color_stops = parse_angular_color_stop_list(tokens); - if (!color_stops.has_value()) - return nullptr; - - return ConicGradientStyleValue::create(from_angle, at_position, move(*color_stops), repeating_gradient); -} - -ErrorOr> Parser::parse_radial_gradient_function(ComponentValue const& component_value) -{ - using EndingShape = RadialGradientStyleValue::EndingShape; - using Extent = RadialGradientStyleValue::Extent; - using CircleSize = RadialGradientStyleValue::CircleSize; - using EllipseSize = RadialGradientStyleValue::EllipseSize; - using Size = RadialGradientStyleValue::Size; - - if (!component_value.is_function()) - return nullptr; - - auto repeating_gradient = GradientRepeating::No; - - auto function_name = component_value.function().name(); - - function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] { - repeating_gradient = GradientRepeating::Yes; - }); - - if (!function_name.equals_ignoring_ascii_case("radial-gradient"sv)) - return nullptr; - - TokenStream tokens { component_value.function().values() }; - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return nullptr; - - bool expect_comma = false; - - auto commit_value = [&](auto value, T&... transactions) { - (transactions.commit(), ...); - return value; - }; - - // radial-gradient( [ || ]? [ at ]? , ) - - Size size = Extent::FarthestCorner; - EndingShape ending_shape = EndingShape::Circle; - PositionValue at_position = PositionValue::center(); - - auto parse_ending_shape = [&]() -> Optional { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto& token = tokens.next_token(); - if (!token.is(Token::Type::Ident)) - return {}; - auto ident = token.token().ident(); - if (ident.equals_ignoring_ascii_case("circle"sv)) - return commit_value(EndingShape::Circle, transaction); - if (ident.equals_ignoring_ascii_case("ellipse"sv)) - return commit_value(EndingShape::Ellipse, transaction); - return {}; - }; - - auto parse_extent_keyword = [](StringView keyword) -> Optional { - if (keyword.equals_ignoring_ascii_case("closest-corner"sv)) - return Extent::ClosestCorner; - if (keyword.equals_ignoring_ascii_case("closest-side"sv)) - return Extent::ClosestSide; - if (keyword.equals_ignoring_ascii_case("farthest-corner"sv)) - return Extent::FarthestCorner; - if (keyword.equals_ignoring_ascii_case("farthest-side"sv)) - return Extent::FarthestSide; - return {}; - }; - - auto parse_size = [&]() -> Optional { - // = - // | - // | - // {2} - auto transaction_size = tokens.begin_transaction(); - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return {}; - auto& token = tokens.next_token(); - if (token.is(Token::Type::Ident)) { - auto extent = parse_extent_keyword(token.token().ident()); - if (!extent.has_value()) - return {}; - return commit_value(*extent, transaction_size); - } - auto first_dimension = parse_dimension(token); - if (!first_dimension.has_value()) - return {}; - if (!first_dimension->is_length_percentage()) - return {}; - auto transaction_second_dimension = tokens.begin_transaction(); - tokens.skip_whitespace(); - if (tokens.has_next_token()) { - auto& second_token = tokens.next_token(); - auto second_dimension = parse_dimension(second_token); - if (second_dimension.has_value() && second_dimension->is_length_percentage()) - return commit_value(EllipseSize { first_dimension->length_percentage(), second_dimension->length_percentage() }, - transaction_size, transaction_second_dimension); - } - if (first_dimension->is_length()) - return commit_value(CircleSize { first_dimension->length() }, transaction_size); - return {}; - }; - - { - // [ || ]? - auto maybe_ending_shape = parse_ending_shape(); - auto maybe_size = parse_size(); - if (!maybe_ending_shape.has_value() && maybe_size.has_value()) - maybe_ending_shape = parse_ending_shape(); - if (maybe_size.has_value()) { - size = *maybe_size; - expect_comma = true; - } - if (maybe_ending_shape.has_value()) { - expect_comma = true; - ending_shape = *maybe_ending_shape; - if (ending_shape == EndingShape::Circle && size.has()) - return nullptr; - if (ending_shape == EndingShape::Ellipse && size.has()) - return nullptr; - } else { - ending_shape = size.has() ? EndingShape::Circle : EndingShape::Ellipse; - } - } - - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return nullptr; - - auto& token = tokens.peek_token(); - if (token.is(Token::Type::Ident) && token.token().ident().equals_ignoring_ascii_case("at"sv)) { - (void)tokens.next_token(); - auto position = parse_position(tokens); - if (!position.has_value()) - return nullptr; - at_position = *position; - expect_comma = true; - } - - tokens.skip_whitespace(); - if (!tokens.has_next_token()) - return nullptr; - if (expect_comma && !tokens.next_token().is(Token::Type::Comma)) - return nullptr; - - // - auto color_stops = parse_linear_color_stop_list(tokens); - if (!color_stops.has_value()) - return nullptr; - - return RadialGradientStyleValue::create(ending_shape, size, at_position, move(*color_stops), repeating_gradient); -} - Optional Parser::parse_position(TokenStream& tokens, PositionValue initial_value) { auto transaction = tokens.begin_transaction();