ladybird/Userland/Libraries/LibWeb/CSS/Parser/GradientParsing.cpp
Sam Atkins e0be17e4fb LibWeb/CSS: Rewrite CSS Parser core methods according to new spec
CSS Syntax 3 (https://drafts.csswg.org/css-syntax) has changed
significantly since we implemented it a couple of years ago. Just about
every parsing algorithm has been rewritten in terms of the new token
stream concept, and to support nested styles. As all of those
algorithms call into each other, this is an unfortunately chonky diff.

As part of this, the transitory types (Declaration, Function, AtRule...)
have been rewritten. That's both because we have new requirements of
what they should be and contain, and also because the spec asks us to
create and then gradually modify them in place, which is easier if they
are plain structs.
2024-10-14 08:08:37 +02:00

529 lines
20 KiB
C++

/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.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.discard_whitespace();
if (!tokens.has_next_token())
return ElementType::Garbage;
RefPtr<CSSStyleValue> color;
Optional<typename TElement::PositionType> position;
Optional<typename TElement::PositionType> second_position;
if (auto dimension = parse_dimension(tokens.next_token()); dimension.has_value() && is_position(*dimension)) {
// [<T-percentage> <color>] or [<T-percentage>]
position = get_position(*dimension);
tokens.discard_a_token(); // dimension
tokens.discard_whitespace();
// <T-percentage>
if (!tokens.has_next_token() || tokens.next_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.discard_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.next_token().is(Token::Type::Comma)) {
auto dimension = parse_dimension(tokens.consume_a_token());
if (!dimension.has_value() || !is_position(*dimension))
return ElementType::Garbage;
*stop_position = get_position(*dimension);
tokens.discard_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.discard_whitespace();
if (!tokens.consume_a_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.discard_whitespace();
if (!tokens.consume_a_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<CSSStyleValue> Parser::parse_linear_gradient_function(TokenStream<ComponentValue>& outer_tokens)
{
using GradientType = LinearGradientStyleValue::GradientType;
auto transaction = outer_tokens.begin_transaction();
auto& component_value = outer_tokens.consume_a_token();
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().value };
tokens.discard_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.next_token();
if (first_param.is(Token::Type::Dimension)) {
// <angle>
tokens.discard_a_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.discard_a_token();
tokens.discard_whitespace();
if (!tokens.has_next_token())
return nullptr;
}
// [left | right] || [top | bottom]
auto const& first_side = tokens.consume_a_token();
if (!first_side.is(Token::Type::Ident))
return nullptr;
auto side_a = to_side(first_side.token().ident());
tokens.discard_whitespace();
Optional<SideOrCorner> side_b;
if (tokens.has_next_token() && tokens.next_token().is(Token::Type::Ident))
side_b = to_side(tokens.consume_a_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.discard_whitespace();
if (!tokens.has_next_token())
return nullptr;
if (has_direction_param && !tokens.consume_a_token().is(Token::Type::Comma))
return nullptr;
auto color_stops = parse_linear_color_stop_list(tokens);
if (!color_stops.has_value())
return nullptr;
transaction.commit();
return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient);
}
RefPtr<CSSStyleValue> Parser::parse_conic_gradient_function(TokenStream<ComponentValue>& outer_tokens)
{
auto transaction = outer_tokens.begin_transaction();
auto& component_value = outer_tokens.consume_a_token();
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().value };
tokens.discard_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.next_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)) {
tokens.discard_a_token();
tokens.discard_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.consume_a_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.discard_whitespace();
if (!tokens.has_next_token())
return nullptr;
token = tokens.next_token();
}
tokens.discard_whitespace();
if (!tokens.has_next_token())
return nullptr;
if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.consume_a_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();
transaction.commit();
return ConicGradientStyleValue::create(from_angle, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
}
RefPtr<CSSStyleValue> Parser::parse_radial_gradient_function(TokenStream<ComponentValue>& outer_tokens)
{
using EndingShape = RadialGradientStyleValue::EndingShape;
using Extent = RadialGradientStyleValue::Extent;
using CircleSize = RadialGradientStyleValue::CircleSize;
using EllipseSize = RadialGradientStyleValue::EllipseSize;
using Size = RadialGradientStyleValue::Size;
auto transaction = outer_tokens.begin_transaction();
auto& component_value = outer_tokens.consume_a_token();
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().value };
tokens.discard_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.discard_whitespace();
auto& token = tokens.consume_a_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.discard_whitespace();
if (!tokens.has_next_token())
return {};
if (tokens.next_token().is(Token::Type::Ident)) {
auto extent = parse_extent_keyword(tokens.consume_a_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.discard_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.discard_whitespace();
if (!tokens.has_next_token())
return nullptr;
auto& token = tokens.next_token();
if (token.is_ident("at"sv)) {
tokens.discard_a_token();
auto position = parse_position_value(tokens);
if (!position)
return nullptr;
at_position = position;
expect_comma = true;
}
tokens.discard_whitespace();
if (!tokens.has_next_token())
return nullptr;
if (expect_comma && !tokens.consume_a_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();
transaction.commit();
return RadialGradientStyleValue::create(ending_shape, size, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
}
}