From 9054ff29f058afb56286ce2596cf1808016d6b46 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 12 Jul 2025 16:15:06 +1200 Subject: [PATCH] LibWeb/CSS: Parse the ::slotted pseudo-element --- .../LibWeb/CSS/Parser/SelectorParsing.cpp | 14 +++++ Libraries/LibWeb/CSS/PseudoElements.json | 5 ++ Libraries/LibWeb/CSS/Selector.cpp | 5 ++ Libraries/LibWeb/CSS/Selector.h | 7 ++- Libraries/LibWeb/Dump.cpp | 1 + Libraries/LibWeb/HTML/Window.cpp | 3 +- .../LibWeb/GenerateCSSPseudoElement.cpp | 3 ++ .../css/css-scoping/slotted-parsing.txt | 38 ++++++++++++++ .../nodes/ParentNode-querySelector-All.txt | 35 +++++++------ .../css/css-scoping/slotted-parsing.html | 51 +++++++++++++++++++ 10 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-scoping/slotted-parsing.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-scoping/slotted-parsing.html diff --git a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 03c10b35550..9c0de6d5168 100644 --- a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -466,6 +466,20 @@ Parser::ParseErrorOr Parser::parse_pseudo_simple_selec 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() || !compound_selector_or_error.value().has_value()) { + dbgln_if(CSS_PARSER_DEBUG, "Failed to parse ::{}() parameter as a compound selector", pseudo_name); + return ParseError::SyntaxError; + } + + auto compound_selector = compound_selector_or_error.release_value().release_value(); + compound_selector.combinator = Selector::Combinator::None; + + Vector compound_selectors { move(compound_selector) }; + value = Selector::create(move(compound_selectors)); + break; + } case PseudoElementMetadata::ParameterType::PTNameSelector: { // = '*' | // https://drafts.csswg.org/css-view-transitions-1/#typedef-pt-name-selector diff --git a/Libraries/LibWeb/CSS/PseudoElements.json b/Libraries/LibWeb/CSS/PseudoElements.json index 7d97350808a..fefd63850ed 100644 --- a/Libraries/LibWeb/CSS/PseudoElements.json +++ b/Libraries/LibWeb/CSS/PseudoElements.json @@ -110,6 +110,11 @@ "slider-track": { "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-track" }, + "slotted": { + "spec": "https://drafts.csswg.org/css-scoping/#slotted-pseudo", + "type": "function", + "function-syntax": "" + }, "view-transition": { "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition", "is-pseudo-root": true diff --git a/Libraries/LibWeb/CSS/Selector.cpp b/Libraries/LibWeb/CSS/Selector.cpp index c8f48b2ddd0..b358025e373 100644 --- a/Libraries/LibWeb/CSS/Selector.cpp +++ b/Libraries/LibWeb/CSS/Selector.cpp @@ -294,6 +294,11 @@ String Selector::PseudoElementSelector::serialize() const } m_value.visit( + [&builder](NonnullRefPtr const& compund_selector) { + builder.append('('); + builder.append(compund_selector->serialize()); + builder.append(')'); + }, [&builder](PTNameSelector const& pt_name_selector) { builder.append('('); if (pt_name_selector.is_universal) diff --git a/Libraries/LibWeb/CSS/Selector.h b/Libraries/LibWeb/CSS/Selector.h index ed53307cc53..10651072e6b 100644 --- a/Libraries/LibWeb/CSS/Selector.h +++ b/Libraries/LibWeb/CSS/Selector.h @@ -31,7 +31,7 @@ public: FlyString value {}; }; - using Value = Variant; + using Value = Variant>; explicit PseudoElementSelector(PseudoElement type, Value value = {}) : m_type(type) @@ -60,10 +60,13 @@ public: PTNameSelector const& pt_name_selector() const { return m_value.get(); } + // NOTE: This can't (currently) be a CompoundSelector due to cyclic dependencies. + Selector const& compound_selector() const { return m_value.get>(); } + private: PseudoElement m_type; String m_name; - Variant m_value; + Value m_value; }; struct SimpleSelector { diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index 43118b578e7..643fb7d5e21 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -613,6 +613,7 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector, int in switch (pseudo_element_metadata.parameter_type) { case CSS::PseudoElementMetadata::ParameterType::None: + case CSS::PseudoElementMetadata::ParameterType::CompoundSelector: break; case CSS::PseudoElementMetadata::ParameterType::PTNameSelector: { auto const& [is_universal, value] = pseudo_element.pt_name_selector(); diff --git a/Libraries/LibWeb/HTML/Window.cpp b/Libraries/LibWeb/HTML/Window.cpp index 6607cfe2cfb..70ecad674cf 100644 --- a/Libraries/LibWeb/HTML/Window.cpp +++ b/Libraries/LibWeb/HTML/Window.cpp @@ -1286,7 +1286,8 @@ GC::Ref Window::get_computed_style(DOM::Element& eleme auto type = parse_pseudo_element_selector(CSS::Parser::ParsingParams(associated_document()), pseudo_element.value()); // 2. If type is failure, or is a ::slotted() or ::part() pseudo-element, let obj be null. - if (!type.has_value()) { + // FIXME: Handle ::part() here too when we support it. + if (!type.has_value() || type.value().type() == CSS::PseudoElement::Slotted) { object = {}; } // 3. Otherwise let obj be the given pseudo-element of elt. diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp index 1e4a9cd424b..6455b00b0b6 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateCSSPseudoElement.cpp @@ -92,6 +92,7 @@ bool pseudo_element_supports_property(PseudoElement, PropertyID); struct PseudoElementMetadata { enum class ParameterType { None, + CompoundSelector, PTNameSelector, } parameter_type; bool is_valid_as_function; @@ -515,6 +516,8 @@ PseudoElementMetadata pseudo_element_metadata(PseudoElement pseudo_element) auto const& function_syntax = pseudo_element.get_string("function-syntax"sv).value(); if (function_syntax == ""sv) { parameter_type = "PTNameSelector"_string; + } else if (function_syntax == ""sv) { + parameter_type = "CompoundSelector"_string; } else { warnln("Unrecognized pseudo-element parameter type: `{}`", function_syntax); VERIFY_NOT_REACHED(); diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scoping/slotted-parsing.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scoping/slotted-parsing.txt new file mode 100644 index 00000000000..d48712d401d --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scoping/slotted-parsing.txt @@ -0,0 +1,38 @@ +Harness status: OK + +Found 32 tests + +13 Pass +19 Fail +Pass "::slotted" should be an invalid selector +Pass "::slotted()" should be an invalid selector +Fail "::slotted(*).class" should be an invalid selector +Pass "::slotted(*)#id {}" should be an invalid selector +Fail "::slotted(*)[attr]" should be an invalid selector +Fail "::slotted(*):host" should be an invalid selector +Fail "::slotted(*):host(div)" should be an invalid selector +Fail "::slotted(*):hover" should be an invalid selector +Fail "::slotted(*):read-only" should be an invalid selector +Fail "::slotted(*)::slotted(*)" should be an invalid selector +Fail "::slotted(*)::before::slotted(*)" should be an invalid selector +Fail "::slotted(*) span" should be an invalid selector +Pass "::slotted(*)" should be a valid selector +Pass "::slotted(div)" should be a valid selector +Pass "::slotted([attr]:hover)" should be a valid selector +Pass "::slotted(:not(.a))" should be a valid selector +Fail "::slotted(*):is()" should be a valid selector +Fail "::slotted(*):is(:hover)" should be a valid selector +Fail "::slotted(*):is(#id)" should be a valid selector +Fail "::slotted(*):where()" should be a valid selector +Fail "::slotted(*):where(:hover)" should be a valid selector +Fail "::slotted(*):where(#id)" should be a valid selector +Fail "::slotted(*):where(::before)" should be a valid selector +Pass "::slotted(*)::before" should be a valid selector +Pass "::slotted(*)::after" should be a valid selector +Pass "::slotted(*)::details-content" should be a valid selector +Pass "::slotted(*)::file-selector-button" should be a valid selector +Pass "::slotted(*)::placeholder" should be a valid selector +Pass "::slotted(*)::marker" should be a valid selector +Fail "::slotted(*)::first-line" should be an invalid selector +Fail "::slotted(*)::first-letter" should be an invalid selector +Fail "::slotted(*)::selection" should be an invalid selector \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt index aefda0483a4..0d680b765c1 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt @@ -2,8 +2,7 @@ Harness status: OK Found 1975 tests -1959 Pass -16 Fail +1975 Pass Pass Selectors-API Test Suite: HTML Pass Document supports querySelector Pass Document supports querySelectorAll @@ -819,10 +818,10 @@ Pass Document.querySelector: Syntax, group of selectors separator, whitespace be ,#group strong Pass Document.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong Pass Document.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong -Fail Document.querySelectorAll: Slotted selector: ::slotted(foo) -Fail Document.querySelector: Slotted selector: ::slotted(foo) -Fail Document.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo -Fail Document.querySelector: Slotted selector (no matching closing paren): ::slotted(foo +Pass Document.querySelectorAll: Slotted selector: ::slotted(foo) +Pass Document.querySelector: Slotted selector: ::slotted(foo) +Pass Document.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo +Pass Document.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Pass Detached Element.querySelectorAll: Type selector, matching html element: html Pass Detached Element.querySelector: Type selector, matching html element: html Pass Detached Element.querySelectorAll: Type selector, matching body element: body @@ -1249,10 +1248,10 @@ Pass Detached Element.querySelector: Syntax, group of selectors separator, white ,#group strong Pass Detached Element.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong Pass Detached Element.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong -Fail Detached Element.querySelectorAll: Slotted selector: ::slotted(foo) -Fail Detached Element.querySelector: Slotted selector: ::slotted(foo) -Fail Detached Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo -Fail Detached Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo +Pass Detached Element.querySelectorAll: Slotted selector: ::slotted(foo) +Pass Detached Element.querySelector: Slotted selector: ::slotted(foo) +Pass Detached Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo +Pass Detached Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Pass Fragment.querySelectorAll: Type selector, matching html element: html Pass Fragment.querySelector: Type selector, matching html element: html Pass Fragment.querySelectorAll: Type selector, matching body element: body @@ -1679,10 +1678,10 @@ Pass Fragment.querySelector: Syntax, group of selectors separator, whitespace be ,#group strong Pass Fragment.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong Pass Fragment.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong -Fail Fragment.querySelectorAll: Slotted selector: ::slotted(foo) -Fail Fragment.querySelector: Slotted selector: ::slotted(foo) -Fail Fragment.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo -Fail Fragment.querySelector: Slotted selector (no matching closing paren): ::slotted(foo +Pass Fragment.querySelectorAll: Slotted selector: ::slotted(foo) +Pass Fragment.querySelector: Slotted selector: ::slotted(foo) +Pass Fragment.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo +Pass Fragment.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Pass In-document Element.querySelectorAll: Type selector, matching html element: html Pass In-document Element.querySelector: Type selector, matching html element: html Pass In-document Element.querySelectorAll: Type selector, matching body element: body @@ -2111,7 +2110,7 @@ Pass In-document Element.querySelector: Syntax, group of selectors separator, wh ,#group strong Pass In-document Element.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong Pass In-document Element.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong -Fail In-document Element.querySelectorAll: Slotted selector: ::slotted(foo) -Fail In-document Element.querySelector: Slotted selector: ::slotted(foo) -Fail In-document Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo -Fail In-document Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo \ No newline at end of file +Pass In-document Element.querySelectorAll: Slotted selector: ::slotted(foo) +Pass In-document Element.querySelector: Slotted selector: ::slotted(foo) +Pass In-document Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo +Pass In-document Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scoping/slotted-parsing.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scoping/slotted-parsing.html new file mode 100644 index 00000000000..5432cfeef1f --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scoping/slotted-parsing.html @@ -0,0 +1,51 @@ + + +CSS Scoping: ::slotted pseudo parsing + + + + + + +