ladybird/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp
Sam Atkins 5d965d6d37 LibWeb/CSS: Simplify parse_as_pseudo_element_selector()
Now that we have a method that parses a single pseudo-element selector,
use that. This actually fixes a bug too.
2026-04-18 08:56:25 +02:00

1566 lines
68 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2025, 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 <LibWeb/CSS/Parser/ErrorReporter.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/Infra/Strings.h>
namespace Web::CSS::Parser {
static bool next_is_pseudo_element(TokenStream<ComponentValue>& tokens)
{
auto& first = tokens.next_token();
auto& second = tokens.peek_token(1);
if (first.is(Token::Type::Colon)) {
// Pseudo-elements
if (second.is(Token::Type::Colon))
return true;
// Legacy single-colon pseudo-elements
if (second.is(Token::Type::Ident)) {
auto pseudo_element = pseudo_element_from_string(second.token().ident());
if (pseudo_element.has_value() && is_legacy_single_colon_pseudo_element(pseudo_element.value()))
return true;
}
}
return false;
}
Optional<SelectorList> Parser::parse_as_selector(SelectorParsingMode parsing_mode)
{
auto selector_list = parse_a_selector_list(m_token_stream, SelectorType::Standalone, parsing_mode);
if (!selector_list.is_error())
return selector_list.release_value();
return {};
}
Optional<SelectorList> Parser::parse_as_relative_selector(SelectorParsingMode parsing_mode)
{
auto selector_list = parse_a_selector_list(m_token_stream, SelectorType::Relative, parsing_mode);
if (!selector_list.is_error())
return selector_list.release_value();
return {};
}
Optional<Selector::PseudoElementSelector> Parser::parse_as_pseudo_element_selector()
{
auto component_values = consume_a_list_of_component_values(m_token_stream);
TokenStream tokens { component_values };
auto maybe_simple_selector = parse_pseudo_element_simple_selector(tokens);
if (maybe_simple_selector.is_error())
return {};
if (tokens.has_next_token())
return {};
auto simple_selector = maybe_simple_selector.release_value();
if (simple_selector.type != Selector::SimpleSelector::Type::PseudoElement)
return {};
return simple_selector.pseudo_element();
}
static NonnullRefPtr<Selector> create_invalid_selector(Selector::Combinator combinator, Vector<ComponentValue> component_values)
{
// Trim leading and trailing whitespace
while (!component_values.is_empty() && component_values.first().is(Token::Type::Whitespace)) {
component_values.take_first();
}
while (!component_values.is_empty() && component_values.last().is(Token::Type::Whitespace)) {
component_values.take_last();
}
Selector::SimpleSelector simple {
.type = Selector::SimpleSelector::Type::Invalid,
.value = Selector::SimpleSelector::Invalid {
.component_values = move(component_values),
}
};
Selector::CompoundSelector compound {
.combinator = combinator,
.simple_selectors = { move(simple) }
};
return Selector::create({ move(compound) });
}
template<typename T>
Parser::ParseErrorOr<SelectorList> Parser::parse_a_selector_list(TokenStream<T>& tokens, SelectorType mode, SelectorParsingMode parsing_mode)
{
SelectorList selectors;
for (;;) {
auto selector_parts = consume_a_list_of_component_values(tokens, Token::Type::Comma);
auto stream = TokenStream(selector_parts);
auto selector = parse_complex_selector(stream, mode);
if (selector.is_error()) {
if (parsing_mode == SelectorParsingMode::Forgiving) {
// Keep the invalid selector around for serialization and nesting
auto combinator = mode == SelectorType::Standalone ? Selector::Combinator::None : Selector::Combinator::Descendant;
selectors.append(create_invalid_selector(combinator, move(selector_parts)));
} else {
return selector.error();
}
} else {
selectors.append(selector.release_value());
}
if (tokens.is_empty())
break;
tokens.discard_a_token();
}
if (selectors.is_empty() && parsing_mode != SelectorParsingMode::Forgiving)
return ParseError::SyntaxError;
return selectors;
}
template Parser::ParseErrorOr<SelectorList> Parser::parse_a_selector_list(TokenStream<ComponentValue>&, SelectorType, SelectorParsingMode);
template Parser::ParseErrorOr<SelectorList> Parser::parse_a_selector_list(TokenStream<Token>&, SelectorType, SelectorParsingMode);
Parser::ParseErrorOr<NonnullRefPtr<Selector>> Parser::parse_complex_selector(TokenStream<ComponentValue>& tokens, SelectorType mode)
{
Vector<Selector::CompoundSelector> compound_selectors;
auto first_combinator = parse_selector_combinator(tokens);
switch (mode) {
case SelectorType::Standalone: {
// Standalone selectors can't start with a combinator.
// Whitespace, which gets parsed as a descendant combinator, is instead treated as None.
if (first_combinator.has_value() && first_combinator != Selector::Combinator::Descendant) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = "Standalone selector starts with a combinator, which is invalid."_string,
});
return ParseError::SyntaxError;
}
first_combinator = Selector::Combinator::None;
break;
}
case SelectorType::Relative:
// Relative selectors default to starting with a descendant combinator.
if (!first_combinator.has_value())
first_combinator = Selector::Combinator::Descendant;
break;
}
auto first_selector = TRY(parse_compound_selector(tokens));
if (first_selector.simple_selectors.is_empty()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = "Failed to parse first compound-selector."_string,
});
return ParseError::SyntaxError;
}
first_selector.combinator = first_combinator.value_or(Selector::Combinator::None);
compound_selectors.append(move(first_selector));
while (tokens.has_next_token()) {
auto combinator = parse_selector_combinator(tokens);
if (!combinator.has_value())
break;
auto compound_selector = TRY(parse_compound_selector(tokens));
if (compound_selector.simple_selectors.is_empty()) {
if (tokens.has_next_token() || combinator != Selector::Combinator::Descendant) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = "Compound-selector is empty."_string,
});
return ParseError::SyntaxError;
}
break;
}
compound_selector.combinator = combinator.release_value();
compound_selectors.append(move(compound_selector));
}
if (compound_selectors.is_empty()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = "Selector contains no compound-selectors."_string,
});
return ParseError::SyntaxError;
}
if (tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = "Not all tokens were consumed."_string,
});
return ParseError::SyntaxError;
}
auto parsed_selector = Selector::create(move(compound_selectors));
// The rest of our code assumes selectors have at most 1 pseudo-element, in the final compound selector,
// so reject anything else for now.
// FIXME: Remove this once we support them elsewhere.
for (auto i = 0u; i < parsed_selector->compound_selectors().size() - 1; ++i) {
for (auto const& simple_selector : parsed_selector->compound_selectors()[i].simple_selectors) {
if (simple_selector.type == Selector::SimpleSelector::Type::PseudoElement) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = parsed_selector->serialize(),
.description = "Pseudo elements before the final compound-selector are not yet supported."_string,
});
return ParseError::SyntaxError;
}
}
}
// https://drafts.csswg.org/css-shadow-1/#selectordef-part
// The ::part() pseudo-element can be followed by other pseudo-elements to style pseudo-elements of the part itself.
auto pseudo_element_count = 0;
Optional<PseudoElement> first_pseudo_element;
Optional<PseudoElement> second_pseudo_element;
for (auto const& simple_selector : parsed_selector->compound_selectors().last().simple_selectors) {
if (simple_selector.type == Selector::SimpleSelector::Type::PseudoElement) {
++pseudo_element_count;
if (!first_pseudo_element.has_value())
first_pseudo_element = simple_selector.pseudo_element().type();
else if (!second_pseudo_element.has_value())
second_pseudo_element = simple_selector.pseudo_element().type();
}
}
if (pseudo_element_count > 1) {
// FIXME: Other pseudo-elements can also be chained (e.g. ::highlight()::before).
// For now, only ::part() followed by one other pseudo-element is supported.
bool is_valid_chain = first_pseudo_element.value() == PseudoElement::Part
&& second_pseudo_element.value() != PseudoElement::Part
&& pseudo_element_count == 2;
if (!is_valid_chain) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = parsed_selector->serialize(),
.description = "Pseudo-element chaining is not yet supported for this combination."_string,
});
return ParseError::SyntaxError;
}
}
return parsed_selector;
}
Parser::ParseErrorOr<Selector::CompoundSelector> Parser::parse_compound_selector(TokenStream<ComponentValue>& tokens)
{
Vector<Selector::SimpleSelector> simple_selectors;
while (tokens.has_next_token()) {
auto component = TRY(parse_simple_selector(tokens));
if (!component.has_value())
break;
if (component->type == Selector::SimpleSelector::Type::TagName && !simple_selectors.is_empty()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = "Tag-name selectors can only go at the beginning of a compound selector."_string,
});
return ParseError::SyntaxError;
}
simple_selectors.append(component.release_value());
}
return Selector::CompoundSelector { Selector::Combinator::None, move(simple_selectors) };
}
Optional<Selector::Combinator> Parser::parse_selector_combinator(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
bool had_initial_whitespace = tokens.next_token().is(Token::Type::Whitespace);
tokens.discard_whitespace();
auto const& next = tokens.next_token();
auto consume_single_delim_combinator = [&](auto combinator) {
tokens.discard_a_token();
tokens.discard_whitespace();
transaction.commit();
return combinator;
};
if (next.is_delim('>'))
return consume_single_delim_combinator(Selector::Combinator::ImmediateChild);
if (next.is_delim('+'))
return consume_single_delim_combinator(Selector::Combinator::NextSibling);
if (next.is_delim('~'))
return consume_single_delim_combinator(Selector::Combinator::SubsequentSibling);
if (next.is_delim('|') && tokens.peek_token(1).is_delim('|')) {
tokens.discard_a_token(); // |
tokens.discard_a_token(); // |
tokens.discard_whitespace();
transaction.commit();
return Selector::Combinator::Column;
}
if (had_initial_whitespace) {
transaction.commit();
return Selector::Combinator::Descendant;
}
return {};
}
Optional<Selector::SimpleSelector::QualifiedName> Parser::parse_selector_qualified_name(TokenStream<ComponentValue>& tokens, AllowWildcardName allow_wildcard_name)
{
auto is_name = [](ComponentValue const& token) {
return token.is_delim('*') || token.is(Token::Type::Ident);
};
auto get_name = [](ComponentValue const& token) {
if (token.is_delim('*'))
return "*"_fly_string;
return token.token().ident();
};
// There are 3 possibilities here:
// (Where <name> and <namespace> are either an <ident> or a `*` delim)
// 1) `|<name>`
// 2) `<namespace>|<name>`
// 3) `<name>`
// Whitespace is forbidden between any of these parts. https://www.w3.org/TR/selectors-4/#white-space
auto transaction = tokens.begin_transaction();
auto const& first_token = tokens.consume_a_token();
if (first_token.is_delim('|')) {
// Case 1: `|<name>`
if (is_name(tokens.next_token())) {
auto const& name_token = tokens.consume_a_token();
if (allow_wildcard_name == AllowWildcardName::No && name_token.is_delim('*'))
return {};
transaction.commit();
return Selector::SimpleSelector::QualifiedName {
.namespace_type = Selector::SimpleSelector::QualifiedName::NamespaceType::None,
.name = get_name(name_token),
};
}
return {};
}
if (!is_name(first_token))
return {};
if (tokens.next_token().is_delim('|') && is_name(tokens.peek_token(1))) {
// Case 2: `<namespace>|<name>`
tokens.discard_a_token(); // `|`
auto namespace_ = get_name(first_token);
auto name = get_name(tokens.consume_a_token());
if (allow_wildcard_name == AllowWildcardName::No && name == "*"sv)
return {};
auto namespace_type = namespace_ == "*"sv
? Selector::SimpleSelector::QualifiedName::NamespaceType::Any
: Selector::SimpleSelector::QualifiedName::NamespaceType::Named;
// https://www.w3.org/TR/selectors-4/#invalid
// a simple selector containing an undeclared namespace prefix is invalid
if (namespace_type == Selector::SimpleSelector::QualifiedName::NamespaceType::Named && !m_declared_namespaces.contains(namespace_))
return {};
transaction.commit();
return Selector::SimpleSelector::QualifiedName {
.namespace_type = namespace_type,
.namespace_ = namespace_,
.name = name,
};
}
// Case 3: `<name>`
auto& name_token = first_token;
if (allow_wildcard_name == AllowWildcardName::No && name_token.is_delim('*'))
return {};
transaction.commit();
return Selector::SimpleSelector::QualifiedName {
.namespace_type = Selector::SimpleSelector::QualifiedName::NamespaceType::Default,
.name = get_name(name_token),
};
}
Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_attribute_simple_selector(ComponentValue const& first_value)
{
auto attribute_tokens = TokenStream { first_value.block().value };
attribute_tokens.discard_whitespace();
if (!attribute_tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = "Attribute selector is empty."_string,
});
return ParseError::SyntaxError;
}
auto maybe_qualified_name = parse_selector_qualified_name(attribute_tokens, AllowWildcardName::No);
if (!maybe_qualified_name.has_value()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Expected qualified-name, got: '{}'.", attribute_tokens.next_token().to_debug_string())),
});
return ParseError::SyntaxError;
}
auto qualified_name = maybe_qualified_name.release_value();
Selector::SimpleSelector simple_selector {
.type = Selector::SimpleSelector::Type::Attribute,
.value = Selector::SimpleSelector::Attribute {
.match_type = Selector::SimpleSelector::Attribute::MatchType::HasAttribute,
.qualified_name = qualified_name,
.case_type = Selector::SimpleSelector::Attribute::CaseType::DefaultMatch,
}
};
attribute_tokens.discard_whitespace();
if (!attribute_tokens.has_next_token())
return simple_selector;
auto parse_attribute_match_type = [&first_value](auto& tokens) -> ParseErrorOr<Selector::SimpleSelector::Attribute::MatchType> {
// This is one of: `=`, `~=`, `*=`, `|=`, `^=`, `$=`
auto transaction = tokens.begin_transaction();
auto const& first_delim = tokens.consume_a_token();
if (!first_delim.is(Token::Type::Delim)) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Expected delim for attribute comparison, got: '{}'.", first_delim.to_debug_string())),
});
return ParseError::SyntaxError;
}
if (first_delim.token().delim() == '=') {
transaction.commit();
return Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch;
}
if (!tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = "Attribute selector ended part way through a match type."_string,
});
return ParseError::SyntaxError;
}
auto const& second_delim = tokens.consume_a_token();
if (!second_delim.is_delim('=')) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Expected a double delim for attribute comparison, got: '{}{}'.", first_delim.to_debug_string(), second_delim.to_debug_string())),
});
return ParseError::SyntaxError;
}
switch (first_delim.token().delim()) {
case '~':
transaction.commit();
return Selector::SimpleSelector::Attribute::MatchType::ContainsWord;
case '*':
transaction.commit();
return Selector::SimpleSelector::Attribute::MatchType::ContainsString;
case '|':
transaction.commit();
return Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment;
case '^':
transaction.commit();
return Selector::SimpleSelector::Attribute::MatchType::StartsWithString;
case '$':
transaction.commit();
return Selector::SimpleSelector::Attribute::MatchType::EndsWithString;
default:
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Invalid attribute selector match type `{:c}=`", first_delim.token().delim())),
});
return ParseError::SyntaxError;
}
};
simple_selector.attribute().match_type = TRY(parse_attribute_match_type(attribute_tokens));
attribute_tokens.discard_whitespace();
if (!attribute_tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = "Attribute selector ended without a value to match."_string,
});
return ParseError::SyntaxError;
}
auto const& value_part = attribute_tokens.consume_a_token();
if (!value_part.is(Token::Type::Ident) && !value_part.is(Token::Type::String)) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Expected a string or ident for the value to match attribute against, got: '{}'.", value_part.to_debug_string())),
});
return ParseError::SyntaxError;
}
auto const& value_string = value_part.token().is(Token::Type::Ident) ? value_part.token().ident() : value_part.token().string();
simple_selector.attribute().value = value_string.to_string();
attribute_tokens.discard_whitespace();
// Handle case-sensitivity suffixes. https://www.w3.org/TR/selectors-4/#attribute-case
if (attribute_tokens.has_next_token()) {
auto const& case_sensitivity_part = attribute_tokens.consume_a_token();
if (case_sensitivity_part.is(Token::Type::Ident)) {
auto case_sensitivity = case_sensitivity_part.token().ident();
if (case_sensitivity.equals_ignoring_ascii_case("i"sv)) {
simple_selector.attribute().case_type = Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch;
} else if (case_sensitivity.equals_ignoring_ascii_case("s"sv)) {
simple_selector.attribute().case_type = Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch;
} else {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Expected a \"i\" or \"s\" attribute selector case sensitivity identifier, got: '{}'.", case_sensitivity_part.to_debug_string())),
});
return ParseError::SyntaxError;
}
} else {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = MUST(String::formatted("Expected an attribute selector case sensitivity identifier, got: '{}'", case_sensitivity_part.to_debug_string())),
});
return ParseError::SyntaxError;
}
}
attribute_tokens.discard_whitespace();
if (attribute_tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = first_value.to_string(),
.description = "Trailing tokens in attribute selector."_string,
});
return ParseError::SyntaxError;
}
return simple_selector;
}
Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_class_simple_selector(TokenStream<ComponentValue>& tokens)
{
auto peek_token_ends_selector = [&]() -> bool {
auto const& value = tokens.next_token();
return value.is(Token::Type::EndOfFile) || value.is(Token::Type::Whitespace) || value.is(Token::Type::Comma);
};
if (peek_token_ends_selector())
return ParseError::SyntaxError;
if (!tokens.consume_a_token().is(Token::Type::Colon))
return ParseError::SyntaxError;
if (peek_token_ends_selector())
return ParseError::SyntaxError;
if (tokens.next_token().is(Token::Type::Ident)) {
auto pseudo_name = tokens.consume_a_token().token().ident();
if (auto pseudo_class = pseudo_class_from_string(pseudo_name); pseudo_class.has_value()) {
if (!pseudo_class_metadata(pseudo_class.value()).is_valid_as_identifier) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_name)),
.value_string = pseudo_name.to_string(),
.description = "Only valid as a function."_string,
});
return ParseError::SyntaxError;
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector { .type = pseudo_class.value() }
};
}
if (has_ignored_vendor_prefix(pseudo_name))
return ParseError::IncludesIgnoredVendorPrefix;
ErrorReporter::the().report(UnknownPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_name)),
});
return ParseError::SyntaxError;
}
if (tokens.next_token().is_function()) {
auto parse_an_plus_b_selector = [this](auto pseudo_class, Vector<ComponentValue> const& function_values, bool allow_of = false) -> ParseErrorOr<Selector::SimpleSelector> {
TokenStream tokens { function_values };
auto an_plus_b_pattern = parse_a_n_plus_b_pattern(tokens);
if (!an_plus_b_pattern.has_value()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_class_name(pseudo_class))),
.value_string = tokens.dump_string(),
.description = "Invalid An+B format."_string,
});
return ParseError::SyntaxError;
}
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.an_plus_b_pattern = an_plus_b_pattern.release_value() }
};
}
if (!allow_of)
return ParseError::SyntaxError;
// Parse the `of <selector-list>` syntax
auto const& maybe_of = tokens.consume_a_token();
if (!maybe_of.is_ident("of"sv))
return ParseError::SyntaxError;
tokens.discard_whitespace();
auto selector_list = TRY(parse_a_selector_list(tokens, SelectorType::Standalone));
tokens.discard_whitespace();
if (tokens.has_next_token())
return ParseError::SyntaxError;
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.an_plus_b_pattern = an_plus_b_pattern.release_value(),
.argument_selector_list = move(selector_list) }
};
};
auto const& pseudo_function = tokens.consume_a_token().function();
auto maybe_pseudo_class = pseudo_class_from_string(pseudo_function.name);
if (!maybe_pseudo_class.has_value()) {
ErrorReporter::the().report(UnknownPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
});
return ParseError::SyntaxError;
}
auto pseudo_class = maybe_pseudo_class.value();
auto metadata = pseudo_class_metadata(pseudo_class);
if (!metadata.is_valid_as_function) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Not valid as a function."_string,
});
return ParseError::SyntaxError;
}
if (pseudo_function.value.is_empty()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Missing arguments."_string,
});
return ParseError::SyntaxError;
}
// "The :has() pseudo-class cannot be nested; :has() is not valid within :has()."
// https://drafts.csswg.org/selectors/#relational
if (pseudo_class == PseudoClass::Has && m_pseudo_class_context.contains_slow(PseudoClass::Has)) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = ":has() is not allowed inside :has()."_string,
});
return ParseError::SyntaxError;
}
m_pseudo_class_context.append(pseudo_class);
ScopeGuard guard = [&] { m_pseudo_class_context.take_last(); };
switch (metadata.parameter_type) {
case PseudoClassMetadata::ParameterType::ANPlusB:
return parse_an_plus_b_selector(pseudo_class, pseudo_function.value, false);
case PseudoClassMetadata::ParameterType::ANPlusBOf:
return parse_an_plus_b_selector(pseudo_class, pseudo_function.value, true);
case PseudoClassMetadata::ParameterType::CompoundSelector: {
auto function_token_stream = TokenStream(pseudo_function.value);
auto compound_selector_or_error = parse_compound_selector(function_token_stream);
if (compound_selector_or_error.is_error()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Failed to parse argument as a compound selector."_string,
});
return ParseError::SyntaxError;
}
function_token_stream.discard_whitespace();
if (function_token_stream.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Trailing tokens after compound selector argument."_string,
});
return ParseError::SyntaxError;
}
auto compound_selector = compound_selector_or_error.release_value();
compound_selector.combinator = Selector::Combinator::None;
auto selector = Selector::create(Vector { move(compound_selector) });
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.argument_selector_list = { move(selector) } }
};
}
case PseudoClassMetadata::ParameterType::ForgivingRelativeSelectorList:
case PseudoClassMetadata::ParameterType::ForgivingSelectorList: {
auto function_token_stream = TokenStream(pseudo_function.value);
auto selector_type = metadata.parameter_type == PseudoClassMetadata::ParameterType::ForgivingSelectorList
? SelectorType::Standalone
: SelectorType::Relative;
// NOTE: Because it's forgiving, even complete garbage will parse OK as an empty selector-list.
auto argument_selector_list = MUST(parse_a_selector_list(function_token_stream, selector_type, SelectorParsingMode::Forgiving));
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.is_forgiving = true,
.argument_selector_list = move(argument_selector_list) }
};
}
case PseudoClassMetadata::ParameterType::Ident: {
auto function_token_stream = TokenStream(pseudo_function.value);
function_token_stream.discard_whitespace();
auto const& maybe_ident_token = function_token_stream.consume_a_token();
function_token_stream.discard_whitespace();
if (!maybe_ident_token.is(Token::Type::Ident) || function_token_stream.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Failed to parse argument as an ident."_string,
});
return ParseError::SyntaxError;
}
auto& ident = maybe_ident_token.token().ident();
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.ident = Selector::SimpleSelector::PseudoClassSelector::Ident {
.keyword = keyword_from_string(ident).value_or(Keyword::Invalid),
.string_value = ident,
},
}
};
}
case PseudoClassMetadata::ParameterType::LanguageRanges: {
Vector<FlyString> languages;
auto function_token_stream = TokenStream(pseudo_function.value);
auto language_token_lists = parse_a_comma_separated_list_of_component_values(function_token_stream);
for (auto const& language_token_list : language_token_lists) {
auto language_token_stream = TokenStream(language_token_list);
language_token_stream.discard_whitespace();
auto const& language_token = language_token_stream.consume_a_token();
if (!(language_token.is(Token::Type::Ident) || language_token.is(Token::Type::String))) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Failed to parse argument as a language range: Not a string/ident."_string,
});
return ParseError::SyntaxError;
}
auto language_string = language_token.is(Token::Type::String) ? language_token.token().string() : language_token.token().ident();
languages.append(language_string);
language_token_stream.discard_whitespace();
if (language_token_stream.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Failed to parse argument as a language range: Has trailing tokens."_string,
});
return ParseError::SyntaxError;
}
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.languages = move(languages) }
};
}
case PseudoClassMetadata::ParameterType::LevelList: {
// https://drafts.csswg.org/selectors-5/#heading-functional-pseudo
// :heading() = :heading( <level># )
// where <level> is a <number-token> with its type flag set to "integer".
Vector<i64> levels;
auto function_token_stream = TokenStream(pseudo_function.value);
auto level_lists = parse_a_comma_separated_list_of_component_values(function_token_stream);
for (auto const& level_tokens : level_lists) {
TokenStream level_token_stream { level_tokens };
level_token_stream.discard_whitespace();
auto& maybe_integer = level_token_stream.consume_a_token();
level_token_stream.discard_whitespace();
if (!maybe_integer.is(Token::Type::Number) || !maybe_integer.token().number().is_integer()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Failed to parse argument as a <level>: Not an <integer> literal."_string,
});
return ParseError::SyntaxError;
}
if (level_token_stream.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_function.name)),
.value_string = pseudo_function.name.to_string(),
.description = "Failed to parse argument as a <level>: Has trailing tokens."_string,
});
return ParseError::SyntaxError;
}
levels.append(maybe_integer.token().number().integer_value());
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.levels = move(levels),
}
};
}
case PseudoClassMetadata::ParameterType::RelativeSelectorList:
case PseudoClassMetadata::ParameterType::SelectorList: {
auto function_token_stream = TokenStream(pseudo_function.value);
auto selector_type = metadata.parameter_type == PseudoClassMetadata::ParameterType::SelectorList
? SelectorType::Standalone
: SelectorType::Relative;
auto not_selector = TRY(parse_a_selector_list(function_token_stream, selector_type));
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoClass,
.value = Selector::SimpleSelector::PseudoClassSelector {
.type = pseudo_class,
.argument_selector_list = move(not_selector) }
};
}
case PseudoClassMetadata::ParameterType::None:
// `None` means this is not a function-type pseudo-class, so this state should be impossible.
VERIFY_NOT_REACHED();
}
}
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.next_token().to_string(),
.description = MUST(String::formatted("Pseudo-class should be an ident or function, got: '{}'", tokens.next_token().to_debug_string())),
});
return ParseError::SyntaxError;
}
Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_element_simple_selector(TokenStream<ComponentValue>& tokens)
{
auto peek_token_ends_selector = [&]() -> bool {
auto const& value = tokens.next_token();
return value.is(Token::Type::EndOfFile) || value.is(Token::Type::Whitespace) || value.is(Token::Type::Comma);
};
if (peek_token_ends_selector())
return ParseError::SyntaxError;
if (!tokens.consume_a_token().is(Token::Type::Colon))
return ParseError::SyntaxError;
// A few pseudo-elements are allowed to have a single colon, like a pseudo-class.
bool const started_with_double_colon = tokens.next_token().is(Token::Type::Colon);
if (started_with_double_colon)
tokens.discard_a_token(); // :
auto const& name_token = tokens.consume_a_token();
bool is_function = false;
FlyString pseudo_name;
if (name_token.is(Token::Type::Ident)) {
pseudo_name = name_token.token().ident();
} else if (name_token.is_function()) {
pseudo_name = name_token.function().name;
is_function = true;
} else {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = name_token.to_string(),
.description = MUST(String::formatted("Pseudo-element should be an ident or function, got: '{}'", name_token.to_debug_string())),
});
return ParseError::SyntaxError;
}
bool is_aliased_pseudo = false;
auto pseudo_element = pseudo_element_from_string(pseudo_name);
if (!pseudo_element.has_value()) {
pseudo_element = aliased_pseudo_element_from_string(pseudo_name);
is_aliased_pseudo = pseudo_element.has_value();
}
if (pseudo_element.has_value()) {
// :has() is fussy about pseudo-elements inside it
if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(*pseudo_element)) {
return ParseError::SyntaxError;
}
// Support legacy single-colon syntax for some older pseudo-elements.
if (!started_with_double_colon) {
if (is_legacy_single_colon_pseudo_element(pseudo_element.value())) {
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement,
.value = Selector::PseudoElementSelector { pseudo_element.value() }
};
}
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted(":{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "This is not a legacy pseudo-element."_string,
});
return ParseError::SyntaxError;
}
auto metadata = pseudo_element_metadata(*pseudo_element);
Selector::PseudoElementSelector::Value value = Empty {};
if (is_function) {
if (!metadata.is_valid_as_function) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Not valid as a function."_string,
});
return ParseError::SyntaxError;
}
// Parse arguments
TokenStream function_tokens { name_token.function().value };
function_tokens.discard_whitespace();
switch (metadata.parameter_type) {
case PseudoElementMetadata::ParameterType::None:
if (function_tokens.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Should have no arguments."_string,
});
return ParseError::SyntaxError;
}
break;
case PseudoElementMetadata::ParameterType::CompoundSelector: {
auto compound_selector_or_error = parse_compound_selector(function_tokens);
if (compound_selector_or_error.is_error()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Failed to parse argument as a compound selector."_string,
});
return ParseError::SyntaxError;
}
function_tokens.discard_whitespace();
if (function_tokens.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Trailing tokens after compound selector argument."_string,
});
return ParseError::SyntaxError;
}
auto compound_selector = compound_selector_or_error.release_value();
compound_selector.combinator = Selector::Combinator::None;
value = Selector::create(Vector { move(compound_selector) });
break;
}
case PseudoElementMetadata::ParameterType::IdentList: {
// <ident>+
Selector::PseudoElementSelector::IdentList idents;
while (function_tokens.has_next_token()) {
if (!function_tokens.next_token().is(Token::Type::Ident)) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Contains invalid <ident>."_string,
});
return ParseError::SyntaxError;
}
idents.append(function_tokens.consume_a_token().token().ident());
function_tokens.discard_whitespace();
}
value = move(idents);
break;
}
case PseudoElementMetadata::ParameterType::PTNameSelector: {
// <pt-name-selector> = '*' | <custom-ident>
// https://drafts.csswg.org/css-view-transitions-1/#typedef-pt-name-selector
if (function_tokens.next_token().is_delim('*')) {
function_tokens.discard_a_token(); // *
value = Selector::PseudoElementSelector::PTNameSelector { .is_universal = true };
} else if (auto custom_ident = parse_custom_ident(function_tokens, {}); custom_ident.has_value()) {
value = Selector::PseudoElementSelector::PTNameSelector { .value = custom_ident.release_value() };
} else {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = MUST(String::formatted("Invalid <pt-name-selector> - expected `*` or `<custom-ident>`, got `{}`", function_tokens.next_token().to_debug_string())),
});
return ParseError::SyntaxError;
}
function_tokens.discard_whitespace();
if (function_tokens.has_next_token()) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Invalid <pt-name-selector> - trailing tokens."_string,
});
return ParseError::SyntaxError;
}
break;
}
}
} else {
if (!metadata.is_valid_as_identifier) {
ErrorReporter::the().report(InvalidPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
.value_string = name_token.to_string(),
.description = "Only valid as a function."_string,
});
return ParseError::SyntaxError;
}
}
// NB: Aliased pseudo-elements behave like their target pseudo-element, but serialize as themselves. So store
// their name like we do for unknown -webkit pseudos below.
if (is_aliased_pseudo) {
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement,
.value = Selector::PseudoElementSelector { pseudo_element.release_value(), pseudo_name.to_string().to_ascii_lowercase(), move(value) }
};
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement,
.value = Selector::PseudoElementSelector { pseudo_element.release_value(), move(value) }
};
}
// https://drafts.csswg.org/selectors-4/#compat
// All other pseudo-elements whose names begin with the string “-webkit-” (matched ASCII case-insensitively)
// and that are not functional notations must be treated as valid at parse time. (That is, ::-webkit-asdf is
// valid at parse time, but ::-webkit-jkl() is not.) If theyre not otherwise recognized and supported, they
// must be treated as matching nothing, and are unknown -webkit- pseudo-elements.
if (!is_function && pseudo_name.starts_with_bytes("-webkit-"sv, CaseSensitivity::CaseInsensitive)) {
// NB: :has() only allows a limited set of pseudo-elements inside it, which doesn't include unknown ones.
if (m_pseudo_class_context.contains_slow(PseudoClass::Has))
return ParseError::SyntaxError;
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::PseudoElement,
// Unknown -webkit- pseudo-elements must be serialized in ASCII lowercase.
.value = Selector::PseudoElementSelector { PseudoElement::UnknownWebKit, pseudo_name.to_string().to_ascii_lowercase() },
};
}
if (has_ignored_vendor_prefix(pseudo_name))
return ParseError::IncludesIgnoredVendorPrefix;
ErrorReporter::the().report(UnknownPseudoClassOrElementError {
.name = MUST(String::formatted("::{}", pseudo_name)),
});
return ParseError::SyntaxError;
}
Parser::ParseErrorOr<Optional<Selector::SimpleSelector>> Parser::parse_simple_selector(TokenStream<ComponentValue>& tokens)
{
auto peek_token_ends_selector = [&]() -> bool {
auto const& value = tokens.next_token();
return (value.is(Token::Type::EndOfFile) || value.is(Token::Type::Whitespace) || value.is(Token::Type::Comma));
};
if (peek_token_ends_selector())
return Optional<Selector::SimpleSelector> {};
// Handle universal and tag-name types together, since both can be namespaced
if (auto qualified_name = parse_selector_qualified_name(tokens, AllowWildcardName::Yes); qualified_name.has_value()) {
if (qualified_name->name.name == "*"sv) {
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::Universal,
.value = qualified_name.release_value(),
};
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::TagName,
.value = qualified_name.release_value(),
};
}
if (next_is_pseudo_element(tokens))
return TRY(parse_pseudo_element_simple_selector(tokens));
if (tokens.next_token().is(Token::Type::Colon))
return TRY(parse_pseudo_class_simple_selector(tokens));
if (tokens.next_token().is(Token::Type::Delim)
&& first_is_one_of(static_cast<char>(tokens.next_token().token().delim()), '>', '+', '~', '|')) {
// Whitespace is not required between the compound-selector and a combinator.
// So, if we see a combinator, return that this compound-selector is done, instead of a syntax error.
return Optional<Selector::SimpleSelector> {};
}
auto const& first_value = tokens.consume_a_token();
if (first_value.is(Token::Type::Delim)) {
u32 delim = first_value.token().delim();
switch (delim) {
case '*':
// Handled already
VERIFY_NOT_REACHED();
case '&':
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::Nesting,
};
case '.': {
if (peek_token_ends_selector())
return ParseError::SyntaxError;
auto const& class_name_value = tokens.consume_a_token();
if (!class_name_value.is(Token::Type::Ident)) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = MUST(String::formatted("Expected an ident after '.', got: {}", class_name_value.to_debug_string())),
});
return ParseError::SyntaxError;
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::Class,
.value = Selector::SimpleSelector::Name { class_name_value.token().ident() }
};
}
default:
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = MUST(String::formatted("Unrecognized delimiter: {}", first_value.token().to_string())),
});
return ParseError::SyntaxError;
}
}
if (first_value.is(Token::Type::Hash)) {
if (first_value.token().hash_type() != Token::HashType::Id) {
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = MUST(String::formatted("Hash token is not an id: {}", first_value.to_debug_string())),
});
return ParseError::SyntaxError;
}
return Selector::SimpleSelector {
.type = Selector::SimpleSelector::Type::Id,
.value = Selector::SimpleSelector::Name { first_value.token().hash_value() }
};
}
if (first_value.is_block() && first_value.block().is_square())
return TRY(parse_attribute_simple_selector(first_value));
ErrorReporter::the().report(InvalidSelectorError {
.value_string = tokens.dump_string(),
.description = MUST(String::formatted("Invalid start of a simple selector: {}", first_value.to_debug_string())),
});
return ParseError::SyntaxError;
}
// https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
Optional<Selector::SimpleSelector::ANPlusBPattern> Parser::parse_a_n_plus_b_pattern(TokenStream<ComponentValue>& values)
{
auto transaction = values.begin_transaction();
auto is_sign = [](ComponentValue const& value) -> bool {
return value.is(Token::Type::Delim) && (value.token().delim() == '+' || value.token().delim() == '-');
};
auto is_series_of_1_or_more_digits = [](StringView string) -> bool {
if (string.is_empty())
return false;
for (char c : string) {
if (!is_ascii_digit(c))
return false;
}
return true;
};
// <n-dimension> is a <dimension-token> with its type flag set to "integer", and a unit that is an ASCII
// case-insensitive match for "n"
auto is_n_dimension = [](ComponentValue const& value) -> bool {
return value.is(Token::Type::Dimension)
&& value.token().number().is_integer()
&& value.token().dimension_unit().equals_ignoring_ascii_case("n"sv);
};
// <ndash-dimension> is a <dimension-token> with its type flag set to "integer", and a unit that is an ASCII
// case-insensitive match for "n-"
auto is_ndash_dimension = [](ComponentValue const& value) -> bool {
return value.is(Token::Type::Dimension)
&& value.token().number().is_integer()
&& value.token().dimension_unit().equals_ignoring_ascii_case("n-"sv);
};
// <ndashdigit-dimension> is a <dimension-token> with its type flag set to "integer", and a unit that is an ASCII
// case-insensitive match for "n-*", where "*" is a series of one or more digits
auto is_ndashdigit_dimension = [&](ComponentValue const& value) -> bool {
return value.is(Token::Type::Dimension)
&& value.token().number().is_integer()
&& value.token().dimension_unit().starts_with_bytes("n-"sv, CaseSensitivity::CaseInsensitive)
&& is_series_of_1_or_more_digits(value.token().dimension_unit().bytes_as_string_view().substring_view(2));
};
// <ndashdigit-ident> is an <ident-token> whose value is an ASCII case-insensitive match for "n-*", where "*" is a
// series of one or more digits
auto is_ndashdigit_ident = [&](ComponentValue const& value) -> bool {
return value.is(Token::Type::Ident)
&& value.token().ident().starts_with_bytes("n-"sv, CaseSensitivity::CaseInsensitive)
&& is_series_of_1_or_more_digits(value.token().ident().bytes_as_string_view().substring_view(2));
};
// <dashndashdigit-ident> is an <ident-token> whose value is an ASCII case-insensitive match for "-n-*", where "*"
// is a series of one or more digits
auto is_dashndashdigit_ident = [&](ComponentValue const& value) -> bool {
return value.is(Token::Type::Ident)
&& value.token().ident().starts_with_bytes("-n-"sv, CaseSensitivity::CaseInsensitive)
&& is_series_of_1_or_more_digits(value.token().ident().bytes_as_string_view().substring_view(3));
};
// <integer> is a <number-token> with its type flag set to "integer"
auto is_integer = [](ComponentValue const& value) -> bool {
return value.is(Token::Type::Number) && value.token().number().is_integer();
};
// <signed-integer> is a <number-token> with its type flag set to "integer", and a sign character
auto is_signed_integer = [](ComponentValue const& value) -> bool {
return value.is(Token::Type::Number) && value.token().number().is_integer_with_explicit_sign();
};
// <signless-integer> is a <number-token> with its type flag set to "integer", and no sign character
auto is_signless_integer = [](ComponentValue const& value) -> bool {
return value.is(Token::Type::Number)
&& value.token().number().is_integer()
&& !value.token().number().is_integer_with_explicit_sign();
};
values.discard_whitespace();
// odd | even
if (values.next_token().is_ident("odd"sv)) {
values.discard_a_token(); // odd
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 2, 1 };
}
if (values.next_token().is_ident("even"sv)) {
values.discard_a_token(); // even
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 2, 0 };
}
// <integer>
if (is_integer(values.next_token())) {
int b = values.consume_a_token().token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 0, b };
}
// <n-dimension>
// <n-dimension> <signed-integer>
// <n-dimension> ['+' | '-'] <signless-integer>
if (is_n_dimension(values.next_token())) {
int a = values.consume_a_token().token().dimension_value_int();
values.discard_whitespace();
// <n-dimension> <signed-integer>
if (is_signed_integer(values.next_token())) {
int b = values.consume_a_token().token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { a, b };
}
// <n-dimension> ['+' | '-'] <signless-integer>
{
auto child_transaction = transaction.create_child();
auto const& second_value = values.consume_a_token();
values.discard_whitespace();
auto const& third_value = values.consume_a_token();
if (is_sign(second_value) && is_signless_integer(third_value)) {
int b = third_value.token().to_integer() * (second_value.is_delim('+') ? 1 : -1);
child_transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { a, b };
}
}
// <n-dimension>
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { a, 0 };
}
// <ndash-dimension> <signless-integer>
if (is_ndash_dimension(values.next_token())) {
values.discard_whitespace();
auto const& first_value = values.consume_a_token();
auto const& second_value = values.consume_a_token();
if (is_signless_integer(second_value)) {
int a = first_value.token().dimension_value_int();
int b = -second_value.token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { a, b };
}
return {};
}
// <ndashdigit-dimension>
if (is_ndashdigit_dimension(values.next_token())) {
auto const& dimension = values.consume_a_token().token();
int a = dimension.dimension_value_int();
auto maybe_b = dimension.dimension_unit().bytes_as_string_view().substring_view(1).to_number<int>();
if (maybe_b.has_value()) {
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { a, maybe_b.value() };
}
return {};
}
// <dashndashdigit-ident>
if (is_dashndashdigit_ident(values.next_token())) {
auto maybe_b = values.consume_a_token().token().ident().bytes_as_string_view().substring_view(2).to_number<int>();
if (maybe_b.has_value()) {
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { -1, maybe_b.value() };
}
return {};
}
// -n
// -n <signed-integer>
// -n ['+' | '-'] <signless-integer>
if (values.next_token().is_ident("-n"sv)) {
values.discard_a_token(); // -n
values.discard_whitespace();
// -n <signed-integer>
if (is_signed_integer(values.next_token())) {
int b = values.consume_a_token().token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { -1, b };
}
// -n ['+' | '-'] <signless-integer>
{
auto child_transaction = transaction.create_child();
auto const& second_value = values.consume_a_token();
values.discard_whitespace();
auto const& third_value = values.consume_a_token();
if (is_sign(second_value) && is_signless_integer(third_value)) {
int b = third_value.token().to_integer() * (second_value.is_delim('+') ? 1 : -1);
child_transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { -1, b };
}
}
// -n
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { -1, 0 };
}
// -n- <signless-integer>
if (values.next_token().is_ident("-n-"sv)) {
values.discard_a_token(); // -n-
values.discard_whitespace();
auto const& second_value = values.consume_a_token();
if (is_signless_integer(second_value)) {
int b = -second_value.token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { -1, b };
}
return {};
}
// All that's left now are these:
// '+'?† n
// '+'?† n <signed-integer>
// '+'?† n ['+' | '-'] <signless-integer>
// '+'?† n- <signless-integer>
// '+'?† <ndashdigit-ident>
// In all of these cases, the + is optional, and has no effect.
// So, we consume the + if it's there.
if (values.next_token().is_delim('+')) {
values.discard_a_token(); // +
// We do *not* skip whitespace here.
}
auto const& first_after_plus = values.consume_a_token();
// '+'?† n
// '+'?† n <signed-integer>
// '+'?† n ['+' | '-'] <signless-integer>
if (first_after_plus.is_ident("n"sv)) {
values.discard_whitespace();
// '+'?† n <signed-integer>
if (is_signed_integer(values.next_token())) {
int b = values.consume_a_token().token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 1, b };
}
// '+'?† n ['+' | '-'] <signless-integer>
{
auto child_transaction = transaction.create_child();
auto const& second_value = values.consume_a_token();
values.discard_whitespace();
auto const& third_value = values.consume_a_token();
if (is_sign(second_value) && is_signless_integer(third_value)) {
int b = third_value.token().to_integer() * (second_value.is_delim('+') ? 1 : -1);
child_transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 1, b };
}
}
// '+'?† n
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 1, 0 };
}
// '+'?† n- <signless-integer>
if (first_after_plus.is_ident("n-"sv)) {
values.discard_whitespace();
auto const& second_value = values.consume_a_token();
if (is_signless_integer(second_value)) {
int b = -second_value.token().to_integer();
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 1, b };
}
return {};
}
// '+'?† <ndashdigit-ident>
if (is_ndashdigit_ident(first_after_plus)) {
auto maybe_b = first_after_plus.token().ident().bytes_as_string_view().substring_view(1).to_number<int>();
if (maybe_b.has_value()) {
transaction.commit();
return Selector::SimpleSelector::ANPlusBPattern { 1, maybe_b.value() };
}
return {};
}
return {};
}
Optional<PageSelectorList> Parser::parse_as_page_selector_list()
{
auto selector_list = parse_a_page_selector_list(m_token_stream);
if (!selector_list.is_error())
return selector_list.release_value();
return {};
}
template<typename T>
Parser::ParseErrorOr<PageSelectorList> Parser::parse_a_page_selector_list(TokenStream<T>& tokens)
{
// https://drafts.csswg.org/css-page-3/#syntax-page-selector
// <page-selector-list> = <page-selector>#
// <page-selector> = [ <ident-token>? <pseudo-page>* ]!
// <pseudo-page> = : [ left | right | first | blank ]
PageSelectorList selector_list;
tokens.discard_whitespace();
while (tokens.has_next_token()) {
// First optional ident
Optional<FlyString> maybe_ident;
if (tokens.next_token().is(Token::Type::Ident))
maybe_ident = static_cast<Token>(tokens.consume_a_token()).ident();
// Then an optional series of pseudo-classes
Vector<PagePseudoClass> pseudo_classes;
while (tokens.next_token().is(Token::Type::Colon)) {
tokens.discard_a_token(); // :
if (!tokens.next_token().is(Token::Type::Ident)) {
ErrorReporter::the().report(InvalidSelectorError {
.rule_name = "@page"_fly_string,
.value_string = tokens.dump_string(),
.description = "Pseudo-classes must be idents."_string,
});
return ParseError::SyntaxError;
}
auto pseudo_class_name = static_cast<Token>(tokens.consume_a_token()).ident();
if (auto pseudo_class = page_pseudo_class_from_string(pseudo_class_name); pseudo_class.has_value()) {
pseudo_classes.append(*pseudo_class);
} else {
ErrorReporter::the().report(UnknownPseudoClassOrElementError {
.rule_name = "@page"_fly_string,
.name = MUST(String::formatted(":{}", pseudo_class_name)),
});
return ParseError::SyntaxError;
}
}
if (!maybe_ident.has_value() && pseudo_classes.is_empty()) {
// Nothing parsed
ErrorReporter::the().report(InvalidSelectorError {
.rule_name = "@page"_fly_string,
.value_string = tokens.dump_string(),
.description = "Is empty."_string,
});
return ParseError::SyntaxError;
}
selector_list.empend(move(maybe_ident), move(pseudo_classes));
tokens.discard_whitespace();
if (tokens.next_token().is(Token::Type::Comma)) {
tokens.discard_a_token(); // ,
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.rule_name = "@page"_fly_string,
.value_string = tokens.dump_string(),
.description = "Trailing comma."_string,
});
return ParseError::SyntaxError;
}
} else if (tokens.has_next_token()) {
ErrorReporter::the().report(InvalidSelectorError {
.rule_name = "@page"_fly_string,
.value_string = tokens.dump_string(),
.description = "Trailing tokens."_string,
});
return ParseError::SyntaxError;
}
}
return selector_list;
}
template Parser::ParseErrorOr<PageSelectorList> Parser::parse_a_page_selector_list(TokenStream<ComponentValue>&);
template Parser::ParseErrorOr<PageSelectorList> Parser::parse_a_page_selector_list(TokenStream<Token>&);
}