2023-08-17 15:18:41 +01:00
/*
2024-10-04 13:19:50 +02:00
* Copyright ( c ) 2018 - 2022 , Andreas Kling < andreas @ ladybird . org >
2023-08-17 15:18:41 +01:00
* Copyright ( c ) 2020 - 2021 , the SerenityOS developers .
2025-07-23 10:15:39 +01:00
* Copyright ( c ) 2021 - 2025 , Sam Atkins < sam @ ladybird . org >
2023-08-17 15:18:41 +01:00
* Copyright ( c ) 2021 , Tobias Christiansen < tobyase @ serenityos . org >
* Copyright ( c ) 2022 , MacDue < macdue @ dueutil . tech >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
2025-07-23 10:15:39 +01:00
# include <LibWeb/CSS/Parser/ErrorReporter.h>
2023-08-17 15:18:41 +01:00
# include <LibWeb/CSS/Parser/Parser.h>
2023-12-10 22:06:55 +13:00
# include <LibWeb/Infra/Strings.h>
2023-08-17 15:18:41 +01:00
namespace Web : : CSS : : Parser {
2026-04-09 12:38:45 +01:00
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 ;
}
2023-08-17 15:18:41 +01:00
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 { } ;
}
2025-03-20 16:56:46 +00:00
Optional < Selector : : PseudoElementSelector > Parser : : parse_as_pseudo_element_selector ( )
2024-08-06 12:44:43 +01:00
{
2026-04-10 15:02:39 +01:00
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 ( ) )
2024-08-06 12:44:43 +01:00
return { } ;
2026-04-10 15:02:39 +01:00
if ( tokens . has_next_token ( ) )
2024-08-06 12:44:43 +01:00
return { } ;
2026-04-10 15:02:39 +01:00
auto simple_selector = maybe_simple_selector . release_value ( ) ;
2024-08-06 12:44:43 +01:00
if ( simple_selector . type ! = Selector : : SimpleSelector : : Type : : PseudoElement )
return { } ;
return simple_selector . pseudo_element ( ) ;
}
2024-11-13 15:49:43 +00:00
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 ) } ) ;
}
2023-08-17 15:18:41 +01:00
template < typename T >
Parser : : ParseErrorOr < SelectorList > Parser : : parse_a_selector_list ( TokenStream < T > & tokens , SelectorType mode , SelectorParsingMode parsing_mode )
{
2024-10-17 12:01:13 +01:00
SelectorList selectors ;
2024-11-22 19:48:50 +11:00
for ( ; ; ) {
auto selector_parts = consume_a_list_of_component_values ( tokens , Token : : Type : : Comma ) ;
2023-08-17 15:18:41 +01:00
auto stream = TokenStream ( selector_parts ) ;
auto selector = parse_complex_selector ( stream , mode ) ;
if ( selector . is_error ( ) ) {
2024-11-13 15:49:43 +00:00
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 ) ) ) ;
2024-11-22 19:48:50 +11:00
} else {
return selector . error ( ) ;
2024-11-13 15:49:43 +00:00
}
2024-11-22 19:48:50 +11:00
} else {
selectors . append ( selector . release_value ( ) ) ;
2023-08-17 15:18:41 +01:00
}
2024-11-22 19:48:50 +11:00
if ( tokens . is_empty ( ) )
break ;
tokens . discard_a_token ( ) ;
2023-08-17 15:18:41 +01:00
}
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 ;
2026-04-08 14:35:55 +01:00
auto first_combinator = parse_selector_combinator ( tokens ) ;
2023-08-17 15:18:41 +01:00
2026-04-08 14:35:55 +01:00
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 ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
2026-04-08 14:35:55 +01:00
}
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 ) ) ;
2026-04-08 16:35:25 +01:00
if ( first_selector . simple_selectors . is_empty ( ) ) {
2026-04-08 14:35:55 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = tokens . dump_string ( ) ,
. description = " Failed to parse first compound-selector. " _string ,
} ) ;
return ParseError : : SyntaxError ;
2023-08-17 15:18:41 +01:00
}
2026-04-08 14:35:55 +01:00
2026-04-08 16:35:25 +01:00
first_selector . combinator = first_combinator . value_or ( Selector : : Combinator : : None ) ;
compound_selectors . append ( move ( first_selector ) ) ;
2023-08-17 15:18:41 +01:00
while ( tokens . has_next_token ( ) ) {
2026-04-08 14:35:55 +01:00
auto combinator = parse_selector_combinator ( tokens ) ;
if ( ! combinator . has_value ( ) )
break ;
2023-08-17 15:18:41 +01:00
auto compound_selector = TRY ( parse_compound_selector ( tokens ) ) ;
2026-04-08 16:35:25 +01:00
if ( compound_selector . simple_selectors . is_empty ( ) ) {
2026-04-08 14:35:55 +01:00
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 ;
}
2026-04-08 16:35:25 +01:00
compound_selector . combinator = combinator . release_value ( ) ;
compound_selectors . append ( move ( compound_selector ) ) ;
2023-08-17 15:18:41 +01:00
}
2026-04-08 14:35:55 +01:00
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 ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
2026-04-08 14:35:55 +01:00
}
2023-08-17 15:18:41 +01:00
2025-12-09 14:02:17 +00:00
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 ;
}
}
}
2026-02-12 09:00:36 +00:00
// 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.
2025-12-09 14:02:17 +00:00
auto pseudo_element_count = 0 ;
2026-02-12 09:00:36 +00:00
Optional < PseudoElement > first_pseudo_element ;
Optional < PseudoElement > second_pseudo_element ;
2025-12-09 14:02:17 +00:00
for ( auto const & simple_selector : parsed_selector - > compound_selectors ( ) . last ( ) . simple_selectors ) {
2026-02-12 09:00:36 +00:00
if ( simple_selector . type = = Selector : : SimpleSelector : : Type : : PseudoElement ) {
2025-12-09 14:02:17 +00:00
+ + pseudo_element_count ;
2026-02-12 09:00:36 +00:00
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 ( ) ;
}
2025-12-09 14:02:17 +00:00
}
if ( pseudo_element_count > 1 ) {
2026-02-12 09:00:36 +00:00
// 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 ;
}
2025-12-09 14:02:17 +00:00
}
return parsed_selector ;
2023-08-17 15:18:41 +01:00
}
2026-04-08 16:35:25 +01:00
Parser : : ParseErrorOr < Selector : : CompoundSelector > Parser : : parse_compound_selector ( TokenStream < ComponentValue > & tokens )
2023-08-17 15:18:41 +01:00
{
Vector < Selector : : SimpleSelector > simple_selectors ;
while ( tokens . has_next_token ( ) ) {
auto component = TRY ( parse_simple_selector ( tokens ) ) ;
if ( ! component . has_value ( ) )
break ;
2024-11-08 20:14:46 +00:00
if ( component - > type = = Selector : : SimpleSelector : : Type : : TagName & & ! simple_selectors . is_empty ( ) ) {
2026-04-08 14:35:55 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = tokens . dump_string ( ) ,
. description = " Tag-name selectors can only go at the beginning of a compound selector. " _string ,
} ) ;
2024-11-08 20:14:46 +00:00
return ParseError : : SyntaxError ;
}
2023-08-17 15:18:41 +01:00
simple_selectors . append ( component . release_value ( ) ) ;
}
2026-04-08 14:35:55 +01:00
return Selector : : CompoundSelector { Selector : : Combinator : : None , move ( simple_selectors ) } ;
2023-08-17 15:18:41 +01:00
}
Optional < Selector : : Combinator > Parser : : parse_selector_combinator ( TokenStream < ComponentValue > & tokens )
{
2026-04-08 14:35:55 +01:00
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 ;
2023-08-17 15:18:41 +01:00
}
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 ( ' * ' ) )
2023-12-10 16:18:26 +13:00
return " * " _fly_string ;
return token . token ( ) . ident ( ) ;
2023-08-17 15:18:41 +01:00
} ;
// 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 ( ) ;
2024-10-31 21:43:21 +01:00
auto const & first_token = tokens . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( first_token . is_delim ( ' | ' ) ) {
// Case 1: `|<name>`
2024-10-09 12:29:29 +01:00
if ( is_name ( tokens . next_token ( ) ) ) {
2024-10-31 21:43:21 +01:00
auto const & name_token = tokens . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( allow_wildcard_name = = AllowWildcardName : : No & & name_token . is_delim ( ' * ' ) )
return { } ;
transaction . commit ( ) ;
return Selector : : SimpleSelector : : QualifiedName {
. namespace_type = Selector : : SimpleSelector : : QualifiedName : : NamespaceType : : None ,
2023-12-10 16:18:26 +13:00
. name = get_name ( name_token ) ,
2023-08-17 15:18:41 +01:00
} ;
}
return { } ;
}
if ( ! is_name ( first_token ) )
return { } ;
2024-10-09 12:29:29 +01:00
if ( tokens . next_token ( ) . is_delim ( ' | ' ) & & is_name ( tokens . peek_token ( 1 ) ) ) {
2023-08-17 15:18:41 +01:00
// Case 2: `<namespace>|<name>`
2024-10-09 12:29:29 +01:00
tokens . discard_a_token ( ) ; // `|`
2023-12-10 16:18:26 +13:00
auto namespace_ = get_name ( first_token ) ;
2024-10-09 12:29:29 +01:00
auto name = get_name ( tokens . consume_a_token ( ) ) ;
2023-08-17 15:18:41 +01:00
if ( allow_wildcard_name = = AllowWildcardName : : No & & name = = " * " sv )
return { } ;
auto namespace_type = namespace_ = = " * " sv
? Selector : : SimpleSelector : : QualifiedName : : NamespaceType : : Any
: Selector : : SimpleSelector : : QualifiedName : : NamespaceType : : Named ;
2025-06-23 22:40:37 +12:00
// 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 { } ;
2025-06-09 22:33:36 +12:00
2023-08-17 15:18:41 +01:00
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 ,
2023-12-10 16:18:26 +13:00
. name = get_name ( name_token ) ,
2023-08-17 15:18:41 +01:00
} ;
}
Parser : : ParseErrorOr < Selector : : SimpleSelector > Parser : : parse_attribute_simple_selector ( ComponentValue const & first_value )
{
2024-10-11 11:17:10 +01:00
auto attribute_tokens = TokenStream { first_value . block ( ) . value } ;
2023-08-17 15:18:41 +01:00
2024-10-09 12:29:29 +01:00
attribute_tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! attribute_tokens . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = first_value . to_string ( ) ,
. description = " Attribute selector is empty. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
auto maybe_qualified_name = parse_selector_qualified_name ( attribute_tokens , AllowWildcardName : : No ) ;
if ( ! maybe_qualified_name . has_value ( ) ) {
2025-07-23 10:15:39 +01:00
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 ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2024-06-22 13:33:48 +02:00
auto qualified_name = maybe_qualified_name . release_value ( ) ;
2023-08-17 15:18:41 +01:00
Selector : : SimpleSelector simple_selector {
. type = Selector : : SimpleSelector : : Type : : Attribute ,
. value = Selector : : SimpleSelector : : Attribute {
. match_type = Selector : : SimpleSelector : : Attribute : : MatchType : : HasAttribute ,
2024-06-22 13:33:48 +02:00
. qualified_name = qualified_name ,
2024-08-18 21:17:08 +01:00
. case_type = Selector : : SimpleSelector : : Attribute : : CaseType : : DefaultMatch ,
2023-08-17 15:18:41 +01:00
}
} ;
2024-10-09 12:29:29 +01:00
attribute_tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! attribute_tokens . has_next_token ( ) )
return simple_selector ;
2026-04-15 17:44:44 +01:00
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 ;
}
2023-08-17 15:18:41 +01:00
2026-04-15 17:44:44 +01:00
if ( ! tokens . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = first_value . to_string ( ) ,
. description = " Attribute selector ended part way through a match type. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2026-04-15 17:44:44 +01:00
auto const & second_delim = tokens . consume_a_token ( ) ;
if ( ! second_delim . is_delim ( ' = ' ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = first_value . to_string ( ) ,
2026-04-15 17:44:44 +01:00
. description = MUST ( String : : formatted ( " Expected a double delim for attribute comparison, got: '{}{}'. " , first_delim . to_debug_string ( ) , second_delim . to_debug_string ( ) ) ) ,
2025-07-23 10:15:39 +01:00
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2026-04-15 17:44:44 +01:00
switch ( first_delim . token ( ) . delim ( ) ) {
2023-08-17 15:18:41 +01:00
case ' ~ ' :
2026-04-15 17:44:44 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : Attribute : : MatchType : : ContainsWord ;
2023-08-17 15:18:41 +01:00
case ' * ' :
2026-04-15 17:44:44 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : Attribute : : MatchType : : ContainsString ;
2023-08-17 15:18:41 +01:00
case ' | ' :
2026-04-15 17:44:44 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : Attribute : : MatchType : : StartsWithSegment ;
2023-08-17 15:18:41 +01:00
case ' ^ ' :
2026-04-15 17:44:44 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : Attribute : : MatchType : : StartsWithString ;
2023-08-17 15:18:41 +01:00
case ' $ ' :
2026-04-15 17:44:44 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : Attribute : : MatchType : : EndsWithString ;
2023-08-17 15:18:41 +01:00
default :
2026-04-15 17:44:44 +01:00
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 ;
2023-08-17 15:18:41 +01:00
}
2026-04-15 17:44:44 +01:00
} ;
simple_selector . attribute ( ) . match_type = TRY ( parse_attribute_match_type ( attribute_tokens ) ) ;
2023-08-17 15:18:41 +01:00
2024-10-09 12:29:29 +01:00
attribute_tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! attribute_tokens . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = first_value . to_string ( ) ,
. description = " Attribute selector ended without a value to match. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2024-10-09 12:29:29 +01:00
auto const & value_part = attribute_tokens . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! value_part . is ( Token : : Type : : Ident ) & & ! value_part . is ( Token : : Type : : String ) ) {
2025-07-23 10:15:39 +01:00
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 ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2023-12-09 09:24:41 +13:00
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 ( ) ;
2023-08-17 15:18:41 +01:00
2024-10-09 12:29:29 +01:00
attribute_tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
// Handle case-sensitivity suffixes. https://www.w3.org/TR/selectors-4/#attribute-case
if ( attribute_tokens . has_next_token ( ) ) {
2024-10-09 12:29:29 +01:00
auto const & case_sensitivity_part = attribute_tokens . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
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 {
2025-07-23 10:15:39 +01:00
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 ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
} else {
2025-07-23 10:15:39 +01:00
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 ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
}
2025-05-15 17:24:30 +01:00
attribute_tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( attribute_tokens . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = first_value . to_string ( ) ,
. description = " Trailing tokens in attribute selector. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
return simple_selector ;
}
2026-04-09 12:38:45 +01:00
Parser : : ParseErrorOr < Selector : : SimpleSelector > Parser : : parse_pseudo_class_simple_selector ( TokenStream < ComponentValue > & tokens )
2023-08-17 15:18:41 +01:00
{
auto peek_token_ends_selector = [ & ] ( ) - > bool {
2024-10-09 12:29:29 +01:00
auto const & value = tokens . next_token ( ) ;
2026-04-09 12:38:45 +01:00
return value . is ( Token : : Type : : EndOfFile ) | | value . is ( Token : : Type : : Whitespace ) | | value . is ( Token : : Type : : Comma ) ;
2023-08-17 15:18:41 +01:00
} ;
if ( peek_token_ends_selector ( ) )
return ParseError : : SyntaxError ;
2026-04-09 12:38:45 +01:00
if ( ! tokens . consume_a_token ( ) . is ( Token : : Type : : Colon ) )
2023-12-10 17:07:32 +13:00
return ParseError : : SyntaxError ;
2023-08-17 15:18:41 +01:00
if ( peek_token_ends_selector ( ) )
return ParseError : : SyntaxError ;
2026-04-09 12:38:45 +01:00
if ( tokens . next_token ( ) . is ( Token : : Type : : Ident ) ) {
auto pseudo_name = tokens . consume_a_token ( ) . token ( ) . ident ( ) ;
2023-08-17 15:18:41 +01:00
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 ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Only valid as a function. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2026-04-09 12:38:45 +01:00
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector { . type = pseudo_class . value ( ) }
} ;
2023-08-17 15:18:41 +01:00
}
2025-12-18 12:47:44 +00:00
if ( has_ignored_vendor_prefix ( pseudo_name ) )
return ParseError : : IncludesIgnoredVendorPrefix ;
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( UnknownPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_name ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2026-04-09 12:38:45 +01:00
if ( tokens . next_token ( ) . is_function ( ) ) {
2025-08-28 10:44:17 +01:00
auto parse_an_plus_b_selector = [ this ] ( auto pseudo_class , Vector < ComponentValue > const & function_values , bool allow_of = false ) - > ParseErrorOr < Selector : : SimpleSelector > {
2026-04-09 12:38:45 +01:00
TokenStream tokens { function_values } ;
2025-08-12 10:28:05 +01:00
auto an_plus_b_pattern = parse_a_n_plus_b_pattern ( tokens ) ;
if ( ! an_plus_b_pattern . has_value ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_class_name ( pseudo_class ) ) ) ,
. value_string = tokens . dump_string ( ) ,
. description = " Invalid An+B format. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2024-10-09 12:29:29 +01:00
tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! tokens . has_next_token ( ) ) {
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector {
. type = pseudo_class ,
2025-08-28 10:44:17 +01:00
. an_plus_b_pattern = an_plus_b_pattern . release_value ( ) }
2023-08-17 15:18:41 +01:00
} ;
}
if ( ! allow_of )
return ParseError : : SyntaxError ;
// Parse the `of <selector-list>` syntax
2024-10-09 12:29:29 +01:00
auto const & maybe_of = tokens . consume_a_token ( ) ;
2023-11-21 12:08:39 +00:00
if ( ! maybe_of . is_ident ( " of " sv ) )
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
2024-10-09 12:29:29 +01:00
tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
auto selector_list = TRY ( parse_a_selector_list ( tokens , SelectorType : : Standalone ) ) ;
2024-10-09 12:29:29 +01:00
tokens . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( tokens . has_next_token ( ) )
return ParseError : : SyntaxError ;
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector {
. type = pseudo_class ,
2025-08-28 10:44:17 +01:00
. an_plus_b_pattern = an_plus_b_pattern . release_value ( ) ,
2023-08-17 15:18:41 +01:00
. argument_selector_list = move ( selector_list ) }
} ;
} ;
2026-04-09 12:38:45 +01:00
auto const & pseudo_function = tokens . consume_a_token ( ) . function ( ) ;
2024-10-11 11:17:10 +01:00
auto maybe_pseudo_class = pseudo_class_from_string ( pseudo_function . name ) ;
2023-08-17 15:18:41 +01:00
if ( ! maybe_pseudo_class . has_value ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( UnknownPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
auto pseudo_class = maybe_pseudo_class . value ( ) ;
auto metadata = pseudo_class_metadata ( pseudo_class ) ;
if ( ! metadata . is_valid_as_function ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Not valid as a function. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2024-10-11 11:17:10 +01:00
if ( pseudo_function . value . is_empty ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Missing arguments. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2024-11-14 12:18:10 +00:00
// "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 ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " :has() is not allowed inside :has(). " _string ,
} ) ;
2024-11-14 12:18:10 +00:00
return ParseError : : SyntaxError ;
}
m_pseudo_class_context . append ( pseudo_class ) ;
ScopeGuard guard = [ & ] { m_pseudo_class_context . take_last ( ) ; } ;
2023-08-17 15:18:41 +01:00
switch ( metadata . parameter_type ) {
case PseudoClassMetadata : : ParameterType : : ANPlusB :
2025-08-12 10:28:05 +01:00
return parse_an_plus_b_selector ( pseudo_class , pseudo_function . value , false ) ;
2023-08-17 15:18:41 +01:00
case PseudoClassMetadata : : ParameterType : : ANPlusBOf :
2025-08-12 10:28:05 +01:00
return parse_an_plus_b_selector ( pseudo_class , pseudo_function . value , true ) ;
2023-08-17 15:18:41 +01:00
case PseudoClassMetadata : : ParameterType : : CompoundSelector : {
2024-10-11 11:17:10 +01:00
auto function_token_stream = TokenStream ( pseudo_function . value ) ;
2023-08-17 15:18:41 +01:00
auto compound_selector_or_error = parse_compound_selector ( function_token_stream ) ;
2026-04-08 16:35:25 +01:00
if ( compound_selector_or_error . is_error ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Failed to parse argument as a compound selector. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2026-04-08 14:41:36 +01:00
function_token_stream . discard_whitespace ( ) ;
if ( function_token_stream . has_next_token ( ) ) {
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " ::{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2026-04-08 14:41:36 +01:00
. description = " Trailing tokens after compound selector argument. " _string ,
} ) ;
return ParseError : : SyntaxError ;
}
2023-08-17 15:18:41 +01:00
2026-04-08 16:35:25 +01:00
auto compound_selector = compound_selector_or_error . release_value ( ) ;
2024-07-23 15:03:27 +02:00
compound_selector . combinator = Selector : : Combinator : : None ;
2026-04-08 16:35:25 +01:00
auto selector = Selector : : create ( Vector { move ( compound_selector ) } ) ;
2023-08-17 15:18:41 +01:00
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector {
. type = pseudo_class ,
. argument_selector_list = { move ( selector ) } }
} ;
}
2024-07-13 09:19:30 -07:00
case PseudoClassMetadata : : ParameterType : : ForgivingRelativeSelectorList :
2023-08-17 15:18:41 +01:00
case PseudoClassMetadata : : ParameterType : : ForgivingSelectorList : {
2024-10-11 11:17:10 +01:00
auto function_token_stream = TokenStream ( pseudo_function . value ) ;
2024-07-13 09:19:30 -07:00
auto selector_type = metadata . parameter_type = = PseudoClassMetadata : : ParameterType : : ForgivingSelectorList
? SelectorType : : Standalone
: SelectorType : : Relative ;
2023-08-17 15:18:41 +01:00
// NOTE: Because it's forgiving, even complete garbage will parse OK as an empty selector-list.
2024-07-13 09:19:30 -07:00
auto argument_selector_list = MUST ( parse_a_selector_list ( function_token_stream , selector_type , SelectorParsingMode : : Forgiving ) ) ;
2023-08-17 15:18:41 +01:00
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector {
. type = pseudo_class ,
2024-11-14 12:48:14 +00:00
. is_forgiving = true ,
2023-08-17 15:18:41 +01:00
. argument_selector_list = move ( argument_selector_list ) }
} ;
}
2023-08-12 18:11:50 +01:00
case PseudoClassMetadata : : ParameterType : : Ident : {
2024-10-11 11:17:10 +01:00
auto function_token_stream = TokenStream ( pseudo_function . value ) ;
2024-10-09 12:29:29 +01:00
function_token_stream . discard_whitespace ( ) ;
2025-05-16 14:57:18 +01:00
auto const & maybe_ident_token = function_token_stream . consume_a_token ( ) ;
2024-10-09 12:29:29 +01:00
function_token_stream . discard_whitespace ( ) ;
2025-05-16 14:57:18 +01:00
if ( ! maybe_ident_token . is ( Token : : Type : : Ident ) | | function_token_stream . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Failed to parse argument as an ident. " _string ,
} ) ;
2023-08-12 18:11:50 +01:00
return ParseError : : SyntaxError ;
}
2025-05-16 14:57:18 +01:00
auto & ident = maybe_ident_token . token ( ) . ident ( ) ;
2023-08-12 18:11:50 +01:00
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector {
. type = pseudo_class ,
2025-05-16 14:57:18 +01:00
. ident = Selector : : SimpleSelector : : PseudoClassSelector : : Ident {
. keyword = keyword_from_string ( ident ) . value_or ( Keyword : : Invalid ) ,
. string_value = ident ,
} ,
}
2023-08-12 18:11:50 +01:00
} ;
}
2023-08-17 15:18:41 +01:00
case PseudoClassMetadata : : ParameterType : : LanguageRanges : {
Vector < FlyString > languages ;
2024-10-11 11:17:10 +01:00
auto function_token_stream = TokenStream ( pseudo_function . value ) ;
2023-08-17 15:18:41 +01:00
auto language_token_lists = parse_a_comma_separated_list_of_component_values ( function_token_stream ) ;
2024-10-31 21:43:21 +01:00
for ( auto const & language_token_list : language_token_lists ) {
2023-08-17 15:18:41 +01:00
auto language_token_stream = TokenStream ( language_token_list ) ;
2024-10-09 12:29:29 +01:00
language_token_stream . discard_whitespace ( ) ;
2024-10-31 21:43:21 +01:00
auto const & language_token = language_token_stream . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! ( language_token . is ( Token : : Type : : Ident ) | | language_token . is ( Token : : Type : : String ) ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Failed to parse argument as a language range: Not a string/ident. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
auto language_string = language_token . is ( Token : : Type : : String ) ? language_token . token ( ) . string ( ) : language_token . token ( ) . ident ( ) ;
2023-12-10 16:18:26 +13:00
languages . append ( language_string ) ;
2023-08-17 15:18:41 +01:00
2024-10-09 12:29:29 +01:00
language_token_stream . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
if ( language_token_stream . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidPseudoClassOrElementError {
. name = MUST ( String : : formatted ( " :{} " , pseudo_function . name ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-07-23 10:15:39 +01:00
. description = " Failed to parse argument as a language range: Has trailing tokens. " _string ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
}
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : PseudoClass ,
. value = Selector : : SimpleSelector : : PseudoClassSelector {
. type = pseudo_class ,
. languages = move ( languages ) }
} ;
}
2025-08-28 10:29:57 +01:00
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 ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-08-28 10:29:57 +01:00
. 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 ) ) ,
2026-04-09 12:38:45 +01:00
. value_string = pseudo_function . name . to_string ( ) ,
2025-08-28 10:29:57 +01:00
. 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 ) ,
}
} ;
}
2024-11-14 11:22:46 +00:00
case PseudoClassMetadata : : ParameterType : : RelativeSelectorList :
2023-08-17 15:18:41 +01:00
case PseudoClassMetadata : : ParameterType : : SelectorList : {
2024-10-11 11:17:10 +01:00
auto function_token_stream = TokenStream ( pseudo_function . value ) ;
2024-11-14 11:22:46 +00:00
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 ) ) ;
2023-08-17 15:18:41 +01:00
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 ( ) ;
}
}
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
2026-04-09 12:38:45 +01:00
. 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 ) ) ,
2025-07-23 10:15:39 +01:00
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
Parser : : ParseErrorOr < Optional < Selector : : SimpleSelector > > Parser : : parse_simple_selector ( TokenStream < ComponentValue > & tokens )
{
auto peek_token_ends_selector = [ & ] ( ) - > bool {
2024-10-09 12:29:29 +01:00
auto const & value = tokens . next_token ( ) ;
2023-08-17 15:18:41 +01:00
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 ( ) ,
} ;
}
2026-04-09 12:38:45 +01:00
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 ) ) ;
2026-04-15 17:59:47 +01:00
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 > { } ;
}
2024-10-09 12:29:29 +01:00
auto const & first_value = tokens . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( first_value . is ( Token : : Type : : Delim ) ) {
u32 delim = first_value . token ( ) . delim ( ) ;
switch ( delim ) {
case ' * ' :
// Handled already
VERIFY_NOT_REACHED ( ) ;
2024-10-15 12:00:29 +01:00
case ' & ' :
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : Nesting ,
} ;
2023-08-17 15:18:41 +01:00
case ' . ' : {
if ( peek_token_ends_selector ( ) )
return ParseError : : SyntaxError ;
2024-10-09 12:29:29 +01:00
auto const & class_name_value = tokens . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( ! class_name_value . is ( Token : : Type : : Ident ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = tokens . dump_string ( ) ,
. description = MUST ( String : : formatted ( " Expected an ident after '.', got: {} " , class_name_value . to_debug_string ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : Class ,
2023-12-10 16:18:26 +13:00
. value = Selector : : SimpleSelector : : Name { class_name_value . token ( ) . ident ( ) }
2023-08-17 15:18:41 +01:00
} ;
}
default :
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = tokens . dump_string ( ) ,
. description = MUST ( String : : formatted ( " Unrecognized delimiter: {} " , first_value . token ( ) . to_string ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
}
if ( first_value . is ( Token : : Type : : Hash ) ) {
if ( first_value . token ( ) . hash_type ( ) ! = Token : : HashType : : Id ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = tokens . dump_string ( ) ,
. description = MUST ( String : : formatted ( " Hash token is not an id: {} " , first_value . to_debug_string ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
return Selector : : SimpleSelector {
. type = Selector : : SimpleSelector : : Type : : Id ,
2023-12-10 16:18:26 +13:00
. value = Selector : : SimpleSelector : : Name { first_value . token ( ) . hash_value ( ) }
2023-08-17 15:18:41 +01:00
} ;
}
if ( first_value . is_block ( ) & & first_value . block ( ) . is_square ( ) )
return TRY ( parse_attribute_simple_selector ( first_value ) ) ;
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. value_string = tokens . dump_string ( ) ,
. description = MUST ( String : : formatted ( " Invalid start of a simple selector: {} " , first_value . to_debug_string ( ) ) ) ,
} ) ;
2023-08-17 15:18:41 +01:00
return ParseError : : SyntaxError ;
}
2026-04-16 11:38:53 +01:00
// https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
2023-08-17 15:18:41 +01:00
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 ( ) = = ' - ' ) ;
} ;
2026-04-16 11:38:53 +01:00
auto is_series_of_1_or_more_digits = [ ] ( StringView string ) - > bool {
if ( string . is_empty ( ) )
2023-08-17 15:18:41 +01:00
return false ;
2026-04-16 11:38:53 +01:00
for ( char c : string ) {
if ( ! is_ascii_digit ( c ) )
return false ;
}
2023-08-17 15:18:41 +01:00
return true ;
} ;
2026-04-16 11:38:53 +01:00
// <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-"
2023-08-17 15:18:41 +01:00
auto is_ndash_dimension = [ ] ( ComponentValue const & value ) - > bool {
2026-04-16 11:38:53 +01:00
return value . is ( Token : : Type : : Dimension )
& & value . token ( ) . number ( ) . is_integer ( )
& & value . token ( ) . dimension_unit ( ) . equals_ignoring_ascii_case ( " n- " sv ) ;
2023-08-17 15:18:41 +01:00
} ;
2026-04-16 11:38:53 +01:00
// <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 ) ) ;
2023-08-17 15:18:41 +01:00
} ;
2026-04-16 11:38:53 +01:00
// <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 ) ) ;
2023-08-17 15:18:41 +01:00
} ;
2026-04-16 11:38:53 +01:00
// <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 ) ) ;
2023-08-17 15:18:41 +01:00
} ;
2026-04-16 11:38:53 +01:00
// <integer> is a <number-token> with its type flag set to "integer"
2023-08-17 15:18:41 +01:00
auto is_integer = [ ] ( ComponentValue const & value ) - > bool {
return value . is ( Token : : Type : : Number ) & & value . token ( ) . number ( ) . is_integer ( ) ;
} ;
2026-04-16 11:38:53 +01:00
// <signed-integer> is a <number-token> with its type flag set to "integer", and a sign character
2023-08-17 15:18:41 +01:00
auto is_signed_integer = [ ] ( ComponentValue const & value ) - > bool {
return value . is ( Token : : Type : : Number ) & & value . token ( ) . number ( ) . is_integer_with_explicit_sign ( ) ;
} ;
2026-04-16 11:38:53 +01:00
// <signless-integer> is a <number-token> with its type flag set to "integer", and no sign character
2023-08-17 15:18:41 +01:00
auto is_signless_integer = [ ] ( ComponentValue const & value ) - > bool {
2026-04-16 11:38:53 +01:00
return value . is ( Token : : Type : : Number )
& & value . token ( ) . number ( ) . is_integer ( )
& & ! value . token ( ) . number ( ) . is_integer_with_explicit_sign ( ) ;
2023-08-17 15:18:41 +01:00
} ;
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
// odd | even
2026-04-16 11:38:53 +01:00
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 } ;
2023-08-17 15:18:41 +01:00
}
2026-04-16 11:38:53 +01:00
2023-08-17 15:18:41 +01:00
// <integer>
2026-04-16 11:38:53 +01:00
if ( is_integer ( values . next_token ( ) ) ) {
int b = values . consume_a_token ( ) . token ( ) . to_integer ( ) ;
2023-08-17 15:18:41 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { 0 , b } ;
}
2026-04-16 11:38:53 +01:00
2023-08-17 15:18:41 +01:00
// <n-dimension>
// <n-dimension> <signed-integer>
// <n-dimension> ['+' | '-'] <signless-integer>
2026-04-16 11:38:53 +01:00
if ( is_n_dimension ( values . next_token ( ) ) ) {
int a = values . consume_a_token ( ) . token ( ) . dimension_value_int ( ) ;
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
// <n-dimension> <signed-integer>
2024-10-09 12:29:29 +01:00
if ( is_signed_integer ( values . next_token ( ) ) ) {
int b = values . consume_a_token ( ) . token ( ) . to_integer ( ) ;
2023-08-17 15:18:41 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { a , b } ;
}
// <n-dimension> ['+' | '-'] <signless-integer>
{
auto child_transaction = transaction . create_child ( ) ;
2024-10-09 12:29:29 +01:00
auto const & second_value = values . consume_a_token ( ) ;
values . discard_whitespace ( ) ;
auto const & third_value = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
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 } ;
}
2026-04-16 11:38:53 +01:00
2023-08-17 15:18:41 +01:00
// <ndash-dimension> <signless-integer>
2026-04-16 11:38:53 +01:00
if ( is_ndash_dimension ( values . next_token ( ) ) ) {
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
2026-04-16 11:38:53 +01:00
auto const & first_value = values . consume_a_token ( ) ;
2024-10-09 12:29:29 +01:00
auto const & second_value = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
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 } ;
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
2026-04-16 11:38:53 +01:00
2023-08-17 15:18:41 +01:00
// <ndashdigit-dimension>
2026-04-16 11:38:53 +01:00
if ( is_ndashdigit_dimension ( values . next_token ( ) ) ) {
auto const & dimension = values . consume_a_token ( ) . token ( ) ;
2023-08-17 15:18:41 +01:00
int a = dimension . dimension_value_int ( ) ;
2023-12-23 15:59:14 +13:00
auto maybe_b = dimension . dimension_unit ( ) . bytes_as_string_view ( ) . substring_view ( 1 ) . to_number < int > ( ) ;
2023-08-17 15:18:41 +01:00
if ( maybe_b . has_value ( ) ) {
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { a , maybe_b . value ( ) } ;
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
2026-04-16 11:38:53 +01:00
2023-08-17 15:18:41 +01:00
// <dashndashdigit-ident>
2026-04-16 11:38:53 +01:00
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 > ( ) ;
2023-08-17 15:18:41 +01:00
if ( maybe_b . has_value ( ) ) {
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { - 1 , maybe_b . value ( ) } ;
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
2026-04-16 11:38:53 +01:00
2023-08-17 15:18:41 +01:00
// -n
// -n <signed-integer>
// -n ['+' | '-'] <signless-integer>
2026-04-16 11:38:53 +01:00
if ( values . next_token ( ) . is_ident ( " -n " sv ) ) {
values . discard_a_token ( ) ; // -n
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
// -n <signed-integer>
2024-10-09 12:29:29 +01:00
if ( is_signed_integer ( values . next_token ( ) ) ) {
int b = values . consume_a_token ( ) . token ( ) . to_integer ( ) ;
2023-08-17 15:18:41 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { - 1 , b } ;
}
// -n ['+' | '-'] <signless-integer>
{
auto child_transaction = transaction . create_child ( ) ;
2024-10-09 12:29:29 +01:00
auto const & second_value = values . consume_a_token ( ) ;
values . discard_whitespace ( ) ;
auto const & third_value = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
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>
2026-04-16 11:38:53 +01:00
if ( values . next_token ( ) . is_ident ( " -n- " sv ) ) {
values . discard_a_token ( ) ; // -n-
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
auto const & second_value = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( is_signless_integer ( second_value ) ) {
int b = - second_value . token ( ) . to_integer ( ) ;
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { - 1 , b } ;
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
// 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.
2026-04-16 11:38:53 +01:00
// So, we consume the + if it's there.
if ( values . next_token ( ) . is_delim ( ' + ' ) ) {
values . discard_a_token ( ) ; // +
2023-08-17 15:18:41 +01:00
// We do *not* skip whitespace here.
}
2024-10-09 12:29:29 +01:00
auto const & first_after_plus = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
// '+'?† n
// '+'?† n <signed-integer>
// '+'?† n ['+' | '-'] <signless-integer>
2023-11-21 12:08:39 +00:00
if ( first_after_plus . is_ident ( " n " sv ) ) {
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
2023-08-17 15:18:41 +01:00
// '+'?† n <signed-integer>
2024-10-09 12:29:29 +01:00
if ( is_signed_integer ( values . next_token ( ) ) ) {
int b = values . consume_a_token ( ) . token ( ) . to_integer ( ) ;
2023-08-17 15:18:41 +01:00
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { 1 , b } ;
}
// '+'?† n ['+' | '-'] <signless-integer>
{
auto child_transaction = transaction . create_child ( ) ;
2024-10-09 12:29:29 +01:00
auto const & second_value = values . consume_a_token ( ) ;
values . discard_whitespace ( ) ;
auto const & third_value = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
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>
2023-11-21 12:08:39 +00:00
if ( first_after_plus . is_ident ( " n- " sv ) ) {
2024-10-09 12:29:29 +01:00
values . discard_whitespace ( ) ;
auto const & second_value = values . consume_a_token ( ) ;
2023-08-17 15:18:41 +01:00
if ( is_signless_integer ( second_value ) ) {
int b = - second_value . token ( ) . to_integer ( ) ;
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { 1 , b } ;
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
// '+'?† <ndashdigit-ident>
if ( is_ndashdigit_ident ( first_after_plus ) ) {
2023-12-23 15:59:14 +13:00
auto maybe_b = first_after_plus . token ( ) . ident ( ) . bytes_as_string_view ( ) . substring_view ( 1 ) . to_number < int > ( ) ;
2023-08-17 15:18:41 +01:00
if ( maybe_b . has_value ( ) ) {
transaction . commit ( ) ;
return Selector : : SimpleSelector : : ANPlusBPattern { 1 , maybe_b . value ( ) } ;
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
2025-07-23 10:15:39 +01:00
return { } ;
2023-08-17 15:18:41 +01:00
}
2025-05-16 11:37:53 +01:00
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 ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. rule_name = " @page " _fly_string ,
. value_string = tokens . dump_string ( ) ,
. description = " Pseudo-classes must be idents. " _string ,
} ) ;
2025-05-16 11:37:53 +01:00
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 {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( UnknownPseudoClassOrElementError {
. rule_name = " @page " _fly_string ,
. name = MUST ( String : : formatted ( " :{} " , pseudo_class_name ) ) ,
} ) ;
2025-05-16 11:37:53 +01:00
return ParseError : : SyntaxError ;
}
}
if ( ! maybe_ident . has_value ( ) & & pseudo_classes . is_empty ( ) ) {
// Nothing parsed
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. rule_name = " @page " _fly_string ,
. value_string = tokens . dump_string ( ) ,
. description = " Is empty. " _string ,
} ) ;
2025-05-16 11:37:53 +01:00
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 ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. rule_name = " @page " _fly_string ,
. value_string = tokens . dump_string ( ) ,
. description = " Trailing comma. " _string ,
} ) ;
2025-05-16 11:37:53 +01:00
return ParseError : : SyntaxError ;
}
} else if ( tokens . has_next_token ( ) ) {
2025-07-23 10:15:39 +01:00
ErrorReporter : : the ( ) . report ( InvalidSelectorError {
. rule_name = " @page " _fly_string ,
. value_string = tokens . dump_string ( ) ,
. description = " Trailing tokens. " _string ,
} ) ;
2025-05-16 11:37:53 +01:00
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 > & ) ;
2023-08-17 15:18:41 +01:00
}