diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index f737e23a6f9..6ae11125636 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -413,6 +413,8 @@ private: }; static ContextType context_type_for_at_rule(FlyString const&); Vector m_rule_context; + + Vector m_pseudo_class_context; // Stack of pseudo-class functions we're currently inside }; } diff --git a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 37765626c38..1cf881b2de3 100644 --- a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -411,6 +411,11 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec // Note: We allow the "ignored" -webkit prefix here for -webkit-progress-bar/-webkit-progress-bar if (auto pseudo_element = Selector::PseudoElement::from_string(pseudo_name); 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->type())) { + return ParseError::SyntaxError; + } + return Selector::SimpleSelector { .type = Selector::SimpleSelector::Type::PseudoElement, .value = pseudo_element.release_value() @@ -423,6 +428,10 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec // 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 (pseudo_name.starts_with_bytes("-webkit-"sv, CaseSensitivity::CaseInsensitive)) { + // :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. @@ -470,6 +479,11 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec case Selector::PseudoElement::Type::Before: case Selector::PseudoElement::Type::FirstLetter: case Selector::PseudoElement::Type::FirstLine: + // :has() is fussy about pseudo-elements inside it + if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(pseudo_element->type())) { + return ParseError::SyntaxError; + } + return Selector::SimpleSelector { .type = Selector::SimpleSelector::Type::PseudoElement, .value = pseudo_element.value() @@ -545,6 +559,16 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec 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)) { + dbgln_if(CSS_PARSER_DEBUG, ":has() is not allowed inside :has()"); + 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_nth_child_selector(pseudo_class, pseudo_function.value, false); diff --git a/Libraries/LibWeb/CSS/Selector.cpp b/Libraries/LibWeb/CSS/Selector.cpp index 2282ff08451..916f83fd68b 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -687,4 +687,11 @@ SelectorList adapt_nested_relative_selector_list(SelectorList const& selectors) return new_list; } +// https://drafts.csswg.org/selectors/#has-allowed-pseudo-element +bool is_has_allowed_pseudo_element(Selector::PseudoElement::Type) +{ + // No spec currently defines any pseudo-elements that are allowed in :has() + return false; +} + } diff --git a/Libraries/LibWeb/CSS/Selector.h b/Libraries/LibWeb/CSS/Selector.h index b8d2425861a..1f0c32ebfcd 100644 --- a/Libraries/LibWeb/CSS/Selector.h +++ b/Libraries/LibWeb/CSS/Selector.h @@ -275,6 +275,8 @@ String serialize_a_group_of_selectors(SelectorList const& selectors); SelectorList adapt_nested_relative_selector_list(SelectorList const&); +bool is_has_allowed_pseudo_element(Selector::PseudoElement::Type); + } namespace AK { diff --git a/Tests/LibWeb/Text/expected/css/invalid-selector-in-has.txt b/Tests/LibWeb/Text/expected/css/invalid-selector-in-has.txt index f3d66c7fe75..6f9b0966989 100644 --- a/Tests/LibWeb/Text/expected/css/invalid-selector-in-has.txt +++ b/Tests/LibWeb/Text/expected/css/invalid-selector-in-has.txt @@ -1 +1,6 @@ :has(:yakthonk) should be invalid: PASS +:has(:not(:yakthonk)) should be invalid: PASS +:has(:has(span)) should be invalid: PASS +:has(:not(:has(span))) should be invalid: PASS +:has(::before) should be invalid: PASS +:has(:not(::before)) should be invalid: PASS diff --git a/Tests/LibWeb/Text/input/css/invalid-selector-in-has.html b/Tests/LibWeb/Text/input/css/invalid-selector-in-has.html index 9c6be65e13b..5bd7fdaff6a 100644 --- a/Tests/LibWeb/Text/input/css/invalid-selector-in-has.html +++ b/Tests/LibWeb/Text/input/css/invalid-selector-in-has.html @@ -5,6 +5,11 @@ test(() => { let selectors = [ ":has(:yakthonk)", + ":has(:not(:yakthonk))", + ":has(:has(span))", + ":has(:not(:has(span)))", + ":has(::before)", + ":has(:not(::before))", ]; let style = document.getElementById("style");