Everywhere: Hoist the Libraries folder to the top-level

This commit is contained in:
Timothy Flynn 2024-11-09 12:25:08 -05:00 committed by Andreas Kling
commit 93712b24bf
Notes: github-actions[bot] 2024-11-10 11:51:52 +00:00
4547 changed files with 104 additions and 113 deletions

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Parser/ComponentValue.h>
namespace Web::CSS::Parser {
ComponentValue::ComponentValue(Token token)
: m_value(token)
{
}
ComponentValue::ComponentValue(Function&& function)
: m_value(move(function))
{
}
ComponentValue::ComponentValue(SimpleBlock&& block)
: m_value(move(block))
{
}
ComponentValue::~ComponentValue() = default;
bool ComponentValue::is_function(StringView name) const
{
return is_function() && function().name.equals_ignoring_ascii_case(name);
}
bool ComponentValue::is_ident(StringView ident) const
{
return is(Token::Type::Ident) && token().ident().equals_ignoring_ascii_case(ident);
}
String ComponentValue::to_string() const
{
return m_value.visit([](auto const& it) { return it.to_string(); });
}
String ComponentValue::to_debug_string() const
{
return m_value.visit(
[](Token const& token) {
return MUST(String::formatted("Token: {}", token.to_debug_string()));
},
[](SimpleBlock const& block) {
return MUST(String::formatted("Block: {}", block.to_string()));
},
[](Function const& function) {
return MUST(String::formatted("Function: {}", function.to_string()));
});
}
String ComponentValue::original_source_text() const
{
return m_value.visit([](auto const& it) { return it.original_source_text(); });
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <AK/RefPtr.h>
#include <LibWeb/CSS/Parser/Token.h>
#include <LibWeb/CSS/Parser/Types.h>
namespace Web::CSS::Parser {
// https://drafts.csswg.org/css-syntax/#component-value
class ComponentValue {
AK_MAKE_DEFAULT_COPYABLE(ComponentValue);
AK_MAKE_DEFAULT_MOVABLE(ComponentValue);
public:
ComponentValue(Token);
explicit ComponentValue(Function&&);
explicit ComponentValue(SimpleBlock&&);
~ComponentValue();
bool is_block() const { return m_value.has<SimpleBlock>(); }
SimpleBlock const& block() const { return m_value.get<SimpleBlock>(); }
bool is_function() const { return m_value.has<Function>(); }
bool is_function(StringView name) const;
Function const& function() const { return m_value.get<Function>(); }
bool is_token() const { return m_value.has<Token>(); }
bool is(Token::Type type) const { return is_token() && token().is(type); }
bool is_delim(u32 delim) const { return is(Token::Type::Delim) && token().delim() == delim; }
bool is_ident(StringView ident) const;
Token const& token() const { return m_value.get<Token>(); }
operator Token() const { return m_value.get<Token>(); }
String to_string() const;
String to_debug_string() const;
String original_source_text() const;
private:
Variant<Token, Function, SimpleBlock> m_value;
};
}
template<>
struct AK::Formatter<Web::CSS::Parser::ComponentValue> : Formatter<StringView> {
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::Parser::ComponentValue const& component_value)
{
return Formatter<StringView>::format(builder, component_value.to_string());
}
};

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Variant.h>
#include <LibWeb/CSS/Angle.h>
#include <LibWeb/CSS/Flex.h>
#include <LibWeb/CSS/Frequency.h>
#include <LibWeb/CSS/Length.h>
#include <LibWeb/CSS/Percentage.h>
#include <LibWeb/CSS/PercentageOr.h>
#include <LibWeb/CSS/Resolution.h>
#include <LibWeb/CSS/Time.h>
namespace Web::CSS::Parser {
class Dimension {
public:
Dimension(Angle&& value)
: m_value(move(value))
{
}
Dimension(Flex&& value)
: m_value(move(value))
{
}
Dimension(Frequency&& value)
: m_value(move(value))
{
}
Dimension(Length&& value)
: m_value(move(value))
{
}
Dimension(Percentage&& value)
: m_value(move(value))
{
}
Dimension(Resolution&& value)
: m_value(move(value))
{
}
Dimension(Time&& value)
: m_value(move(value))
{
}
bool is_angle() const { return m_value.has<Angle>(); }
Angle angle() const { return m_value.get<Angle>(); }
bool is_angle_percentage() const { return is_angle() || is_percentage(); }
AnglePercentage angle_percentage() const
{
if (is_angle())
return angle();
return percentage();
}
bool is_flex() const { return m_value.has<Flex>(); }
Flex flex() const { return m_value.get<Flex>(); }
bool is_frequency() const { return m_value.has<Frequency>(); }
Frequency frequency() const { return m_value.get<Frequency>(); }
bool is_frequency_percentage() const { return is_frequency() || is_percentage(); }
FrequencyPercentage frequency_percentage() const
{
if (is_frequency())
return frequency();
return percentage();
}
bool is_length() const { return m_value.has<Length>(); }
Length length() const { return m_value.get<Length>(); }
bool is_length_percentage() const { return is_length() || is_percentage(); }
LengthPercentage length_percentage() const
{
if (is_length())
return length();
return percentage();
}
bool is_percentage() const { return m_value.has<Percentage>(); }
Percentage percentage() const { return m_value.get<Percentage>(); }
bool is_resolution() const { return m_value.has<Resolution>(); }
Resolution resolution() const { return m_value.get<Resolution>(); }
bool is_time() const { return m_value.has<Time>(); }
Time time() const { return m_value.get<Time>(); }
bool is_time_percentage() const { return is_time() || is_percentage(); }
TimePercentage time_percentage() const
{
if (is_time())
return time();
return percentage();
}
private:
Variant<Angle, Flex, Frequency, Length, Percentage, Resolution, Time> m_value;
};
}

View file

@ -0,0 +1,530 @@
/*
* 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 <AK/NonnullRawPtr.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> )
NonnullRawPtr<ComponentValue const> 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 const& 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);
}
}

View file

@ -0,0 +1,97 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2023, the SerenityOS developers.
* Copyright (c) 2021-2024, 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 <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSRuleList.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/Parser/Parser.h>
namespace Web {
CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingContext const& context, StringView css, Optional<URL::URL> location)
{
if (css.is_empty()) {
auto rule_list = CSS::CSSRuleList::create_empty(context.realm());
auto media_list = CSS::MediaList::create(context.realm(), {});
auto style_sheet = CSS::CSSStyleSheet::create(context.realm(), rule_list, media_list, location);
style_sheet->set_source_text({});
return style_sheet;
}
auto* style_sheet = CSS::Parser::Parser::create(context, css).parse_as_css_stylesheet(location);
// FIXME: Avoid this copy
style_sheet->set_source_text(MUST(String::from_utf8(css)));
return style_sheet;
}
CSS::ElementInlineCSSStyleDeclaration* parse_css_style_attribute(CSS::Parser::ParsingContext const& context, StringView css, DOM::Element& element)
{
if (css.is_empty())
return CSS::ElementInlineCSSStyleDeclaration::create(element, {}, {});
return CSS::Parser::Parser::create(context, css).parse_as_style_attribute(element);
}
RefPtr<CSS::CSSStyleValue> parse_css_value(CSS::Parser::ParsingContext const& context, StringView string, CSS::PropertyID property_id)
{
if (string.is_empty())
return nullptr;
return CSS::Parser::Parser::create(context, string).parse_as_css_value(property_id);
}
CSS::CSSRule* parse_css_rule(CSS::Parser::ParsingContext const& context, StringView css_text)
{
return CSS::Parser::Parser::create(context, css_text).parse_as_css_rule();
}
Optional<CSS::SelectorList> parse_selector(CSS::Parser::ParsingContext const& context, StringView selector_text)
{
return CSS::Parser::Parser::create(context, selector_text).parse_as_selector();
}
Optional<CSS::SelectorList> parse_selector_for_nested_style_rule(CSS::Parser::ParsingContext const& context, StringView selector_text)
{
auto parser = CSS::Parser::Parser::create(context, selector_text);
auto maybe_selectors = parser.parse_as_relative_selector(CSS::Parser::Parser::SelectorParsingMode::Standard);
if (!maybe_selectors.has_value())
return {};
return adapt_nested_relative_selector_list(*maybe_selectors);
}
Optional<CSS::Selector::PseudoElement> parse_pseudo_element_selector(CSS::Parser::ParsingContext const& context, StringView selector_text)
{
return CSS::Parser::Parser::create(context, selector_text).parse_as_pseudo_element_selector();
}
RefPtr<CSS::MediaQuery> parse_media_query(CSS::Parser::ParsingContext const& context, StringView string)
{
return CSS::Parser::Parser::create(context, string).parse_as_media_query();
}
Vector<NonnullRefPtr<CSS::MediaQuery>> parse_media_query_list(CSS::Parser::ParsingContext const& context, StringView string)
{
return CSS::Parser::Parser::create(context, string).parse_as_media_query_list();
}
RefPtr<CSS::Supports> parse_css_supports(CSS::Parser::ParsingContext const& context, StringView string)
{
if (string.is_empty())
return {};
return CSS::Parser::Parser::create(context, string).parse_as_supports();
}
Optional<CSS::StyleProperty> parse_css_supports_condition(CSS::Parser::ParsingContext const& context, StringView string)
{
if (string.is_empty())
return {};
return CSS::Parser::Parser::create(context, string).parse_as_supports_condition();
}
}

View file

@ -0,0 +1,650 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.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/CSSMediaRule.h>
#include <LibWeb/CSS/CSSNestedDeclarations.h>
#include <LibWeb/CSS/CalculatedOr.h>
#include <LibWeb/CSS/MediaList.h>
#include <LibWeb/CSS/MediaQuery.h>
#include <LibWeb/CSS/Parser/Parser.h>
namespace Web::CSS::Parser {
Vector<NonnullRefPtr<MediaQuery>> Parser::parse_as_media_query_list()
{
return parse_a_media_query_list(m_token_stream);
}
template<typename T>
Vector<NonnullRefPtr<MediaQuery>> Parser::parse_a_media_query_list(TokenStream<T>& tokens)
{
// https://www.w3.org/TR/mediaqueries-4/#mq-list
// AD-HOC: Ignore whitespace-only queries
// to make `@media {..}` equivalent to `@media all {..}`
tokens.discard_whitespace();
if (!tokens.has_next_token())
return {};
auto comma_separated_lists = parse_a_comma_separated_list_of_component_values(tokens);
AK::Vector<NonnullRefPtr<MediaQuery>> media_queries;
for (auto& media_query_parts : comma_separated_lists) {
auto stream = TokenStream(media_query_parts);
media_queries.append(parse_media_query(stream));
}
return media_queries;
}
RefPtr<MediaQuery> Parser::parse_as_media_query()
{
// https://www.w3.org/TR/cssom-1/#parse-a-media-query
auto media_query_list = parse_as_media_query_list();
if (media_query_list.is_empty())
return MediaQuery::create_not_all();
if (media_query_list.size() == 1)
return media_query_list.first();
return nullptr;
}
// `<media-query>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-query
NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<ComponentValue>& tokens)
{
// `<media-query> = <media-condition>
// | [ not | only ]? <media-type> [ and <media-condition-without-or> ]?`
// `[ not | only ]?`, Returns whether to negate the query
auto parse_initial_modifier = [](auto& tokens) -> Optional<bool> {
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("not"sv)) {
transaction.commit();
return true;
}
if (ident.equals_ignoring_ascii_case("only"sv)) {
transaction.commit();
return false;
}
return {};
};
auto invalid_media_query = [&]() {
// "A media query that does not match the grammar in the previous section must be replaced by `not all`
// during parsing." - https://www.w3.org/TR/mediaqueries-5/#error-handling
if constexpr (CSS_PARSER_DEBUG) {
dbgln("Invalid media query:");
tokens.dump_all_tokens();
}
return MediaQuery::create_not_all();
};
auto media_query = MediaQuery::create();
tokens.discard_whitespace();
// `<media-condition>`
if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) {
tokens.discard_whitespace();
if (tokens.has_next_token())
return invalid_media_query();
media_query->m_media_condition = move(media_condition);
return media_query;
}
// `[ not | only ]?`
if (auto modifier = parse_initial_modifier(tokens); modifier.has_value()) {
media_query->m_negated = modifier.value();
tokens.discard_whitespace();
}
// `<media-type>`
if (auto media_type = parse_media_type(tokens); media_type.has_value()) {
media_query->m_media_type = media_type.value();
tokens.discard_whitespace();
} else {
return invalid_media_query();
}
if (!tokens.has_next_token())
return media_query;
// `[ and <media-condition-without-or> ]?`
if (auto const& maybe_and = tokens.consume_a_token(); maybe_and.is_ident("and"sv)) {
if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::No)) {
tokens.discard_whitespace();
if (tokens.has_next_token())
return invalid_media_query();
media_query->m_media_condition = move(media_condition);
return media_query;
}
return invalid_media_query();
}
return invalid_media_query();
}
// `<media-condition>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition
// `<media-condition-widthout-or>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition-without-or
// (We distinguish between these two with the `allow_or` parameter.)
OwnPtr<MediaCondition> Parser::parse_media_condition(TokenStream<ComponentValue>& tokens, MediaCondition::AllowOr allow_or)
{
// `<media-not> | <media-in-parens> [ <media-and>* | <media-or>* ]`
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// `<media-not> = not <media-in-parens>`
auto parse_media_not = [&](auto& tokens) -> OwnPtr<MediaCondition> {
auto local_transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first_token = tokens.consume_a_token();
if (first_token.is_ident("not"sv)) {
if (auto child_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) {
local_transaction.commit();
return MediaCondition::from_not(child_condition.release_nonnull());
}
}
return {};
};
auto parse_media_with_combinator = [&](auto& tokens, StringView combinator) -> OwnPtr<MediaCondition> {
auto local_transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first = tokens.consume_a_token();
if (first.is_ident(combinator)) {
tokens.discard_whitespace();
if (auto media_in_parens = parse_media_in_parens(tokens)) {
local_transaction.commit();
return media_in_parens;
}
}
return {};
};
// `<media-and> = and <media-in-parens>`
auto parse_media_and = [&](auto& tokens) { return parse_media_with_combinator(tokens, "and"sv); };
// `<media-or> = or <media-in-parens>`
auto parse_media_or = [&](auto& tokens) { return parse_media_with_combinator(tokens, "or"sv); };
// `<media-not>`
if (auto maybe_media_not = parse_media_not(tokens)) {
transaction.commit();
return maybe_media_not.release_nonnull();
}
// `<media-in-parens> [ <media-and>* | <media-or>* ]`
if (auto maybe_media_in_parens = parse_media_in_parens(tokens)) {
tokens.discard_whitespace();
// Only `<media-in-parens>`
if (!tokens.has_next_token()) {
transaction.commit();
return maybe_media_in_parens.release_nonnull();
}
Vector<NonnullOwnPtr<MediaCondition>> child_conditions;
child_conditions.append(maybe_media_in_parens.release_nonnull());
// `<media-and>*`
if (auto media_and = parse_media_and(tokens)) {
child_conditions.append(media_and.release_nonnull());
tokens.discard_whitespace();
while (tokens.has_next_token()) {
if (auto next_media_and = parse_media_and(tokens)) {
child_conditions.append(next_media_and.release_nonnull());
tokens.discard_whitespace();
continue;
}
// We failed - invalid syntax!
return {};
}
transaction.commit();
return MediaCondition::from_and_list(move(child_conditions));
}
// `<media-or>*`
if (allow_or == MediaCondition::AllowOr::Yes) {
if (auto media_or = parse_media_or(tokens)) {
child_conditions.append(media_or.release_nonnull());
tokens.discard_whitespace();
while (tokens.has_next_token()) {
if (auto next_media_or = parse_media_or(tokens)) {
child_conditions.append(next_media_or.release_nonnull());
tokens.discard_whitespace();
continue;
}
// We failed - invalid syntax!
return {};
}
transaction.commit();
return MediaCondition::from_or_list(move(child_conditions));
}
}
}
return {};
}
// `<media-feature>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-feature
Optional<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& tokens)
{
// `[ <mf-plain> | <mf-boolean> | <mf-range> ]`
tokens.discard_whitespace();
// `<mf-name> = <ident>`
struct MediaFeatureName {
enum Type {
Normal,
Min,
Max
} type;
MediaFeatureID id;
};
auto parse_mf_name = [](auto& tokens, bool allow_min_max_prefix) -> Optional<MediaFeatureName> {
auto transaction = tokens.begin_transaction();
auto& token = tokens.consume_a_token();
if (token.is(Token::Type::Ident)) {
auto name = token.token().ident();
if (auto id = media_feature_id_from_string(name); id.has_value()) {
transaction.commit();
return MediaFeatureName { MediaFeatureName::Type::Normal, id.value() };
}
if (allow_min_max_prefix && (name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) || name.starts_with_bytes("max-"sv, CaseSensitivity::CaseInsensitive))) {
auto adjusted_name = name.bytes_as_string_view().substring_view(4);
if (auto id = media_feature_id_from_string(adjusted_name); id.has_value() && media_feature_type_is_range(id.value())) {
transaction.commit();
return MediaFeatureName {
name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) ? MediaFeatureName::Type::Min : MediaFeatureName::Type::Max,
id.value()
};
}
}
}
return {};
};
// `<mf-boolean> = <mf-name>`
auto parse_mf_boolean = [&](auto& tokens) -> Optional<MediaFeature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
return MediaFeature::boolean(maybe_name->id);
}
}
return {};
};
// `<mf-plain> = <mf-name> : <mf-value>`
auto parse_mf_plain = [&](auto& tokens) -> Optional<MediaFeature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto maybe_name = parse_mf_name(tokens, true); maybe_name.has_value()) {
tokens.discard_whitespace();
if (tokens.consume_a_token().is(Token::Type::Colon)) {
tokens.discard_whitespace();
if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
switch (maybe_name->type) {
case MediaFeatureName::Type::Normal:
return MediaFeature::plain(maybe_name->id, maybe_value.release_value());
case MediaFeatureName::Type::Min:
return MediaFeature::min(maybe_name->id, maybe_value.release_value());
case MediaFeatureName::Type::Max:
return MediaFeature::max(maybe_name->id, maybe_value.release_value());
}
VERIFY_NOT_REACHED();
}
}
}
}
return {};
};
// `<mf-lt> = '<' '='?
// <mf-gt> = '>' '='?
// <mf-eq> = '='
// <mf-comparison> = <mf-lt> | <mf-gt> | <mf-eq>`
auto parse_comparison = [](auto& tokens) -> Optional<MediaFeature::Comparison> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first = tokens.consume_a_token();
if (first.is(Token::Type::Delim)) {
auto first_delim = first.token().delim();
if (first_delim == '=') {
transaction.commit();
return MediaFeature::Comparison::Equal;
}
if (first_delim == '<') {
auto& second = tokens.next_token();
if (second.is_delim('=')) {
tokens.discard_a_token();
transaction.commit();
return MediaFeature::Comparison::LessThanOrEqual;
}
transaction.commit();
return MediaFeature::Comparison::LessThan;
}
if (first_delim == '>') {
auto& second = tokens.next_token();
if (second.is_delim('=')) {
tokens.discard_a_token();
transaction.commit();
return MediaFeature::Comparison::GreaterThanOrEqual;
}
transaction.commit();
return MediaFeature::Comparison::GreaterThan;
}
}
return {};
};
auto flip = [](MediaFeature::Comparison comparison) {
switch (comparison) {
case MediaFeature::Comparison::Equal:
return MediaFeature::Comparison::Equal;
case MediaFeature::Comparison::LessThan:
return MediaFeature::Comparison::GreaterThan;
case MediaFeature::Comparison::LessThanOrEqual:
return MediaFeature::Comparison::GreaterThanOrEqual;
case MediaFeature::Comparison::GreaterThan:
return MediaFeature::Comparison::LessThan;
case MediaFeature::Comparison::GreaterThanOrEqual:
return MediaFeature::Comparison::LessThanOrEqual;
}
VERIFY_NOT_REACHED();
};
auto comparisons_match = [](MediaFeature::Comparison a, MediaFeature::Comparison b) -> bool {
switch (a) {
case MediaFeature::Comparison::Equal:
return b == MediaFeature::Comparison::Equal;
case MediaFeature::Comparison::LessThan:
case MediaFeature::Comparison::LessThanOrEqual:
return b == MediaFeature::Comparison::LessThan || b == MediaFeature::Comparison::LessThanOrEqual;
case MediaFeature::Comparison::GreaterThan:
case MediaFeature::Comparison::GreaterThanOrEqual:
return b == MediaFeature::Comparison::GreaterThan || b == MediaFeature::Comparison::GreaterThanOrEqual;
}
VERIFY_NOT_REACHED();
};
// `<mf-range> = <mf-name> <mf-comparison> <mf-value>
// | <mf-value> <mf-comparison> <mf-name>
// | <mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>
// | <mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>`
auto parse_mf_range = [&](auto& tokens) -> Optional<MediaFeature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// `<mf-name> <mf-comparison> <mf-value>`
// NOTE: We have to check for <mf-name> first, since all <mf-name>s will also parse as <mf-value>.
if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value() && media_feature_type_is_range(maybe_name->id)) {
tokens.discard_whitespace();
if (auto maybe_comparison = parse_comparison(tokens); maybe_comparison.has_value()) {
tokens.discard_whitespace();
if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token() && !maybe_value->is_ident()) {
transaction.commit();
return MediaFeature::half_range(maybe_value.release_value(), flip(maybe_comparison.release_value()), maybe_name->id);
}
}
}
}
// `<mf-value> <mf-comparison> <mf-name>
// | <mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>
// | <mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>`
// NOTE: To parse the first value, we need to first find and parse the <mf-name> so we know what value types to parse.
// To allow for <mf-value> to be any number of tokens long, we scan forward until we find a comparison, and then
// treat the next non-whitespace token as the <mf-name>, which should be correct as long as they don't add a value
// type that can include a comparison in it. :^)
Optional<MediaFeatureName> maybe_name;
{
// This transaction is never committed, we just use it to rewind automatically.
auto temp_transaction = tokens.begin_transaction();
while (tokens.has_next_token() && !maybe_name.has_value()) {
if (auto maybe_comparison = parse_comparison(tokens); maybe_comparison.has_value()) {
// We found a comparison, so the next non-whitespace token should be the <mf-name>
tokens.discard_whitespace();
maybe_name = parse_mf_name(tokens, false);
break;
}
tokens.discard_a_token();
tokens.discard_whitespace();
}
}
// Now, we can parse the range properly.
if (maybe_name.has_value() && media_feature_type_is_range(maybe_name->id)) {
if (auto maybe_left_value = parse_media_feature_value(maybe_name->id, tokens); maybe_left_value.has_value()) {
tokens.discard_whitespace();
if (auto maybe_left_comparison = parse_comparison(tokens); maybe_left_comparison.has_value()) {
tokens.discard_whitespace();
tokens.discard_a_token(); // The <mf-name> which we already parsed above.
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
return MediaFeature::half_range(maybe_left_value.release_value(), maybe_left_comparison.release_value(), maybe_name->id);
}
if (auto maybe_right_comparison = parse_comparison(tokens); maybe_right_comparison.has_value()) {
tokens.discard_whitespace();
if (auto maybe_right_value = parse_media_feature_value(maybe_name->id, tokens); maybe_right_value.has_value()) {
tokens.discard_whitespace();
// For this to be valid, the following must be true:
// - Comparisons must either both be >/>= or both be </<=.
// - Neither comparison can be `=`.
// - Neither value can be an ident.
auto left_comparison = maybe_left_comparison.release_value();
auto right_comparison = maybe_right_comparison.release_value();
if (!tokens.has_next_token()
&& comparisons_match(left_comparison, right_comparison)
&& left_comparison != MediaFeature::Comparison::Equal
&& !maybe_left_value->is_ident() && !maybe_right_value->is_ident()) {
transaction.commit();
return MediaFeature::range(maybe_left_value.release_value(), left_comparison, maybe_name->id, right_comparison, maybe_right_value.release_value());
}
}
}
}
}
}
return {};
};
if (auto maybe_mf_boolean = parse_mf_boolean(tokens); maybe_mf_boolean.has_value())
return maybe_mf_boolean.release_value();
if (auto maybe_mf_plain = parse_mf_plain(tokens); maybe_mf_plain.has_value())
return maybe_mf_plain.release_value();
if (auto maybe_mf_range = parse_mf_range(tokens); maybe_mf_range.has_value())
return maybe_mf_range.release_value();
return {};
}
Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& token = tokens.consume_a_token();
if (!token.is(Token::Type::Ident))
return {};
transaction.commit();
auto ident = token.token().ident();
return media_type_from_string(ident);
}
// `<media-in-parens>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-in-parens
OwnPtr<MediaCondition> Parser::parse_media_in_parens(TokenStream<ComponentValue>& tokens)
{
// `<media-in-parens> = ( <media-condition> ) | ( <media-feature> ) | <general-enclosed>`
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// `( <media-condition> ) | ( <media-feature> )`
auto const& first_token = tokens.next_token();
if (first_token.is_block() && first_token.block().is_paren()) {
TokenStream inner_token_stream { first_token.block().value };
if (auto maybe_media_condition = parse_media_condition(inner_token_stream, MediaCondition::AllowOr::Yes)) {
tokens.discard_a_token();
transaction.commit();
return maybe_media_condition.release_nonnull();
}
if (auto maybe_media_feature = parse_media_feature(inner_token_stream); maybe_media_feature.has_value()) {
tokens.discard_a_token();
transaction.commit();
return MediaCondition::from_feature(maybe_media_feature.release_value());
}
}
// `<general-enclosed>`
// FIXME: We should only be taking this branch if the grammar doesn't match the above options.
// Currently we take it if the above fail to parse, which is different.
// eg, `@media (min-width: 76yaks)` is valid grammar, but does not parse because `yaks` isn't a unit.
if (auto maybe_general_enclosed = parse_general_enclosed(tokens); maybe_general_enclosed.has_value()) {
transaction.commit();
return MediaCondition::from_general_enclosed(maybe_general_enclosed.release_value());
}
return {};
}
// `<mf-value>`, https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value
Optional<MediaFeatureValue> Parser::parse_media_feature_value(MediaFeatureID media_feature, TokenStream<ComponentValue>& tokens)
{
// NOTE: Calculations are not allowed for media feature values, at least in the current spec, so we reject them.
// Identifiers
if (tokens.next_token().is(Token::Type::Ident)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto keyword = keyword_from_string(tokens.consume_a_token().token().ident());
if (keyword.has_value() && media_feature_accepts_keyword(media_feature, keyword.value())) {
transaction.commit();
return MediaFeatureValue(keyword.value());
}
}
// One branch for each member of the MediaFeatureValueType enum:
// Boolean (<mq-boolean> in the spec: a 1 or 0)
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Boolean)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto integer = parse_integer(tokens); integer.has_value() && !integer->is_calculated()) {
auto integer_value = integer->value();
if (integer_value == 0 || integer_value == 1) {
transaction.commit();
return MediaFeatureValue(integer_value);
}
}
}
// Integer
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Integer)) {
auto transaction = tokens.begin_transaction();
if (auto integer = parse_integer(tokens); integer.has_value() && !integer->is_calculated()) {
transaction.commit();
return MediaFeatureValue(integer->value());
}
}
// Length
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Length)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto length = parse_length(tokens); length.has_value() && !length->is_calculated()) {
transaction.commit();
return MediaFeatureValue(length->value());
}
}
// Ratio
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Ratio)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto ratio = parse_ratio(tokens); ratio.has_value()) {
transaction.commit();
return MediaFeatureValue(ratio.release_value());
}
}
// Resolution
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Resolution)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto resolution = parse_resolution(tokens); resolution.has_value() && !resolution->is_calculated()) {
transaction.commit();
return MediaFeatureValue(resolution->value());
}
}
return {};
}
JS::GCPtr<CSSMediaRule> Parser::convert_to_media_rule(AtRule const& rule, Nested nested)
{
auto media_query_tokens = TokenStream { rule.prelude };
auto media_query_list = parse_a_media_query_list(media_query_tokens);
auto media_list = MediaList::create(m_context.realm(), move(media_query_list));
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) {
if (auto child_rule = convert_to_rule(rule, nested))
child_rules.append(child_rule);
},
[&](Vector<Declaration> const& declarations) {
auto* declaration = convert_to_style_declaration(declarations);
if (!declaration) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested declarations invalid; discarding.");
return;
}
child_rules.append(CSSNestedDeclarations::create(m_context.realm(), *declaration));
});
}
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
return CSSMediaRule::create(m_context.realm(), media_list, rule_list);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,434 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/RefPtr.h>
#include <AK/Vector.h>
#include <LibGfx/Font/UnicodeRange.h>
#include <LibWeb/CSS/CSSStyleDeclaration.h>
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/GeneralEnclosed.h>
#include <LibWeb/CSS/MediaQuery.h>
#include <LibWeb/CSS/ParsedFontFace.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
#include <LibWeb/CSS/Parser/Dimension.h>
#include <LibWeb/CSS/Parser/ParsingContext.h>
#include <LibWeb/CSS/Parser/TokenStream.h>
#include <LibWeb/CSS/Parser/Tokenizer.h>
#include <LibWeb/CSS/Parser/Types.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/Ratio.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h>
#include <LibWeb/CSS/StyleValues/CSSMathValue.h>
#include <LibWeb/CSS/Supports.h>
#include <LibWeb/Forward.h>
namespace Web::CSS::Parser {
class PropertyDependencyNode;
class Parser {
public:
static Parser create(ParsingContext const&, StringView input, StringView encoding = "utf-8"sv);
Parser(Parser&&);
CSSStyleSheet* parse_as_css_stylesheet(Optional<URL::URL> location);
ElementInlineCSSStyleDeclaration* parse_as_style_attribute(DOM::Element&);
CSSRule* parse_as_css_rule();
Optional<StyleProperty> parse_as_supports_condition();
enum class SelectorParsingMode {
Standard,
// `<forgiving-selector-list>` and `<forgiving-relative-selector-list>`
// are handled with this parameter, not as separate functions.
// https://drafts.csswg.org/selectors/#forgiving-selector
Forgiving
};
// Contrary to the name, these parse a comma-separated list of selectors, according to the spec.
Optional<SelectorList> parse_as_selector(SelectorParsingMode = SelectorParsingMode::Standard);
Optional<SelectorList> parse_as_relative_selector(SelectorParsingMode = SelectorParsingMode::Standard);
Optional<Selector::PseudoElement> parse_as_pseudo_element_selector();
Vector<NonnullRefPtr<MediaQuery>> parse_as_media_query_list();
RefPtr<MediaQuery> parse_as_media_query();
RefPtr<Supports> parse_as_supports();
RefPtr<CSSStyleValue> parse_as_css_value(PropertyID);
Optional<ComponentValue> parse_as_component_value();
Vector<ParsedFontFace::Source> parse_as_font_face_src();
static NonnullRefPtr<CSSStyleValue> resolve_unresolved_style_value(ParsingContext const&, DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>, PropertyID, UnresolvedStyleValue const&);
[[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr);
private:
Parser(ParsingContext const&, Vector<Token>);
enum class ParseError {
IncludesIgnoredVendorPrefix,
InternalError,
SyntaxError,
};
template<typename T>
using ParseErrorOr = ErrorOr<T, ParseError>;
// "Parse a stylesheet" is intended to be the normal parser entry point, for parsing stylesheets.
struct ParsedStyleSheet {
Optional<URL::URL> location;
Vector<Rule> rules;
};
template<typename T>
ParsedStyleSheet parse_a_stylesheet(TokenStream<T>&, Optional<URL::URL> location);
// "Parse a stylesheets contents" is intended for use by the CSSStyleSheet replace() method, and similar, which parse text into the contents of an existing stylesheet.
template<typename T>
Vector<Rule> parse_a_stylesheets_contents(TokenStream<T>&);
// "Parse a blocks contents" is intended for parsing the contents of any block in CSS (including things like the style attribute),
// and APIs such as the CSSStyleDeclaration cssText attribute.
template<typename T>
Vector<RuleOrListOfDeclarations> parse_a_blocks_contents(TokenStream<T>&);
// "Parse a rule" is intended for use by the CSSStyleSheet#insertRule method, and similar functions which might exist, which parse text into a single rule.
template<typename T>
Optional<Rule> parse_a_rule(TokenStream<T>&);
// "Parse a declaration" is used in @supports conditions. [CSS3-CONDITIONAL]
template<typename T>
Optional<Declaration> parse_a_declaration(TokenStream<T>&);
// "Parse a component value" is for things that need to consume a single value, like the parsing rules for attr().
template<typename T>
Optional<ComponentValue> parse_a_component_value(TokenStream<T>&);
// "Parse a list of component values" is for the contents of presentational attributes, which parse text into a single declarations value,
// or for parsing a stand-alone selector [SELECT] or list of Media Queries [MEDIAQ], as in Selectors API or the media HTML attribute.
template<typename T>
Vector<ComponentValue> parse_a_list_of_component_values(TokenStream<T>&);
template<typename T>
Vector<Vector<ComponentValue>> parse_a_comma_separated_list_of_component_values(TokenStream<T>&);
enum class SelectorType {
Standalone,
Relative
};
template<typename T>
ParseErrorOr<SelectorList> parse_a_selector_list(TokenStream<T>&, SelectorType, SelectorParsingMode = SelectorParsingMode::Standard);
template<typename T>
Vector<NonnullRefPtr<MediaQuery>> parse_a_media_query_list(TokenStream<T>&);
template<typename T>
RefPtr<Supports> parse_a_supports(TokenStream<T>&);
Optional<Selector::SimpleSelector::ANPlusBPattern> parse_a_n_plus_b_pattern(TokenStream<ComponentValue>&);
template<typename T>
[[nodiscard]] Vector<Rule> consume_a_stylesheets_contents(TokenStream<T>&);
enum class Nested {
No,
Yes,
};
template<typename T>
Optional<AtRule> consume_an_at_rule(TokenStream<T>&, Nested nested = Nested::No);
struct InvalidRuleError { };
template<typename T>
Variant<Empty, QualifiedRule, InvalidRuleError> consume_a_qualified_rule(TokenStream<T>&, Optional<Token::Type> stop_token = {}, Nested = Nested::No);
template<typename T>
Vector<RuleOrListOfDeclarations> consume_a_block(TokenStream<T>&);
template<typename T>
Vector<RuleOrListOfDeclarations> consume_a_blocks_contents(TokenStream<T>&);
template<typename T>
Optional<Declaration> consume_a_declaration(TokenStream<T>&, Nested = Nested::No);
template<typename T>
void consume_the_remnants_of_a_bad_declaration(TokenStream<T>&, Nested);
template<typename T>
[[nodiscard]] Vector<ComponentValue> consume_a_list_of_component_values(TokenStream<T>&, Optional<Token::Type> stop_token = {}, Nested = Nested::No);
template<typename T>
[[nodiscard]] ComponentValue consume_a_component_value(TokenStream<T>&);
template<typename T>
SimpleBlock consume_a_simple_block(TokenStream<T>&);
template<typename T>
Function consume_a_function(TokenStream<T>&);
// TODO: consume_a_unicode_range_value()
Optional<GeneralEnclosed> parse_general_enclosed(TokenStream<ComponentValue>&);
template<typename T>
Vector<ParsedFontFace::Source> parse_font_face_src(TokenStream<T>&);
enum class AllowBlankLayerName {
No,
Yes,
};
Optional<FlyString> parse_layer_name(TokenStream<ComponentValue>&, AllowBlankLayerName);
bool is_valid_in_the_current_context(Declaration const&) const;
bool is_valid_in_the_current_context(AtRule const&) const;
bool is_valid_in_the_current_context(QualifiedRule const&) const;
JS::GCPtr<CSSRule> convert_to_rule(Rule const&, Nested);
JS::GCPtr<CSSStyleRule> convert_to_style_rule(QualifiedRule const&, Nested);
JS::GCPtr<CSSFontFaceRule> convert_to_font_face_rule(AtRule const&);
JS::GCPtr<CSSKeyframesRule> convert_to_keyframes_rule(AtRule const&);
JS::GCPtr<CSSImportRule> convert_to_import_rule(AtRule const&);
JS::GCPtr<CSSRule> convert_to_layer_rule(AtRule const&, Nested);
JS::GCPtr<CSSMediaRule> convert_to_media_rule(AtRule const&, Nested);
JS::GCPtr<CSSNamespaceRule> convert_to_namespace_rule(AtRule const&);
JS::GCPtr<CSSSupportsRule> convert_to_supports_rule(AtRule const&, Nested);
JS::GCPtr<CSSPropertyRule> convert_to_property_rule(AtRule const& rule);
PropertyOwningCSSStyleDeclaration* convert_to_style_declaration(Vector<Declaration> const&);
Optional<StyleProperty> convert_to_style_property(Declaration const&);
Optional<Dimension> parse_dimension(ComponentValue const&);
Optional<AngleOrCalculated> parse_angle(TokenStream<ComponentValue>&);
Optional<AnglePercentage> parse_angle_percentage(TokenStream<ComponentValue>&);
Optional<FlexOrCalculated> parse_flex(TokenStream<ComponentValue>&);
Optional<FrequencyOrCalculated> parse_frequency(TokenStream<ComponentValue>&);
Optional<FrequencyPercentage> parse_frequency_percentage(TokenStream<ComponentValue>&);
Optional<IntegerOrCalculated> parse_integer(TokenStream<ComponentValue>&);
Optional<LengthOrCalculated> parse_length(TokenStream<ComponentValue>&);
Optional<LengthPercentage> parse_length_percentage(TokenStream<ComponentValue>&);
Optional<NumberOrCalculated> parse_number(TokenStream<ComponentValue>&);
Optional<NumberPercentage> parse_number_percentage(TokenStream<ComponentValue>&);
Optional<ResolutionOrCalculated> parse_resolution(TokenStream<ComponentValue>&);
Optional<TimeOrCalculated> parse_time(TokenStream<ComponentValue>&);
Optional<TimePercentage> parse_time_percentage(TokenStream<ComponentValue>&);
Optional<LengthOrCalculated> parse_source_size_value(TokenStream<ComponentValue>&);
Optional<Ratio> parse_ratio(TokenStream<ComponentValue>&);
Optional<Gfx::UnicodeRange> parse_unicode_range(TokenStream<ComponentValue>&);
Optional<Gfx::UnicodeRange> parse_unicode_range(StringView);
Vector<Gfx::UnicodeRange> parse_unicode_ranges(TokenStream<ComponentValue>&);
Optional<GridSize> parse_grid_size(ComponentValue const&);
Optional<GridFitContent> parse_fit_content(Vector<ComponentValue> const&);
Optional<GridMinMax> parse_min_max(Vector<ComponentValue> const&);
Optional<GridRepeat> parse_repeat(Vector<ComponentValue> const&);
Optional<ExplicitGridTrack> parse_track_sizing_function(ComponentValue const&);
Optional<URL::URL> parse_url_function(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_url_value(TokenStream<ComponentValue>&);
Optional<ShapeRadius> parse_shape_radius(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_basic_shape_value(TokenStream<ComponentValue>&);
template<typename TElement>
Optional<Vector<TElement>> parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto is_position, auto get_position);
Optional<Vector<LinearColorStopListElement>> parse_linear_color_stop_list(TokenStream<ComponentValue>&);
Optional<Vector<AngularColorStopListElement>> parse_angular_color_stop_list(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_linear_gradient_function(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_conic_gradient_function(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_radial_gradient_function(TokenStream<ComponentValue>&);
ParseErrorOr<NonnullRefPtr<CSSStyleValue>> parse_css_value(PropertyID, TokenStream<ComponentValue>&, Optional<String> original_source_text = {});
RefPtr<CSSStyleValue> parse_css_value_for_property(PropertyID, TokenStream<ComponentValue>&);
struct PropertyAndValue {
PropertyID property;
RefPtr<CSSStyleValue> style_value;
};
Optional<PropertyAndValue> parse_css_value_for_properties(ReadonlySpan<PropertyID>, TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_builtin_value(TokenStream<ComponentValue>&);
RefPtr<CSSMathValue> parse_calculated_value(ComponentValue const&);
RefPtr<CustomIdentStyleValue> parse_custom_ident_value(TokenStream<ComponentValue>&, std::initializer_list<StringView> blacklist);
// NOTE: Implemented in generated code. (GenerateCSSMathFunctions.cpp)
OwnPtr<CalculationNode> parse_math_function(PropertyID, Function const&);
OwnPtr<CalculationNode> parse_a_calc_function_node(Function const&);
RefPtr<CSSStyleValue> parse_keyword_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_hue_none_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_solidus_and_alpha_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_rgb_color_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_hsl_color_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_hwb_color_value(TokenStream<ComponentValue>&);
Optional<Array<RefPtr<CSSStyleValue>, 4>> parse_lab_like_color_value(TokenStream<ComponentValue>&, StringView);
RefPtr<CSSStyleValue> parse_lab_color_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_oklab_color_value(TokenStream<ComponentValue>&);
Optional<Array<RefPtr<CSSStyleValue>, 4>> parse_lch_like_color_value(TokenStream<ComponentValue>&, StringView);
RefPtr<CSSStyleValue> parse_lch_color_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_oklch_color_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_color_function(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_color_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_value(TokenStream<ComponentValue>&);
enum class AllowReversed {
No,
Yes,
};
RefPtr<CSSStyleValue> parse_counter_definitions_value(TokenStream<ComponentValue>&, AllowReversed, i32 default_value_if_not_reversed);
RefPtr<CSSStyleValue> parse_rect_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_ratio_value(TokenStream<ComponentValue>&);
RefPtr<StringStyleValue> parse_string_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_image_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_paint_value(TokenStream<ComponentValue>&);
enum class PositionParsingMode {
Normal,
BackgroundPosition,
};
RefPtr<PositionStyleValue> parse_position_value(TokenStream<ComponentValue>&, PositionParsingMode = PositionParsingMode::Normal);
RefPtr<CSSStyleValue> parse_filter_value_list_value(TokenStream<ComponentValue>&);
RefPtr<StringStyleValue> parse_opentype_tag_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_dimension_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_angle_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_angle_percentage_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_flex_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_frequency_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_frequency_percentage_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_integer_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_length_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_length_percentage_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_number_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_number_percentage_value(TokenStream<ComponentValue>& tokens);
RefPtr<CSSStyleValue> parse_number_percentage_none_value(TokenStream<ComponentValue>& tokens);
RefPtr<CSSStyleValue> parse_percentage_value(TokenStream<ComponentValue>& tokens);
RefPtr<CSSStyleValue> parse_resolution_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_time_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_time_percentage_value(TokenStream<ComponentValue>&);
template<typename ParseFunction>
RefPtr<CSSStyleValue> parse_comma_separated_value_list(TokenStream<ComponentValue>&, ParseFunction);
RefPtr<CSSStyleValue> parse_simple_comma_separated_value_list(PropertyID, TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_all_as_single_keyword_value(TokenStream<ComponentValue>&, Keyword);
RefPtr<CSSStyleValue> parse_aspect_ratio_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_background_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_single_background_position_x_or_y_value(TokenStream<ComponentValue>&, PropertyID);
RefPtr<CSSStyleValue> parse_single_background_repeat_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_single_background_size_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_border_value(PropertyID, TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_border_radius_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_border_radius_shorthand_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_columns_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_content_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_increment_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_reset_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_set_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_display_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_flex_shorthand_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_flex_flow_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_font_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_font_family_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_font_language_override_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_font_feature_settings_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_font_variation_settings_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_list_style_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_math_depth_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_overflow_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_place_content_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_place_items_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_place_self_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_quotes_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_scrollbar_gutter_value(TokenStream<ComponentValue>&);
enum class AllowInsetKeyword {
No,
Yes,
};
RefPtr<CSSStyleValue> parse_shadow_value(TokenStream<ComponentValue>&, AllowInsetKeyword);
RefPtr<CSSStyleValue> parse_single_shadow_value(TokenStream<ComponentValue>&, AllowInsetKeyword);
RefPtr<CSSStyleValue> parse_text_decoration_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_text_decoration_line_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_rotate_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_easing_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_transform_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_transform_origin_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_transition_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_grid_track_size_list(TokenStream<ComponentValue>&, bool allow_separate_line_name_blocks = false);
RefPtr<CSSStyleValue> parse_grid_auto_track_sizes(TokenStream<ComponentValue>&);
RefPtr<GridAutoFlowStyleValue> parse_grid_auto_flow_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_grid_track_size_list_shorthand_value(PropertyID, TokenStream<ComponentValue>&);
RefPtr<GridTrackPlacementStyleValue> parse_grid_track_placement(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_grid_track_placement_shorthand_value(PropertyID, TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_grid_template_areas_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_grid_area_shorthand_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_grid_shorthand_value(TokenStream<ComponentValue>&);
OwnPtr<CalculationNode> parse_a_calculation(Vector<ComponentValue> const&);
ParseErrorOr<NonnullRefPtr<Selector>> parse_complex_selector(TokenStream<ComponentValue>&, SelectorType);
ParseErrorOr<Optional<Selector::CompoundSelector>> parse_compound_selector(TokenStream<ComponentValue>&);
Optional<Selector::Combinator> parse_selector_combinator(TokenStream<ComponentValue>&);
enum class AllowWildcardName {
No,
Yes,
};
Optional<Selector::SimpleSelector::QualifiedName> parse_selector_qualified_name(TokenStream<ComponentValue>&, AllowWildcardName);
ParseErrorOr<Selector::SimpleSelector> parse_attribute_simple_selector(ComponentValue const&);
ParseErrorOr<Selector::SimpleSelector> parse_pseudo_simple_selector(TokenStream<ComponentValue>&);
ParseErrorOr<Optional<Selector::SimpleSelector>> parse_simple_selector(TokenStream<ComponentValue>&);
NonnullRefPtr<MediaQuery> parse_media_query(TokenStream<ComponentValue>&);
OwnPtr<MediaCondition> parse_media_condition(TokenStream<ComponentValue>&, MediaCondition::AllowOr allow_or);
Optional<MediaFeature> parse_media_feature(TokenStream<ComponentValue>&);
Optional<MediaQuery::MediaType> parse_media_type(TokenStream<ComponentValue>&);
OwnPtr<MediaCondition> parse_media_in_parens(TokenStream<ComponentValue>&);
Optional<MediaFeatureValue> parse_media_feature_value(MediaFeatureID, TokenStream<ComponentValue>&);
OwnPtr<Supports::Condition> parse_supports_condition(TokenStream<ComponentValue>&);
Optional<Supports::InParens> parse_supports_in_parens(TokenStream<ComponentValue>&);
Optional<Supports::Feature> parse_supports_feature(TokenStream<ComponentValue>&);
NonnullRefPtr<CSSStyleValue> resolve_unresolved_style_value(DOM::Element&, Optional<Selector::PseudoElement::Type>, PropertyID, UnresolvedStyleValue const&);
bool expand_variables(DOM::Element&, Optional<Selector::PseudoElement::Type>, FlyString const& property_name, HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>>& dependencies, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest);
bool expand_unresolved_values(DOM::Element&, FlyString const& property_name, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest);
bool substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector<ComponentValue>& dest);
static bool has_ignored_vendor_prefix(StringView);
static bool is_generic_font_family(Keyword);
struct PropertiesAndCustomProperties {
Vector<StyleProperty> properties;
HashMap<FlyString, StyleProperty> custom_properties;
};
PropertiesAndCustomProperties extract_properties(Vector<RuleOrListOfDeclarations> const&);
void extract_property(Declaration const&, Parser::PropertiesAndCustomProperties&);
ParsingContext m_context;
Vector<Token> m_tokens;
TokenStream<Token> m_token_stream;
enum class ContextType {
Unknown,
Style,
AtMedia,
AtFontFace,
AtKeyframes,
Keyframe,
AtSupports,
SupportsCondition,
AtLayer,
AtProperty,
};
static ContextType context_type_for_at_rule(FlyString const&);
Vector<ContextType> m_rule_context;
};
}
namespace Web {
CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingContext const&, StringView, Optional<URL::URL> location = {});
CSS::ElementInlineCSSStyleDeclaration* parse_css_style_attribute(CSS::Parser::ParsingContext const&, StringView, DOM::Element&);
RefPtr<CSS::CSSStyleValue> parse_css_value(CSS::Parser::ParsingContext const&, StringView, CSS::PropertyID property_id = CSS::PropertyID::Invalid);
Optional<CSS::SelectorList> parse_selector(CSS::Parser::ParsingContext const&, StringView);
Optional<CSS::SelectorList> parse_selector_for_nested_style_rule(CSS::Parser::ParsingContext const&, StringView);
Optional<CSS::Selector::PseudoElement> parse_pseudo_element_selector(CSS::Parser::ParsingContext const&, StringView);
CSS::CSSRule* parse_css_rule(CSS::Parser::ParsingContext const&, StringView);
RefPtr<CSS::MediaQuery> parse_media_query(CSS::Parser::ParsingContext const&, StringView);
Vector<NonnullRefPtr<CSS::MediaQuery>> parse_media_query_list(CSS::Parser::ParsingContext const&, StringView);
RefPtr<CSS::Supports> parse_css_supports(CSS::Parser::ParsingContext const&, StringView);
Optional<CSS::StyleProperty> parse_css_supports_condition(CSS::Parser::ParsingContext const&, StringView);
}

View file

@ -0,0 +1,71 @@
/*
* 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 <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/DOM/Document.h>
namespace Web::CSS::Parser {
ParsingContext::ParsingContext(JS::Realm& realm, Mode mode)
: m_realm(realm)
, m_mode(mode)
{
}
ParsingContext::ParsingContext(JS::Realm& realm, URL::URL url, Mode mode)
: m_realm(realm)
, m_url(move(url))
, m_mode(mode)
{
}
ParsingContext::ParsingContext(DOM::Document const& document, URL::URL url, Mode mode)
: m_realm(const_cast<JS::Realm&>(document.realm()))
, m_document(&document)
, m_url(move(url))
, m_mode(mode)
{
}
ParsingContext::ParsingContext(DOM::Document const& document, Mode mode)
: m_realm(const_cast<JS::Realm&>(document.realm()))
, m_document(&document)
, m_url(document.url())
, m_mode(mode)
{
}
ParsingContext::ParsingContext(DOM::ParentNode& parent_node, Mode mode)
: m_realm(parent_node.realm())
, m_document(&parent_node.document())
, m_url(parent_node.document().url())
, m_mode(mode)
{
}
bool ParsingContext::in_quirks_mode() const
{
return m_document ? m_document->in_quirks_mode() : false;
}
// https://www.w3.org/TR/css-values-4/#relative-urls
URL::URL ParsingContext::complete_url(StringView relative_url) const
{
return m_url.complete_url(relative_url);
}
HTML::Window const* ParsingContext::window() const
{
if (!m_document)
return nullptr;
return m_document->window();
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/DOM/Document.h>
namespace Web::CSS::Parser {
class ParsingContext {
public:
enum class Mode {
Normal,
SVGPresentationAttribute, // See https://svgwg.org/svg2-draft/types.html#presentation-attribute-css-value
};
explicit ParsingContext(JS::Realm&, Mode = Mode::Normal);
explicit ParsingContext(JS::Realm&, URL::URL, Mode = Mode::Normal);
explicit ParsingContext(DOM::Document const&, Mode = Mode::Normal);
explicit ParsingContext(DOM::Document const&, URL::URL, Mode = Mode::Normal);
explicit ParsingContext(DOM::ParentNode&, Mode = Mode::Normal);
Mode mode() const { return m_mode; }
bool is_parsing_svg_presentation_attribute() const { return m_mode == Mode::SVGPresentationAttribute; }
bool in_quirks_mode() const;
DOM::Document const* document() const { return m_document; }
HTML::Window const* window() const;
URL::URL complete_url(StringView) const;
PropertyID current_property_id() const { return m_current_property_id; }
void set_current_property_id(PropertyID property_id) { m_current_property_id = property_id; }
JS::Realm& realm() const { return m_realm; }
private:
JS::NonnullGCPtr<JS::Realm> m_realm;
JS::GCPtr<DOM::Document const> m_document;
PropertyID m_current_property_id { PropertyID::Invalid };
URL::URL m_url;
Mode m_mode { Mode::Normal };
};
}

View file

@ -0,0 +1,884 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Tommy van der Vorst <tommy@pixelspark.nl>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2024, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSKeyframeRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
#include <LibWeb/CSS/CSSLayerBlockRule.h>
#include <LibWeb/CSS/CSSLayerStatementRule.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSNamespaceRule.h>
#include <LibWeb/CSS/CSSNestedDeclarations.h>
#include <LibWeb/CSS/CSSPropertyRule.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSSupportsRule.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyName.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/OpenTypeTaggedStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
namespace Web::CSS::Parser {
JS::GCPtr<CSSRule> Parser::convert_to_rule(Rule const& rule, Nested nested)
{
return rule.visit(
[this, nested](AtRule const& at_rule) -> JS::GCPtr<CSSRule> {
if (has_ignored_vendor_prefix(at_rule.name))
return {};
if (at_rule.name.equals_ignoring_ascii_case("font-face"sv))
return convert_to_font_face_rule(at_rule);
if (at_rule.name.equals_ignoring_ascii_case("import"sv))
return convert_to_import_rule(at_rule);
if (at_rule.name.equals_ignoring_ascii_case("keyframes"sv))
return convert_to_keyframes_rule(at_rule);
if (at_rule.name.equals_ignoring_ascii_case("layer"sv))
return convert_to_layer_rule(at_rule, nested);
if (at_rule.name.equals_ignoring_ascii_case("media"sv))
return convert_to_media_rule(at_rule, nested);
if (at_rule.name.equals_ignoring_ascii_case("namespace"sv))
return convert_to_namespace_rule(at_rule);
if (at_rule.name.equals_ignoring_ascii_case("supports"sv))
return convert_to_supports_rule(at_rule, nested);
if (at_rule.name.equals_ignoring_ascii_case("property"sv))
return convert_to_property_rule(at_rule);
// FIXME: More at rules!
dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", at_rule.name);
return {};
},
[this, nested](QualifiedRule const& qualified_rule) -> JS::GCPtr<CSSRule> {
return convert_to_style_rule(qualified_rule, nested);
});
}
JS::GCPtr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
{
TokenStream prelude_stream { qualified_rule.prelude };
auto maybe_selectors = parse_a_selector_list(prelude_stream,
nested == Nested::Yes ? SelectorType::Relative : SelectorType::Standalone);
if (maybe_selectors.is_error()) {
if (maybe_selectors.error() == ParseError::SyntaxError) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule selectors invalid; discarding.");
if constexpr (CSS_PARSER_DEBUG) {
prelude_stream.dump_all_tokens();
}
}
return {};
}
if (maybe_selectors.value().is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
return {};
}
SelectorList selectors = maybe_selectors.release_value();
if (nested == Nested::Yes)
selectors = adapt_nested_relative_selector_list(selectors);
auto* declaration = convert_to_style_declaration(qualified_rule.declarations);
if (!declaration) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule declaration invalid; discarding.");
return {};
}
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
for (auto& child : qualified_rule.child_rules) {
child.visit(
[&](Rule const& rule) {
// "In addition to nested style rules, this specification allows nested group rules inside of style rules:
// any at-rule whose body contains style rules can be nested inside of a style rule as well."
// https://drafts.csswg.org/css-nesting-1/#nested-group-rules
if (auto converted_rule = convert_to_rule(rule, Nested::Yes)) {
if (is<CSSGroupingRule>(*converted_rule)) {
child_rules.append(converted_rule);
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested {} is not allowed inside style rule; discarding.", converted_rule->class_name());
}
}
},
[&](Vector<Declaration> const& declarations) {
auto* declaration = convert_to_style_declaration(declarations);
if (!declaration) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested declarations invalid; discarding.");
return;
}
child_rules.append(CSSNestedDeclarations::create(m_context.realm(), *declaration));
});
}
auto nested_rules = CSSRuleList::create(m_context.realm(), move(child_rules));
return CSSStyleRule::create(m_context.realm(), move(selectors), *declaration, *nested_rules);
}
JS::GCPtr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
{
// https://drafts.csswg.org/css-cascade-5/#at-import
// @import [ <url> | <string> ]
// [ layer | layer(<layer-name>) ]?
// <import-conditions> ;
//
// <import-conditions> = [ supports( [ <supports-condition> | <declaration> ] ) ]?
// <media-query-list>?
if (rule.prelude.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Empty prelude.");
return {};
}
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Block is not allowed.");
return {};
}
TokenStream tokens { rule.prelude };
tokens.discard_whitespace();
Optional<URL::URL> url = parse_url_function(tokens);
if (!url.has_value() && tokens.next_token().is(Token::Type::String))
url = m_context.complete_url(tokens.consume_a_token().token().string());
if (!url.has_value()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
return {};
}
tokens.discard_whitespace();
// TODO: Support layers and import-conditions
if (tokens.has_next_token()) {
if constexpr (CSS_PARSER_DEBUG) {
dbgln("Failed to parse @import rule: Trailing tokens after URL are not yet supported.");
tokens.dump_all_tokens();
}
return {};
}
return CSSImportRule::create(url.value(), const_cast<DOM::Document&>(*m_context.document()));
}
Optional<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
{
// https://drafts.csswg.org/css-cascade-5/#typedef-layer-name
// <layer-name> = <ident> [ '.' <ident> ]*
// "The CSS-wide keywords are reserved for future use, and cause the rule to be invalid at parse time if used as an <ident> in the <layer-name>."
auto is_valid_layer_name_part = [](auto& token) {
return token.is(Token::Type::Ident) && !is_css_wide_keyword(token.token().ident());
};
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (!tokens.has_next_token() && allow_blank_layer_name == AllowBlankLayerName::Yes) {
// No name present, just return a blank one
return FlyString();
}
auto& first_name_token = tokens.consume_a_token();
if (!is_valid_layer_name_part(first_name_token))
return {};
StringBuilder builder;
builder.append(first_name_token.token().ident());
while (tokens.has_next_token()) {
// Repeatedly parse `'.' <ident>`
if (!tokens.next_token().is_delim('.'))
break;
tokens.discard_a_token(); // '.'
auto& name_token = tokens.consume_a_token();
if (!is_valid_layer_name_part(name_token))
return {};
builder.appendff(".{}", name_token.token().ident());
}
transaction.commit();
return builder.to_fly_string_without_validation();
}
JS::GCPtr<CSSRule> Parser::convert_to_layer_rule(AtRule const& rule, Nested nested)
{
// https://drafts.csswg.org/css-cascade-5/#at-layer
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
// CSSLayerBlockRule
// @layer <layer-name>? {
// <rule-list>
// }
// First, the name
FlyString layer_name = {};
auto prelude_tokens = TokenStream { rule.prelude };
if (auto maybe_name = parse_layer_name(prelude_tokens, AllowBlankLayerName::Yes); maybe_name.has_value()) {
layer_name = maybe_name.release_value();
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (not a valid layer name) prelude = {}; discarding.", rule.prelude);
return {};
}
prelude_tokens.discard_whitespace();
if (prelude_tokens.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (tokens after layer name) prelude = {}; discarding.", rule.prelude);
return {};
}
// Then the rules
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) {
if (auto child_rule = convert_to_rule(rule, nested))
child_rules.append(child_rule);
},
[&](Vector<Declaration> const& declarations) {
auto* declaration = convert_to_style_declaration(declarations);
if (!declaration) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested declarations invalid; discarding.");
return;
}
child_rules.append(CSSNestedDeclarations::create(m_context.realm(), *declaration));
});
}
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
return CSSLayerBlockRule::create(m_context.realm(), layer_name, rule_list);
}
// CSSLayerStatementRule
// @layer <layer-name>#;
auto tokens = TokenStream { rule.prelude };
tokens.discard_whitespace();
Vector<FlyString> layer_names;
while (tokens.has_next_token()) {
// Comma
if (!layer_names.is_empty()) {
if (auto comma = tokens.consume_a_token(); !comma.is(Token::Type::Comma)) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer missing separating comma, ({}) prelude = {}; discarding.", comma.to_debug_string(), rule.prelude);
return {};
}
tokens.discard_whitespace();
}
if (auto name = parse_layer_name(tokens, AllowBlankLayerName::No); name.has_value()) {
layer_names.append(name.release_value());
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer contains invalid name, prelude = {}; discarding.", rule.prelude);
return {};
}
tokens.discard_whitespace();
}
if (layer_names.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer statement has no layer names, prelude = {}; discarding.", rule.prelude);
return {};
}
return CSSLayerStatementRule::create(m_context.realm(), move(layer_names));
}
JS::GCPtr<CSSKeyframesRule> Parser::convert_to_keyframes_rule(AtRule const& rule)
{
// https://drafts.csswg.org/css-animations/#keyframes
// @keyframes = @keyframes <keyframes-name> { <qualified-rule-list> }
// <keyframes-name> = <custom-ident> | <string>
// <keyframe-block> = <keyframe-selector># { <declaration-list> }
// <keyframe-selector> = from | to | <percentage [0,100]>
if (rule.prelude.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @keyframes rule: Empty prelude.");
return {};
}
// FIXME: Is there some way of detecting if there is a block or not?
auto prelude_stream = TokenStream { rule.prelude };
prelude_stream.discard_whitespace();
auto& token = prelude_stream.consume_a_token();
if (!token.is_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
return {};
}
auto name_token = token.token();
prelude_stream.discard_whitespace();
if (prelude_stream.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
return {};
}
if (name_token.is(Token::Type::Ident) && (is_css_wide_keyword(name_token.ident()) || name_token.ident().equals_ignoring_ascii_case("none"sv))) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.ident());
return {};
}
if (!name_token.is(Token::Type::String) && !name_token.is(Token::Type::Ident)) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.to_debug_string());
return {};
}
auto name = name_token.to_string();
JS::MarkedVector<CSSRule*> keyframes(m_context.realm().heap());
rule.for_each_as_qualified_rule_list([&](auto& qualified_rule) {
if (!qualified_rule.child_rules.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes keyframe rule contains at-rules; discarding them.");
}
auto selectors = Vector<CSS::Percentage> {};
TokenStream child_tokens { qualified_rule.prelude };
while (child_tokens.has_next_token()) {
child_tokens.discard_whitespace();
if (!child_tokens.has_next_token())
break;
auto tok = child_tokens.consume_a_token();
if (!tok.is_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule has invalid selector: {}; discarding.", tok.to_debug_string());
child_tokens.reconsume_current_input_token();
break;
}
auto token = tok.token();
auto read_a_selector = false;
if (token.is(Token::Type::Ident)) {
if (token.ident().equals_ignoring_ascii_case("from"sv)) {
selectors.append(CSS::Percentage(0));
read_a_selector = true;
}
if (token.ident().equals_ignoring_ascii_case("to"sv)) {
selectors.append(CSS::Percentage(100));
read_a_selector = true;
}
} else if (token.is(Token::Type::Percentage)) {
selectors.append(CSS::Percentage(token.percentage()));
read_a_selector = true;
}
if (read_a_selector) {
child_tokens.discard_whitespace();
if (child_tokens.consume_a_token().is(Token::Type::Comma))
continue;
}
child_tokens.reconsume_current_input_token();
break;
}
PropertiesAndCustomProperties properties;
qualified_rule.for_each_as_declaration_list([&](auto const& declaration) {
extract_property(declaration, properties);
});
auto style = PropertyOwningCSSStyleDeclaration::create(m_context.realm(), move(properties.properties), move(properties.custom_properties));
for (auto& selector : selectors) {
auto keyframe_rule = CSSKeyframeRule::create(m_context.realm(), selector, *style);
keyframes.append(keyframe_rule);
}
});
return CSSKeyframesRule::create(m_context.realm(), name, CSSRuleList::create(m_context.realm(), move(keyframes)));
}
JS::GCPtr<CSSNamespaceRule> Parser::convert_to_namespace_rule(AtRule const& rule)
{
// https://drafts.csswg.org/css-namespaces/#syntax
// @namespace <namespace-prefix>? [ <string> | <url> ] ;
// <namespace-prefix> = <ident>
if (rule.prelude.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Empty prelude.");
return {};
}
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Block is not allowed.");
return {};
}
auto tokens = TokenStream { rule.prelude };
tokens.discard_whitespace();
Optional<FlyString> prefix = {};
if (tokens.next_token().is(Token::Type::Ident)) {
prefix = tokens.consume_a_token().token().ident();
tokens.discard_whitespace();
}
FlyString namespace_uri;
if (auto url = parse_url_function(tokens); url.has_value()) {
namespace_uri = MUST(url.value().to_string());
} else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) {
namespace_uri = url_token.token().string();
} else {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
return {};
}
tokens.discard_whitespace();
if (tokens.has_next_token()) {
if constexpr (CSS_PARSER_DEBUG) {
dbgln("Failed to parse @namespace rule: Trailing tokens after URL.");
tokens.dump_all_tokens();
}
return {};
}
return CSSNamespaceRule::create(m_context.realm(), prefix, namespace_uri);
}
JS::GCPtr<CSSSupportsRule> Parser::convert_to_supports_rule(AtRule const& rule, Nested nested)
{
// https://drafts.csswg.org/css-conditional-3/#at-supports
// @supports <supports-condition> {
// <rule-list>
// }
if (rule.prelude.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @supports rule: Empty prelude.");
return {};
}
auto supports_tokens = TokenStream { rule.prelude };
auto supports = parse_a_supports(supports_tokens);
if (!supports) {
if constexpr (CSS_PARSER_DEBUG) {
dbgln("Failed to parse @supports rule: supports clause invalid.");
supports_tokens.dump_all_tokens();
}
return {};
}
JS::MarkedVector<CSSRule*> child_rules { m_context.realm().heap() };
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) {
if (auto child_rule = convert_to_rule(rule, nested))
child_rules.append(child_rule);
},
[&](Vector<Declaration> const& declarations) {
auto* declaration = convert_to_style_declaration(declarations);
if (!declaration) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested declarations invalid; discarding.");
return;
}
child_rules.append(CSSNestedDeclarations::create(m_context.realm(), *declaration));
});
}
auto rule_list = CSSRuleList::create(m_context.realm(), child_rules);
return CSSSupportsRule::create(m_context.realm(), supports.release_nonnull(), rule_list);
}
JS::GCPtr<CSSPropertyRule> Parser::convert_to_property_rule(AtRule const& rule)
{
// https://drafts.css-houdini.org/css-properties-values-api-1/#at-ruledef-property
// @property <custom-property-name> {
// <declaration-list>
// }
if (rule.prelude.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @property rule: Empty prelude.");
return {};
}
auto prelude_stream = TokenStream { rule.prelude };
prelude_stream.discard_whitespace();
auto const& token = prelude_stream.consume_a_token();
if (!token.is_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
return {};
}
auto name_token = token.token();
prelude_stream.discard_whitespace();
if (prelude_stream.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
return {};
}
if (!name_token.is(Token::Type::Ident)) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name is invalid: {}; discarding.", name_token.to_debug_string());
return {};
}
if (!is_a_custom_property_name_string(name_token.ident())) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name doesn't start with '--': {}; discarding.", name_token.ident());
return {};
}
auto const& name = name_token.ident();
Optional<FlyString> syntax_maybe;
Optional<bool> inherits_maybe;
Optional<String> initial_value_maybe;
rule.for_each_as_declaration_list([&](auto& declaration) {
if (declaration.name.equals_ignoring_ascii_case("syntax"sv)) {
TokenStream token_stream { declaration.value };
token_stream.discard_whitespace();
auto const& syntax_token = token_stream.consume_a_token();
if (syntax_token.is(Token::Type::String)) {
token_stream.discard_whitespace();
if (token_stream.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in syntax");
} else {
syntax_maybe = syntax_token.token().string();
}
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected value for @property \"syntax\": {}; discarding.", declaration.to_string());
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("inherits"sv)) {
TokenStream token_stream { declaration.value };
token_stream.discard_whitespace();
auto const& inherits_token = token_stream.consume_a_token();
if (inherits_token.is_ident("true"sv) || inherits_token.is_ident("false"sv)) {
auto const& ident = inherits_token.token().ident();
token_stream.discard_whitespace();
if (token_stream.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in inherits");
} else {
inherits_maybe = (ident == "true");
}
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Expected true/false for @property \"inherits\" value, got: {}; discarding.", inherits_token.to_debug_string());
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("initial-value"sv)) {
// FIXME: Ensure that the initial value matches the syntax, and parse the correct CSSValue out
StringBuilder initial_value_sb;
for (auto const& component : declaration.value) {
initial_value_sb.append(component.to_string());
}
initial_value_maybe = MUST(initial_value_sb.to_string());
return;
}
});
if (syntax_maybe.has_value() && inherits_maybe.has_value()) {
return CSSPropertyRule::create(m_context.realm(), name, syntax_maybe.value(), inherits_maybe.value(), std::move(initial_value_maybe));
}
return {};
}
JS::GCPtr<CSSFontFaceRule> Parser::convert_to_font_face_rule(AtRule const& rule)
{
// https://drafts.csswg.org/css-fonts/#font-face-rule
Optional<FlyString> font_family;
Optional<FlyString> font_named_instance;
Vector<ParsedFontFace::Source> src;
Vector<Gfx::UnicodeRange> unicode_range;
Optional<int> weight;
Optional<int> slope;
Optional<int> width;
Optional<Percentage> ascent_override;
Optional<Percentage> descent_override;
Optional<Percentage> line_gap_override;
FontDisplay font_display = FontDisplay::Auto;
Optional<FlyString> language_override;
Optional<OrderedHashMap<FlyString, i64>> font_feature_settings;
Optional<OrderedHashMap<FlyString, double>> font_variation_settings;
// "normal" is returned as nullptr
auto parse_as_percentage_or_normal = [&](Vector<ComponentValue> const& values) -> ErrorOr<Optional<Percentage>> {
// normal | <percentage [0,∞]>
TokenStream tokens { values };
if (auto percentage_value = parse_percentage_value(tokens)) {
tokens.discard_whitespace();
if (tokens.has_next_token())
return Error::from_string_literal("Unexpected trailing tokens");
if (percentage_value->is_percentage() && percentage_value->as_percentage().percentage().value() >= 0)
return percentage_value->as_percentage().percentage();
// TODO: Once we implement calc-simplification in the parser, we should no longer see math values here,
// unless they're impossible to resolve and thus invalid.
if (percentage_value->is_math()) {
if (auto result = percentage_value->as_math().resolve_percentage(); result.has_value())
return result.value();
}
return Error::from_string_literal("Invalid percentage");
}
tokens.discard_whitespace();
if (!tokens.consume_a_token().is_ident("normal"sv))
return Error::from_string_literal("Expected `normal | <percentage [0,∞]>`");
tokens.discard_whitespace();
if (tokens.has_next_token())
return Error::from_string_literal("Unexpected trailing tokens");
return OptionalNone {};
};
rule.for_each_as_declaration_list([&](auto& declaration) {
if (declaration.name.equals_ignoring_ascii_case("ascent-override"sv)) {
auto value = parse_as_percentage_or_normal(declaration.value);
if (value.is_error()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face ascent-override: {}", value.error());
} else {
ascent_override = value.release_value();
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("descent-override"sv)) {
auto value = parse_as_percentage_or_normal(declaration.value);
if (value.is_error()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face descent-override: {}", value.error());
} else {
descent_override = value.release_value();
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-display"sv)) {
TokenStream token_stream { declaration.value };
if (auto keyword_value = parse_keyword_value(token_stream)) {
token_stream.discard_whitespace();
if (token_stream.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in font-display");
} else {
auto value = keyword_to_font_display(keyword_value->to_keyword());
if (value.has_value()) {
font_display = *value;
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: `{}` is not a valid value for font-display", keyword_value->to_string());
}
}
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-family"sv)) {
// FIXME: This is very similar to, but different from, the logic in parse_font_family_value().
// Ideally they could share code.
Vector<FlyString> font_family_parts;
bool had_syntax_error = false;
for (size_t i = 0; i < declaration.value.size(); ++i) {
auto const& part = declaration.value[i];
if (part.is(Token::Type::Whitespace))
continue;
if (part.is(Token::Type::String)) {
if (!font_family_parts.is_empty()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
had_syntax_error = true;
break;
}
font_family_parts.append(part.token().string());
continue;
}
if (part.is(Token::Type::Ident)) {
if (is_css_wide_keyword(part.token().ident())) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
had_syntax_error = true;
break;
}
auto keyword = keyword_from_string(part.token().ident());
if (keyword.has_value() && is_generic_font_family(keyword.value())) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
had_syntax_error = true;
break;
}
font_family_parts.append(part.token().ident());
continue;
}
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @font-face font-family format invalid; discarding.");
had_syntax_error = true;
break;
}
if (had_syntax_error || font_family_parts.is_empty())
return;
font_family = String::join(' ', font_family_parts).release_value_but_fixme_should_propagate_errors();
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-feature-settings"sv)) {
TokenStream token_stream { declaration.value };
if (auto value = parse_css_value(CSS::PropertyID::FontFeatureSettings, token_stream); !value.is_error()) {
if (value.value()->to_keyword() == Keyword::Normal) {
font_feature_settings.clear();
} else if (value.value()->is_value_list()) {
auto const& feature_tags = value.value()->as_value_list().values();
OrderedHashMap<FlyString, i64> settings;
settings.ensure_capacity(feature_tags.size());
for (auto const& feature_tag : feature_tags) {
if (!feature_tag->is_open_type_tagged()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-feature-settings descriptor is not an OpenTypeTaggedStyleValue; skipping");
continue;
}
auto const& setting_value = feature_tag->as_open_type_tagged().value();
if (setting_value->is_integer()) {
settings.set(feature_tag->as_open_type_tagged().tag(), setting_value->as_integer().integer());
} else if (setting_value->is_math() && setting_value->as_math().resolves_to_number()) {
if (auto integer = setting_value->as_math().resolve_integer(); integer.has_value()) {
settings.set(feature_tag->as_open_type_tagged().tag(), *integer);
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Calculated value in font-feature-settings descriptor cannot be resolved at parse time; skipping");
}
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-feature-settings descriptor is not an OpenTypeTaggedStyleValue holding a <integer>; skipping");
}
}
font_feature_settings = move(settings);
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-feature-settings descriptor, not compatible with value returned from parsing font-feature-settings property: {}", value.value()->to_string());
}
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-language-override"sv)) {
TokenStream token_stream { declaration.value };
if (auto maybe_value = parse_css_value(CSS::PropertyID::FontLanguageOverride, token_stream); !maybe_value.is_error()) {
auto& value = maybe_value.value();
if (value->is_string()) {
language_override = value->as_string().string_value();
} else {
language_override.clear();
}
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-named-instance"sv)) {
// auto | <string>
TokenStream token_stream { declaration.value };
token_stream.discard_whitespace();
auto& token = token_stream.consume_a_token();
token_stream.discard_whitespace();
if (token_stream.has_next_token()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in font-named-instance");
return;
}
if (token.is_ident("auto"sv)) {
font_named_instance.clear();
} else if (token.is(Token::Type::String)) {
font_named_instance = token.token().string();
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-named-instance from {}", token.to_debug_string());
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-style"sv)) {
TokenStream token_stream { declaration.value };
if (auto value = parse_css_value(CSS::PropertyID::FontStyle, token_stream); !value.is_error()) {
slope = value.value()->to_font_slope();
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-variation-settings"sv)) {
TokenStream token_stream { declaration.value };
if (auto value = parse_css_value(CSS::PropertyID::FontVariationSettings, token_stream); !value.is_error()) {
if (value.value()->to_keyword() == Keyword::Normal) {
font_variation_settings.clear();
} else if (value.value()->is_value_list()) {
auto const& variation_tags = value.value()->as_value_list().values();
OrderedHashMap<FlyString, double> settings;
settings.ensure_capacity(variation_tags.size());
for (auto const& variation_tag : variation_tags) {
if (!variation_tag->is_open_type_tagged()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-variation-settings descriptor is not an OpenTypeTaggedStyleValue; skipping");
continue;
}
auto const& setting_value = variation_tag->as_open_type_tagged().value();
if (setting_value->is_number()) {
settings.set(variation_tag->as_open_type_tagged().tag(), setting_value->as_number().number());
} else if (setting_value->is_math() && setting_value->as_math().resolves_to_number()) {
if (auto number = setting_value->as_math().resolve_number(); number.has_value()) {
settings.set(variation_tag->as_open_type_tagged().tag(), *number);
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Calculated value in font-variation-settings descriptor cannot be resolved at parse time; skipping");
}
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Value in font-variation-settings descriptor is not an OpenTypeTaggedStyleValue holding a <number>; skipping");
}
}
font_variation_settings = move(settings);
} else {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse font-variation-settings descriptor, not compatible with value returned from parsing font-variation-settings property: {}", value.value()->to_string());
}
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-weight"sv)) {
TokenStream token_stream { declaration.value };
if (auto value = parse_css_value(CSS::PropertyID::FontWeight, token_stream); !value.is_error()) {
weight = value.value()->to_font_weight();
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("font-width"sv)
|| declaration.name.equals_ignoring_ascii_case("font-stretch"sv)) {
TokenStream token_stream { declaration.value };
if (auto value = parse_css_value(CSS::PropertyID::FontWidth, token_stream); !value.is_error()) {
width = value.value()->to_font_width();
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("line-gap-override"sv)) {
auto value = parse_as_percentage_or_normal(declaration.value);
if (value.is_error()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face line-gap-override: {}", value.error());
} else {
line_gap_override = value.release_value();
}
return;
}
if (declaration.name.equals_ignoring_ascii_case("src"sv)) {
TokenStream token_stream { declaration.value };
Vector<ParsedFontFace::Source> supported_sources = parse_font_face_src(token_stream);
if (!supported_sources.is_empty())
src = move(supported_sources);
return;
}
if (declaration.name.equals_ignoring_ascii_case("unicode-range"sv)) {
TokenStream token_stream { declaration.value };
auto unicode_ranges = parse_unicode_ranges(token_stream);
if (unicode_ranges.is_empty())
return;
unicode_range = move(unicode_ranges);
return;
}
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unrecognized descriptor '{}' in @font-face; discarding.", declaration.name);
});
if (!font_family.has_value()) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Failed to parse @font-face: no font-family!");
return {};
}
if (unicode_range.is_empty()) {
unicode_range.empend(0x0u, 0x10FFFFu);
}
return CSSFontFaceRule::create(m_context.realm(), ParsedFontFace { font_family.release_value(), move(weight), move(slope), move(width), move(src), move(unicode_range), move(ascent_override), move(descent_override), move(line_gap_override), font_display, move(font_named_instance), move(language_override), move(font_feature_settings), move(font_variation_settings) });
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,216 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/Parser/Token.h>
#include <LibWeb/CSS/Serialize.h>
namespace Web::CSS::Parser {
String Token::to_string() const
{
StringBuilder builder;
switch (m_type) {
case Type::EndOfFile:
return String {};
case Type::Ident:
return serialize_an_identifier(ident());
case Type::Function:
return MUST(String::formatted("{}(", serialize_an_identifier(function())));
case Type::AtKeyword:
return MUST(String::formatted("@{}", serialize_an_identifier(at_keyword())));
case Type::Hash: {
switch (m_hash_type) {
case HashType::Id:
return MUST(String::formatted("#{}", serialize_an_identifier(hash_value())));
case HashType::Unrestricted:
return MUST(String::formatted("#{}", hash_value()));
}
VERIFY_NOT_REACHED();
}
case Type::String:
return serialize_a_string(string());
case Type::BadString:
return String {};
case Type::Url:
return serialize_a_url(url());
case Type::BadUrl:
return "url()"_string;
case Type::Delim:
return String { m_value };
case Type::Number:
return String::number(m_number_value.value());
case Type::Percentage:
return MUST(String::formatted("{}%", m_number_value.value()));
case Type::Dimension:
return MUST(String::formatted("{}{}", m_number_value.value(), dimension_unit()));
case Type::Whitespace:
return " "_string;
case Type::CDO:
return "<!--"_string;
case Type::CDC:
return "-->"_string;
case Type::Colon:
return ":"_string;
case Type::Semicolon:
return ";"_string;
case Type::Comma:
return ","_string;
case Type::OpenSquare:
return "["_string;
case Type::CloseSquare:
return "]"_string;
case Type::OpenParen:
return "("_string;
case Type::CloseParen:
return ")"_string;
case Type::OpenCurly:
return "{"_string;
case Type::CloseCurly:
return "}"_string;
case Type::Invalid:
default:
VERIFY_NOT_REACHED();
}
}
String Token::to_debug_string() const
{
switch (m_type) {
case Type::Invalid:
VERIFY_NOT_REACHED();
case Type::EndOfFile:
return "__EOF__"_string;
case Type::Ident:
return MUST(String::formatted("Ident: {}", ident()));
case Type::Function:
return MUST(String::formatted("Function: {}", function()));
case Type::AtKeyword:
return MUST(String::formatted("AtKeyword: {}", at_keyword()));
case Type::Hash:
return MUST(String::formatted("Hash: {} (hash_type: {})", hash_value(), m_hash_type == HashType::Unrestricted ? "Unrestricted" : "Id"));
case Type::String:
return MUST(String::formatted("String: {}", string()));
case Type::BadString:
return "BadString"_string;
case Type::Url:
return MUST(String::formatted("Url: {}", url()));
case Type::BadUrl:
return "BadUrl"_string;
case Type::Delim:
return MUST(String::formatted("Delim: {}", m_value));
case Type::Number:
return MUST(String::formatted("Number: {}{} (number_type: {})", m_number_value.value() > 0 && m_number_value.is_integer_with_explicit_sign() ? "+" : "", m_number_value.value(), m_number_value.is_integer() ? "Integer" : "Number"));
case Type::Percentage:
return MUST(String::formatted("Percentage: {}% (number_type: {})", percentage(), m_number_value.is_integer() ? "Integer" : "Number"));
case Type::Dimension:
return MUST(String::formatted("Dimension: {}{} (number_type: {})", dimension_value(), dimension_unit(), m_number_value.is_integer() ? "Integer" : "Number"));
case Type::Whitespace:
return "Whitespace"_string;
case Type::CDO:
return "CDO"_string;
case Type::CDC:
return "CDC"_string;
case Type::Colon:
return "Colon"_string;
case Type::Semicolon:
return "Semicolon"_string;
case Type::Comma:
return "Comma"_string;
case Type::OpenSquare:
return "OpenSquare"_string;
case Type::CloseSquare:
return "CloseSquare"_string;
case Type::OpenParen:
return "OpenParen"_string;
case Type::CloseParen:
return "CloseParen"_string;
case Type::OpenCurly:
return "OpenCurly"_string;
case Type::CloseCurly:
return "CloseCurly"_string;
}
VERIFY_NOT_REACHED();
}
Token::Type Token::mirror_variant() const
{
if (is(Token::Type::OpenCurly)) {
return Type::CloseCurly;
}
if (is(Token::Type::OpenSquare)) {
return Type::CloseSquare;
}
if (is(Token::Type::OpenParen)) {
return Type::CloseParen;
}
return Type::Invalid;
}
StringView Token::bracket_string() const
{
if (is(Token::Type::OpenCurly)) {
return "{"sv;
}
if (is(Token::Type::CloseCurly)) {
return "}"sv;
}
if (is(Token::Type::OpenSquare)) {
return "["sv;
}
if (is(Token::Type::CloseSquare)) {
return "]"sv;
}
if (is(Token::Type::OpenParen)) {
return "("sv;
}
if (is(Token::Type::CloseParen)) {
return ")"sv;
}
return ""sv;
}
StringView Token::bracket_mirror_string() const
{
if (is(Token::Type::OpenCurly)) {
return "}"sv;
}
if (is(Token::Type::CloseCurly)) {
return "{"sv;
}
if (is(Token::Type::OpenSquare)) {
return "]"sv;
}
if (is(Token::Type::CloseSquare)) {
return "["sv;
}
if (is(Token::Type::OpenParen)) {
return ")"sv;
}
if (is(Token::Type::CloseParen)) {
return "("sv;
}
return ""sv;
}
}

View file

@ -0,0 +1,214 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <LibWeb/CSS/Number.h>
namespace Web::CSS::Parser {
class Token {
friend class Tokenizer;
public:
enum class Type {
Invalid,
EndOfFile,
Ident,
Function,
AtKeyword,
Hash,
String,
BadString,
Url,
BadUrl,
Delim,
Number,
Percentage,
Dimension,
Whitespace,
CDO,
CDC,
Colon,
Semicolon,
Comma,
OpenSquare,
CloseSquare,
OpenParen,
CloseParen,
OpenCurly,
CloseCurly
};
enum class HashType {
Id,
Unrestricted,
};
struct Position {
size_t line { 0 };
size_t column { 0 };
};
Type type() const { return m_type; }
bool is(Type type) const { return m_type == type; }
FlyString const& ident() const
{
VERIFY(m_type == Type::Ident);
return m_value;
}
FlyString const& function() const
{
VERIFY(m_type == Type::Function);
return m_value;
}
u32 delim() const
{
VERIFY(m_type == Type::Delim);
return *m_value.code_points().begin();
}
FlyString const& string() const
{
VERIFY(m_type == Type::String);
return m_value;
}
FlyString const& url() const
{
VERIFY(m_type == Type::Url);
return m_value;
}
FlyString const& at_keyword() const
{
VERIFY(m_type == Type::AtKeyword);
return m_value;
}
HashType hash_type() const
{
VERIFY(m_type == Type::Hash);
return m_hash_type;
}
FlyString const& hash_value() const
{
VERIFY(m_type == Type::Hash);
return m_value;
}
Number const& number() const
{
VERIFY(m_type == Type::Number || m_type == Type::Dimension || m_type == Type::Percentage);
return m_number_value;
}
double number_value() const
{
VERIFY(m_type == Type::Number);
return m_number_value.value();
}
i64 to_integer() const
{
VERIFY(m_type == Type::Number && m_number_value.is_integer());
return m_number_value.integer_value();
}
FlyString const& dimension_unit() const
{
VERIFY(m_type == Type::Dimension);
return m_value;
}
double dimension_value() const
{
VERIFY(m_type == Type::Dimension);
return m_number_value.value();
}
i64 dimension_value_int() const { return m_number_value.integer_value(); }
double percentage() const
{
VERIFY(m_type == Type::Percentage);
return m_number_value.value();
}
Type mirror_variant() const;
StringView bracket_string() const;
StringView bracket_mirror_string() const;
String to_string() const;
String to_debug_string() const;
String const& original_source_text() const { return m_original_source_text; }
Position const& start_position() const { return m_start_position; }
Position const& end_position() const { return m_end_position; }
static Token create_string(FlyString str)
{
Token token;
token.m_type = Type::String;
token.m_value = move(str);
return token;
}
static Token create_number(double value, Number::Type number_type)
{
Token token;
token.m_type = Type::Number;
token.m_number_value = Number(number_type, value);
return token;
}
static Token create_percentage(double value)
{
Token token;
token.m_type = Type::Percentage;
token.m_number_value = Number(Number::Type::Number, value);
return token;
}
static Token create_dimension(double value, FlyString unit)
{
Token token;
token.m_type = Type::Dimension;
token.m_number_value = Number(Number::Type::Number, value);
token.m_value = move(unit);
return token;
}
static Token create_ident(FlyString ident)
{
Token token;
token.m_type = Type::Ident;
token.m_value = move(ident);
return token;
}
static Token create_url(FlyString url)
{
Token token;
token.m_type = Type::Url;
token.m_value = move(url);
return token;
}
private:
Type m_type { Type::Invalid };
FlyString m_value;
Number m_number_value;
HashType m_hash_type { HashType::Unrestricted };
String m_original_source_text;
Position m_start_position;
Position m_end_position;
};
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Format.h>
#include <AK/Vector.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
#include <LibWeb/CSS/Parser/Tokenizer.h>
namespace Web::CSS::Parser {
// https://drafts.csswg.org/css-syntax/#css-token-stream
template<typename T>
class TokenStream {
public:
class StateTransaction {
public:
explicit StateTransaction(TokenStream<T>& token_stream)
: m_token_stream(token_stream)
, m_saved_index(token_stream.m_index)
{
}
~StateTransaction()
{
if (!m_commit)
m_token_stream.m_index = m_saved_index;
}
StateTransaction create_child() { return StateTransaction(*this); }
void commit()
{
m_commit = true;
if (m_parent)
m_parent->commit();
}
private:
explicit StateTransaction(StateTransaction& parent)
: m_parent(&parent)
, m_token_stream(parent.m_token_stream)
, m_saved_index(parent.m_token_stream.m_index)
{
}
StateTransaction* m_parent { nullptr };
TokenStream<T>& m_token_stream;
size_t m_saved_index { 0 };
bool m_commit { false };
};
explicit TokenStream(Span<T const> tokens)
: m_tokens(tokens)
, m_eof(make_eof())
{
}
explicit TokenStream(Vector<T> const& tokens)
: m_tokens(tokens.span())
, m_eof(make_eof())
{
}
static TokenStream<T> of_single_token(T const& token)
{
return TokenStream(Span<T const> { &token, 1 });
}
TokenStream(TokenStream<T> const&) = delete;
TokenStream(TokenStream<T>&&) = default;
// https://drafts.csswg.org/css-syntax/#token-stream-next-token
[[nodiscard]] T const& next_token() const
{
// The item of tokens at index.
// If that index would be out-of-bounds past the end of the list, its instead an <eof-token>.
if (m_index < m_tokens.size())
return m_tokens[m_index];
return m_eof;
}
// https://drafts.csswg.org/css-syntax/#token-stream-empty
[[nodiscard]] bool is_empty() const
{
// A token stream is empty if the next token is an <eof-token>.
return next_token().is(Token::Type::EndOfFile);
}
// https://drafts.csswg.org/css-syntax/#token-stream-consume-a-token
[[nodiscard]] T const& consume_a_token()
{
// Let token be the next token. Increment index, then return token.
auto& token = next_token();
++m_index;
return token;
}
// https://drafts.csswg.org/css-syntax/#token-stream-discard-a-token
void discard_a_token()
{
// If the token stream is not empty, increment index.
if (!is_empty())
++m_index;
}
// https://drafts.csswg.org/css-syntax/#token-stream-mark
void mark()
{
// Append index to marked indexes.
m_marked_indexes.append(m_index);
}
// https://drafts.csswg.org/css-syntax/#token-stream-restore-a-mark
void restore_a_mark()
{
// Pop from marked indexes, and set index to the popped value.
m_index = m_marked_indexes.take_last();
}
// https://drafts.csswg.org/css-syntax/#token-stream-discard-a-mark
void discard_a_mark()
{
// Pop from marked indexes, and do nothing with the popped value.
m_marked_indexes.take_last();
}
// https://drafts.csswg.org/css-syntax/#token-stream-discard-whitespace
void discard_whitespace()
{
// While the next token is a <whitespace-token>, discard a token.
while (next_token().is(Token::Type::Whitespace))
discard_a_token();
}
bool has_next_token()
{
return !is_empty();
}
// Deprecated, used in older versions of the spec.
T const& current_token()
{
if (m_index < 1 || (m_index - 1) >= m_tokens.size())
return m_eof;
return m_tokens.at(m_index - 1);
}
// Deprecated
T const& peek_token(size_t offset = 0)
{
if (remaining_token_count() <= offset)
return m_eof;
return m_tokens.at(m_index + offset);
}
// Deprecated, was used in older versions of the spec.
void reconsume_current_input_token()
{
if (m_index > 0)
--m_index;
}
StateTransaction begin_transaction() { return StateTransaction(*this); }
size_t remaining_token_count() const
{
if (m_tokens.size() > m_index)
return m_tokens.size() - m_index;
return 0;
}
void dump_all_tokens()
{
dbgln("Dumping all tokens:");
for (size_t i = 0; i < m_tokens.size(); ++i) {
auto& token = m_tokens[i];
if (i == m_index - 1)
dbgln("-> {}", token.to_debug_string());
else
dbgln(" {}", token.to_debug_string());
}
}
void copy_state(Badge<Parser>, TokenStream<T> const& other)
{
m_index = other.m_index;
}
private:
// https://drafts.csswg.org/css-syntax/#token-stream-tokens
Span<T const> m_tokens;
// https://drafts.csswg.org/css-syntax/#token-stream-index
size_t m_index { 0 };
// https://drafts.csswg.org/css-syntax/#token-stream-marked-indexes
Vector<size_t> m_marked_indexes;
T make_eof()
{
if constexpr (IsSame<T, Token>) {
return Tokenizer::create_eof_token();
}
if constexpr (IsSame<T, ComponentValue>) {
return ComponentValue(Tokenizer::create_eof_token());
}
}
T m_eof;
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <AK/Types.h>
#include <AK/Utf8View.h>
#include <LibWeb/CSS/Parser/Token.h>
#include <LibWeb/Forward.h>
namespace Web::CSS::Parser {
class U32Twin {
public:
void set(size_t index, u32 value)
{
if (index == 0)
first = value;
if (index == 1)
second = value;
}
u32 first {};
u32 second {};
};
class U32Triplet {
public:
void set(size_t index, u32 value)
{
if (index == 0)
first = value;
if (index == 1)
second = value;
if (index == 2)
third = value;
}
U32Twin to_twin_12()
{
return { first, second };
}
U32Twin to_twin_23()
{
return { second, third };
}
u32 first {};
u32 second {};
u32 third {};
};
class Tokenizer {
public:
static Vector<Token> tokenize(StringView input, StringView encoding);
[[nodiscard]] static Token create_eof_token();
private:
explicit Tokenizer(String decoded_input);
[[nodiscard]] Vector<Token> tokenize();
size_t current_byte_offset() const;
String input_since(size_t offset) const;
[[nodiscard]] u32 next_code_point();
[[nodiscard]] u32 peek_code_point(size_t offset = 0) const;
[[nodiscard]] U32Twin peek_twin() const;
[[nodiscard]] U32Triplet peek_triplet() const;
[[nodiscard]] U32Twin start_of_input_stream_twin();
[[nodiscard]] U32Triplet start_of_input_stream_triplet();
[[nodiscard]] static Token create_new_token(Token::Type);
[[nodiscard]] static Token create_value_token(Token::Type, FlyString&& value, String&& representation);
[[nodiscard]] static Token create_value_token(Token::Type, u32 value, String&& representation);
[[nodiscard]] Token consume_a_token();
[[nodiscard]] Token consume_string_token(u32 ending_code_point);
[[nodiscard]] Token consume_a_numeric_token();
[[nodiscard]] Token consume_an_ident_like_token();
[[nodiscard]] Number consume_a_number();
[[nodiscard]] double convert_a_string_to_a_number(StringView);
[[nodiscard]] FlyString consume_an_ident_sequence();
[[nodiscard]] u32 consume_escaped_code_point();
[[nodiscard]] Token consume_a_url_token();
void consume_the_remnants_of_a_bad_url();
void consume_comments();
void consume_as_much_whitespace_as_possible();
void reconsume_current_input_code_point();
[[nodiscard]] static bool is_valid_escape_sequence(U32Twin);
[[nodiscard]] static bool would_start_an_ident_sequence(U32Triplet);
[[nodiscard]] static bool would_start_a_number(U32Triplet);
String m_decoded_input;
Utf8View m_utf8_view;
AK::Utf8CodePointIterator m_utf8_iterator;
AK::Utf8CodePointIterator m_prev_utf8_iterator;
Token::Position m_position;
Token::Position m_prev_position;
};
}

View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
#include <LibWeb/CSS/Parser/Types.h>
#include <LibWeb/CSS/Serialize.h>
namespace Web::CSS::Parser {
String Declaration::to_string() const
{
if (original_text.has_value())
return original_text.value();
StringBuilder builder;
serialize_an_identifier(builder, name);
builder.append(": "sv);
builder.join(' ', value);
if (important == Important::Yes)
builder.append(" !important"sv);
return MUST(builder.to_string());
}
String SimpleBlock::to_string() const
{
StringBuilder builder;
builder.append(token.bracket_string());
builder.join(' ', value);
builder.append(token.bracket_mirror_string());
return builder.to_string_without_validation();
}
String SimpleBlock::original_source_text() const
{
StringBuilder builder;
builder.append(token.original_source_text());
for (auto const& component_value : value) {
builder.append(component_value.original_source_text());
}
builder.append(end_token.original_source_text());
return builder.to_string_without_validation();
}
String Function::to_string() const
{
StringBuilder builder;
serialize_an_identifier(builder, name);
builder.append('(');
for (auto& item : value)
builder.append(item.to_string());
builder.append(')');
return builder.to_string_without_validation();
}
String Function::original_source_text() const
{
StringBuilder builder;
builder.append(name_token.original_source_text());
for (auto const& component_value : value) {
builder.append(component_value.original_source_text());
}
builder.append(end_token.original_source_text());
return builder.to_string_without_validation();
}
void AtRule::for_each(AtRuleVisitor&& visit_at_rule, QualifiedRuleVisitor&& visit_qualified_rule, DeclarationVisitor&& visit_declaration) const
{
for (auto const& child : child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) {
rule.visit(
[&](AtRule const& at_rule) { visit_at_rule(at_rule); },
[&](QualifiedRule const& qualified_rule) { visit_qualified_rule(qualified_rule); });
},
[&](Vector<Declaration> const& declarations) {
for (auto const& declaration : declarations)
visit_declaration(declaration);
});
}
}
// https://drafts.csswg.org/css-syntax/#typedef-declaration-list
void AtRule::for_each_as_declaration_list(DeclarationVisitor&& visit) const
{
// <declaration-list>: only declarations are allowed; at-rules and qualified rules are automatically invalid.
for_each(
[](auto const& at_rule) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal @{} rule in `<declaration-list>`; discarding.", at_rule.name); },
[](auto const&) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal qualified rule in `<declaration-list>`; discarding."); },
move(visit));
}
// https://drafts.csswg.org/css-syntax/#typedef-qualified-rule-list
void AtRule::for_each_as_qualified_rule_list(QualifiedRuleVisitor&& visit) const
{
// <qualified-rule-list>: only qualified rules are allowed; declarations and at-rules are automatically invalid.
for_each(
[](auto const& at_rule) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal @{} rule in `<qualified-rule-list>`; discarding.", at_rule.name); },
move(visit),
[](auto const&) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal list of declarations in `<qualified-rule-list>`; discarding."); });
}
// https://drafts.csswg.org/css-syntax/#typedef-at-rule-list
void AtRule::for_each_as_at_rule_list(AtRuleVisitor&& visit) const
{
// <at-rule-list>: only at-rules are allowed; declarations and qualified rules are automatically invalid.
for_each(
move(visit),
[](auto const&) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal qualified rule in `<at-rule-list>`; discarding."); },
[](auto const&) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal list of declarations in `<at-rule-list>`; discarding."); });
}
// https://drafts.csswg.org/css-syntax/#typedef-declaration-rule-list
void AtRule::for_each_as_declaration_rule_list(AtRuleVisitor&& visit_at_rule, DeclarationVisitor&& visit_declaration) const
{
// <declaration-rule-list>: declarations and at-rules are allowed; qualified rules are automatically invalid.
for_each(
move(visit_at_rule),
[](auto const&) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal qualified rule in `<declaration-rule-list>`; discarding."); },
move(visit_declaration));
}
// https://drafts.csswg.org/css-syntax/#typedef-rule-list
void AtRule::for_each_as_rule_list(RuleVisitor&& visit) const
{
// <rule-list>: qualified rules and at-rules are allowed; declarations are automatically invalid.
for (auto const& child : child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) { visit(rule); },
[&](Vector<Declaration> const&) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal list of declarations in `<rule-list>`; discarding."); });
}
}
// https://drafts.csswg.org/css-syntax/#typedef-declaration-list
void QualifiedRule::for_each_as_declaration_list(DeclarationVisitor&& visit) const
{
// <declaration-list>: only declarations are allowed; at-rules and qualified rules are automatically invalid.
for (auto const& declaration : declarations)
visit(declaration);
for (auto const& child : child_rules) {
child.visit(
[&](Rule const&) {
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Found illegal qualified rule in `<declaration-list>`; discarding.");
},
[&](Vector<Declaration> const& declarations) {
for (auto const& declaration : declarations)
visit(declaration);
});
}
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <AK/Function.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibWeb/CSS/Parser/Token.h>
#include <LibWeb/CSS/StyleProperty.h>
#include <LibWeb/Forward.h>
namespace Web::CSS::Parser {
// https://drafts.csswg.org/css-syntax/#css-rule
using Rule = Variant<AtRule, QualifiedRule>;
using RuleOrListOfDeclarations = Variant<Rule, Vector<Declaration, 0>>;
using AtRuleVisitor = AK::Function<void(AtRule const&)>;
using QualifiedRuleVisitor = AK::Function<void(QualifiedRule const&)>;
using RuleVisitor = AK::Function<void(Rule const&)>;
using DeclarationVisitor = AK::Function<void(Declaration const&)>;
// https://drafts.csswg.org/css-syntax/#ref-for-at-rule%E2%91%A0%E2%91%A1
struct AtRule {
FlyString name;
Vector<ComponentValue> prelude;
Vector<RuleOrListOfDeclarations> child_rules_and_lists_of_declarations;
void for_each(AtRuleVisitor&& visit_at_rule, QualifiedRuleVisitor&& visit_qualified_rule, DeclarationVisitor&& visit_declaration) const;
void for_each_as_declaration_list(DeclarationVisitor&& visit) const;
void for_each_as_qualified_rule_list(QualifiedRuleVisitor&& visit) const;
void for_each_as_at_rule_list(AtRuleVisitor&& visit) const;
void for_each_as_declaration_rule_list(AtRuleVisitor&& visit_at_rule, DeclarationVisitor&& visit_declaration) const;
void for_each_as_rule_list(RuleVisitor&& visit) const;
};
// https://drafts.csswg.org/css-syntax/#qualified-rule
struct QualifiedRule {
Vector<ComponentValue> prelude;
Vector<Declaration> declarations;
Vector<RuleOrListOfDeclarations> child_rules;
void for_each_as_declaration_list(DeclarationVisitor&& visit) const;
};
// https://drafts.csswg.org/css-syntax/#declaration
struct Declaration {
FlyString name;
Vector<ComponentValue> value;
Important important = Important::No;
Optional<String> original_text = {};
// FIXME: Only needed by our janky @supports re-serialization-re-parse code.
String to_string() const;
};
// https://drafts.csswg.org/css-syntax/#simple-block
struct SimpleBlock {
Token token;
Vector<ComponentValue> value;
Token end_token = {};
bool is_curly() const { return token.is(Token::Type::OpenCurly); }
bool is_paren() const { return token.is(Token::Type::OpenParen); }
bool is_square() const { return token.is(Token::Type::OpenSquare); }
String to_string() const;
String original_source_text() const;
};
// https://drafts.csswg.org/css-syntax/#function
struct Function {
FlyString name;
Vector<ComponentValue> value;
Token name_token = {};
Token end_token = {};
String to_string() const;
String original_source_text() const;
};
}