123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- /*
- * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
- * Copyright (c) 2020-2021, the SerenityOS developers.
- * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
- * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
- * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
- #include <AK/Debug.h>
- #include <LibWeb/CSS/Parser/Parser.h>
- #include <LibWeb/CSS/StyleValues/ConicGradientStyleValue.h>
- #include <LibWeb/CSS/StyleValues/LinearGradientStyleValue.h>
- #include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
- #include <LibWeb/CSS/StyleValues/RadialGradientStyleValue.h>
- namespace Web::CSS::Parser {
- template<typename TElement>
- Optional<Vector<TElement>> Parser::parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto is_position, auto get_position)
- {
- 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;
- RefPtr<StyleValue> color;
- Optional<typename TElement::PositionType> position;
- Optional<typename TElement::PositionType> second_position;
- if (auto dimension = parse_dimension(tokens.peek_token()); dimension.has_value() && is_position(*dimension)) {
- // [<T-percentage> <color>] or [<T-percentage>]
- position = get_position(*dimension);
- (void)tokens.next_token(); // dimension
- tokens.skip_whitespace();
- // <T-percentage>
- if (!tokens.has_next_token() || tokens.peek_token().is(Token::Type::Comma)) {
- element.transition_hint = typename TElement::ColorHint { *position };
- return ElementType::ColorHint;
- }
- // <T-percentage> <color>
- auto maybe_color = parse_color_value(tokens);
- if (!maybe_color)
- return ElementType::Garbage;
- color = maybe_color.release_nonnull();
- } else {
- // [<color> <T-percentage>?]
- auto maybe_color = parse_color_value(tokens);
- if (!maybe_color)
- return ElementType::Garbage;
- color = maybe_color.release_nonnull();
- tokens.skip_whitespace();
- // Allow up to [<color> <T-percentage> <T-percentage>] (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 dimension = parse_dimension(tokens.next_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<TElement> 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) {
- // <color-hint>, <color-stop>
- 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) {
- // <color-stop>
- } 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<Vector<LinearColorStopListElement>> Parser::parse_linear_color_stop_list(TokenStream<ComponentValue>& tokens)
- {
- // <color-stop-list> =
- // <linear-color-stop> , [ <linear-color-hint>? , <linear-color-stop> ]#
- return parse_color_stop_list<LinearColorStopListElement>(
- tokens,
- [](Dimension& dimension) { return dimension.is_length_percentage(); },
- [](Dimension& dimension) { return dimension.length_percentage(); });
- }
- Optional<Vector<AngularColorStopListElement>> Parser::parse_angular_color_stop_list(TokenStream<ComponentValue>& tokens)
- {
- // <angular-color-stop-list> =
- // <angular-color-stop> , [ <angular-color-hint>? , <angular-color-stop> ]#
- return parse_color_stop_list<AngularColorStopListElement>(
- tokens,
- [](Dimension& dimension) { return dimension.is_angle_percentage(); },
- [](Dimension& dimension) { return dimension.angle_percentage(); });
- }
- RefPtr<StyleValue> 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().bytes_as_string_view();
- 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([ <angle> | to <side-or-corner> ]?, <color-stop-list>)
- 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<SideOrCorner> {
- 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)) {
- // <angle>
- tokens.next_token();
- auto 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)) {
- // <side-or-corner> = [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<SideOrCorner> 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);
- }
- RefPtr<StyleValue> 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().bytes_as_string_view();
- 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);
- RefPtr<PositionStyleValue> at_position;
- // conic-gradient( [ [ from <angle> ]? [ at <position> ]? ] ||
- // <color-interpolation-method> , <angular-color-stop-list> )
- 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 <angle>
- 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;
- auto 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 <position>
- if (got_at_position)
- return nullptr;
- auto position = parse_position_value(tokens);
- if (!position)
- return nullptr;
- at_position = position;
- got_at_position = true;
- } else if (consume_identifier("in"sv)) {
- // <color-interpolation-method>
- 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;
- if (!at_position)
- at_position = PositionStyleValue::create_center();
- return ConicGradientStyleValue::create(from_angle, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
- }
- RefPtr<StyleValue> 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().bytes_as_string_view();
- 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 = [&]<typename... T>(auto value, T&... transactions) {
- (transactions.commit(), ...);
- return value;
- };
- // radial-gradient( [ <ending-shape> || <size> ]? [ at <position> ]? , <color-stop-list> )
- Size size = Extent::FarthestCorner;
- EndingShape ending_shape = EndingShape::Circle;
- RefPtr<PositionStyleValue> at_position;
- auto parse_ending_shape = [&]() -> Optional<EndingShape> {
- 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<Extent> {
- 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<Size> {
- // <size> =
- // <extent-keyword> |
- // <length [0,∞]> |
- // <length-percentage [0,∞]>{2}
- auto transaction_size = tokens.begin_transaction();
- tokens.skip_whitespace();
- if (!tokens.has_next_token())
- return {};
- if (tokens.peek_token().is(Token::Type::Ident)) {
- auto extent = parse_extent_keyword(tokens.next_token().token().ident());
- if (!extent.has_value())
- return {};
- return commit_value(*extent, transaction_size);
- }
- auto first_radius = parse_length_percentage(tokens);
- if (!first_radius.has_value())
- return {};
- auto transaction_second_dimension = tokens.begin_transaction();
- tokens.skip_whitespace();
- if (tokens.has_next_token()) {
- auto second_radius = parse_length_percentage(tokens);
- if (second_radius.has_value())
- return commit_value(EllipseSize { first_radius.release_value(), second_radius.release_value() },
- transaction_size, transaction_second_dimension);
- }
- // FIXME: Support calculated lengths
- if (first_radius->is_length())
- return commit_value(CircleSize { first_radius->length() }, transaction_size);
- return {};
- };
- {
- // [ <ending-shape> || <size> ]?
- 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<EllipseSize>())
- return nullptr;
- if (ending_shape == EndingShape::Ellipse && size.has<CircleSize>())
- return nullptr;
- } else {
- ending_shape = size.has<CircleSize>() ? EndingShape::Circle : EndingShape::Ellipse;
- }
- }
- tokens.skip_whitespace();
- if (!tokens.has_next_token())
- return nullptr;
- auto& token = tokens.peek_token();
- if (token.is_ident("at"sv)) {
- (void)tokens.next_token();
- auto position = parse_position_value(tokens);
- if (!position)
- 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;
- // <color-stop-list>
- auto color_stops = parse_linear_color_stop_list(tokens);
- if (!color_stops.has_value())
- return nullptr;
- if (!at_position)
- at_position = PositionStyleValue::create_center();
- return RadialGradientStyleValue::create(ending_shape, size, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
- }
- }
|