mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-18 15:52:21 +00:00
Previously, and according to the spec, `a::part(foo)::before` would be a single CompoundSelector, even though it matches against 3 different targets. This meant some awkward swapping of targets in the middle of matching, and in particular it made `::part()` and `::slotted()` quite hacky, requiring them to track extra data on the MatchContext to then use later. This was scattered around and difficult to follow. Partly inspired by Gecko, this commit instead introduces an invisible PseudoElement combinator. After parsing a selector, we find any CompoundSelectors that contain a pseudo-element and split them up, so that each CompoundSelector only has a single target in the end. Where the pseudo-element was at the start of a CompoundSelector, we insert an invisible universal selector before it to represent its originating element. So now, a CompoundSelector deals with one target, and switching targets is done at the combinator. The one inconsistency is that we match the target of ::slotted() and ::part() in pseudo_element_transition_target(), instead of before then when processing the SimpleSelector. This is to avoid repeating the same computations twice. No outward-facing behaviour changes, though the invalidation metrics have changed.
1635 lines
71 KiB
C++
1635 lines
71 KiB
C++
/*
|
||
* 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) });
|
||
}
|
||
|
||
static Vector<Selector::CompoundSelector> normalize_pseudo_element_transitions(Vector<Selector::CompoundSelector>&& compound_selectors)
|
||
{
|
||
// Splits up any CompoundSelectors including pseudo-elements, so that they are separated by a PseudoElement
|
||
// combinator, while ensuring that any pseudo-element CompoundSelectors are preceded by a CompoundSelector for
|
||
// their originating element.
|
||
// Any that don't have one specified explicitly receive an implicit `*` before them:
|
||
// eg, `::before` becomes two CompoundSelectors, for the implicit `*` and for the `::before` pseudo-element.
|
||
|
||
// If we don't have any pseudo-elements, return the original CompoundSelectors unchanged.
|
||
bool contains_pseudo_element = false;
|
||
for (auto const& compound_selector : compound_selectors) {
|
||
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
||
if (simple_selector.type != Selector::SimpleSelector::Type::PseudoElement)
|
||
continue;
|
||
contains_pseudo_element = true;
|
||
break;
|
||
}
|
||
if (contains_pseudo_element)
|
||
break;
|
||
}
|
||
if (!contains_pseudo_element)
|
||
return move(compound_selectors);
|
||
|
||
Vector<Selector::CompoundSelector> normalized_compound_selectors;
|
||
|
||
auto append_compound = [&](Selector::Combinator combinator, Vector<Selector::SimpleSelector>&& simple_selectors) {
|
||
if (simple_selectors.is_empty())
|
||
return;
|
||
normalized_compound_selectors.append({
|
||
.combinator = combinator,
|
||
.simple_selectors = move(simple_selectors),
|
||
});
|
||
};
|
||
|
||
for (auto& compound_selector : compound_selectors) {
|
||
auto current_combinator = compound_selector.combinator;
|
||
Vector<Selector::SimpleSelector> current_simple_selectors;
|
||
current_simple_selectors.ensure_capacity(compound_selector.simple_selectors.size());
|
||
|
||
for (auto& simple_selector : compound_selector.simple_selectors) {
|
||
if (simple_selector.type == Selector::SimpleSelector::Type::PseudoElement) {
|
||
if (current_simple_selectors.is_empty()) {
|
||
normalized_compound_selectors.append({
|
||
.combinator = current_combinator,
|
||
.is_implicit_universal_anchor = true,
|
||
.simple_selectors = { Selector::SimpleSelector {
|
||
.type = Selector::SimpleSelector::Type::Universal,
|
||
.value = Selector::SimpleSelector::QualifiedName {
|
||
.namespace_type = Selector::SimpleSelector::QualifiedName::NamespaceType::Any,
|
||
.name = Selector::SimpleSelector::Name { "*"_fly_string },
|
||
},
|
||
} },
|
||
});
|
||
} else {
|
||
append_compound(current_combinator, move(current_simple_selectors));
|
||
current_simple_selectors = {};
|
||
}
|
||
current_combinator = Selector::Combinator::PseudoElement;
|
||
}
|
||
current_simple_selectors.append(move(simple_selector));
|
||
}
|
||
|
||
append_compound(current_combinator, move(current_simple_selectors));
|
||
}
|
||
|
||
return normalized_compound_selectors;
|
||
}
|
||
|
||
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> raw_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);
|
||
raw_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();
|
||
raw_compound_selectors.append(move(compound_selector));
|
||
}
|
||
|
||
if (raw_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(normalize_pseudo_element_transitions(move(raw_compound_selectors)));
|
||
|
||
auto pseudo_element_count = 0;
|
||
Optional<PseudoElement> first_pseudo_element;
|
||
Optional<PseudoElement> second_pseudo_element;
|
||
bool saw_pseudo_element_transition = false;
|
||
for (auto const& compound_selector : parsed_selector->compound_selectors()) {
|
||
if (saw_pseudo_element_transition && compound_selector.combinator != Selector::Combinator::PseudoElement) {
|
||
ErrorReporter::the().report(InvalidSelectorError {
|
||
.value_string = parsed_selector->serialize(),
|
||
.description = "Pseudo-elements cannot be followed by a non-pseudo-element combinator."_string,
|
||
});
|
||
return ParseError::SyntaxError;
|
||
}
|
||
if (compound_selector.combinator == Selector::Combinator::PseudoElement)
|
||
saw_pseudo_element_transition = true;
|
||
|
||
if (!compound_selector.simple_selectors.is_empty()
|
||
&& compound_selector.simple_selectors.first().type == Selector::SimpleSelector::Type::PseudoElement) {
|
||
++pseudo_element_count;
|
||
auto pseudo_element = compound_selector.simple_selectors.first().pseudo_element().type();
|
||
if (!first_pseudo_element.has_value())
|
||
first_pseudo_element = pseudo_element;
|
||
else if (!second_pseudo_element.has_value())
|
||
second_pseudo_element = pseudo_element;
|
||
}
|
||
}
|
||
|
||
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 {
|
||
.combinator = Selector::Combinator::None,
|
||
.simple_selectors = 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().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().to_integer());
|
||
}
|
||
|
||
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 they’re 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().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().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().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().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().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().is_integer()
|
||
&& !value.token().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>&);
|
||
|
||
}
|