LibWeb/CSS: Parse the ::slotted pseudo-element

This commit is contained in:
Shannon Booth 2025-07-12 16:15:06 +12:00 committed by Sam Atkins
commit 9054ff29f0
Notes: github-actions[bot] 2025-07-15 12:55:25 +00:00
10 changed files with 141 additions and 21 deletions

View file

@ -466,6 +466,20 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
return ParseError::SyntaxError; return ParseError::SyntaxError;
} }
break; 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: { case PseudoElementMetadata::ParameterType::PTNameSelector: {
// <pt-name-selector> = '*' | <custom-ident> // <pt-name-selector> = '*' | <custom-ident>
// https://drafts.csswg.org/css-view-transitions-1/#typedef-pt-name-selector // https://drafts.csswg.org/css-view-transitions-1/#typedef-pt-name-selector

View file

@ -110,6 +110,11 @@
"slider-track": { "slider-track": {
"spec": "https://drafts.csswg.org/css-forms-1/#selectordef-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": "<compound-selector>"
},
"view-transition": { "view-transition": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition", "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition",
"is-pseudo-root": true "is-pseudo-root": true

View file

@ -294,6 +294,11 @@ String Selector::PseudoElementSelector::serialize() const
} }
m_value.visit( m_value.visit(
[&builder](NonnullRefPtr<Selector> const& compund_selector) {
builder.append('(');
builder.append(compund_selector->serialize());
builder.append(')');
},
[&builder](PTNameSelector const& pt_name_selector) { [&builder](PTNameSelector const& pt_name_selector) {
builder.append('('); builder.append('(');
if (pt_name_selector.is_universal) if (pt_name_selector.is_universal)

View file

@ -31,7 +31,7 @@ public:
FlyString value {}; FlyString value {};
}; };
using Value = Variant<Empty, PTNameSelector>; using Value = Variant<Empty, PTNameSelector, NonnullRefPtr<Selector>>;
explicit PseudoElementSelector(PseudoElement type, Value value = {}) explicit PseudoElementSelector(PseudoElement type, Value value = {})
: m_type(type) : m_type(type)
@ -60,10 +60,13 @@ public:
PTNameSelector const& pt_name_selector() const { return m_value.get<PTNameSelector>(); } PTNameSelector const& pt_name_selector() const { return m_value.get<PTNameSelector>(); }
// NOTE: This can't (currently) be a CompoundSelector due to cyclic dependencies.
Selector const& compound_selector() const { return m_value.get<NonnullRefPtr<Selector>>(); }
private: private:
PseudoElement m_type; PseudoElement m_type;
String m_name; String m_name;
Variant<Empty, PTNameSelector> m_value; Value m_value;
}; };
struct SimpleSelector { struct SimpleSelector {

View file

@ -613,6 +613,7 @@ void dump_selector(StringBuilder& builder, CSS::Selector const& selector, int in
switch (pseudo_element_metadata.parameter_type) { switch (pseudo_element_metadata.parameter_type) {
case CSS::PseudoElementMetadata::ParameterType::None: case CSS::PseudoElementMetadata::ParameterType::None:
case CSS::PseudoElementMetadata::ParameterType::CompoundSelector:
break; break;
case CSS::PseudoElementMetadata::ParameterType::PTNameSelector: { case CSS::PseudoElementMetadata::ParameterType::PTNameSelector: {
auto const& [is_universal, value] = pseudo_element.pt_name_selector(); auto const& [is_universal, value] = pseudo_element.pt_name_selector();

View file

@ -1286,7 +1286,8 @@ GC::Ref<CSS::CSSStyleDeclaration> Window::get_computed_style(DOM::Element& eleme
auto type = parse_pseudo_element_selector(CSS::Parser::ParsingParams(associated_document()), pseudo_element.value()); 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. // 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 = {}; object = {};
} }
// 3. Otherwise let obj be the given pseudo-element of elt. // 3. Otherwise let obj be the given pseudo-element of elt.

View file

@ -92,6 +92,7 @@ bool pseudo_element_supports_property(PseudoElement, PropertyID);
struct PseudoElementMetadata { struct PseudoElementMetadata {
enum class ParameterType { enum class ParameterType {
None, None,
CompoundSelector,
PTNameSelector, PTNameSelector,
} parameter_type; } parameter_type;
bool is_valid_as_function; 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(); auto const& function_syntax = pseudo_element.get_string("function-syntax"sv).value();
if (function_syntax == "<pt-name-selector>"sv) { if (function_syntax == "<pt-name-selector>"sv) {
parameter_type = "PTNameSelector"_string; parameter_type = "PTNameSelector"_string;
} else if (function_syntax == "<compound-selector>"sv) {
parameter_type = "CompoundSelector"_string;
} else { } else {
warnln("Unrecognized pseudo-element parameter type: `{}`", function_syntax); warnln("Unrecognized pseudo-element parameter type: `{}`", function_syntax);
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();

View file

@ -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

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 1975 tests Found 1975 tests
1959 Pass 1975 Pass
16 Fail
Pass Selectors-API Test Suite: HTML Pass Selectors-API Test Suite: HTML
Pass Document supports querySelector Pass Document supports querySelector
Pass Document supports querySelectorAll Pass Document supports querySelectorAll
@ -819,10 +818,10 @@ Pass Document.querySelector: Syntax, group of selectors separator, whitespace be
,#group strong ,#group strong
Pass Document.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#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 Pass Document.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong
Fail Document.querySelectorAll: Slotted selector: ::slotted(foo) Pass Document.querySelectorAll: Slotted selector: ::slotted(foo)
Fail Document.querySelector: Slotted selector: ::slotted(foo) Pass Document.querySelector: Slotted selector: ::slotted(foo)
Fail Document.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Pass Document.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo
Fail Document.querySelector: 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.querySelectorAll: Type selector, matching html element: html
Pass Detached Element.querySelector: 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 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 ,#group strong
Pass Detached Element.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#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 Pass Detached Element.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong
Fail Detached Element.querySelectorAll: Slotted selector: ::slotted(foo) Pass Detached Element.querySelectorAll: Slotted selector: ::slotted(foo)
Fail Detached Element.querySelector: Slotted selector: ::slotted(foo) Pass Detached Element.querySelector: Slotted selector: ::slotted(foo)
Fail Detached Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Pass 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.querySelector: Slotted selector (no matching closing paren): ::slotted(foo
Pass Fragment.querySelectorAll: Type selector, matching html element: html Pass Fragment.querySelectorAll: Type selector, matching html element: html
Pass Fragment.querySelector: Type selector, matching html element: html Pass Fragment.querySelector: Type selector, matching html element: html
Pass Fragment.querySelectorAll: Type selector, matching body element: body 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 ,#group strong
Pass Fragment.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#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 Pass Fragment.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong
Fail Fragment.querySelectorAll: Slotted selector: ::slotted(foo) Pass Fragment.querySelectorAll: Slotted selector: ::slotted(foo)
Fail Fragment.querySelector: Slotted selector: ::slotted(foo) Pass Fragment.querySelector: Slotted selector: ::slotted(foo)
Fail Fragment.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Pass Fragment.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo
Fail Fragment.querySelector: 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.querySelectorAll: Type selector, matching html element: html
Pass In-document Element.querySelector: 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 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 ,#group strong
Pass In-document Element.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#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 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) Pass In-document Element.querySelectorAll: Slotted selector: ::slotted(foo)
Fail In-document Element.querySelector: Slotted selector: ::slotted(foo) Pass In-document Element.querySelector: Slotted selector: ::slotted(foo)
Fail In-document Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Pass 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 Pass In-document Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Scoping: ::slotted pseudo parsing</title>
<link rel="author" title="Rune Lillesveen" href="mailto:rune@opera.com">
<link rel="help" href="https://drafts.csswg.org/css-scoping/#slotted-pseudo">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../css/support/parsing-testcommon.js"></script>
<style id="styleElm">
</style>
<script>
test_invalid_selector("::slotted");
test_invalid_selector("::slotted()");
test_invalid_selector("::slotted(*).class");
test_invalid_selector("::slotted(*)#id {}");
test_invalid_selector("::slotted(*)[attr]");
test_invalid_selector("::slotted(*):host");
test_invalid_selector("::slotted(*):host(div)");
test_invalid_selector("::slotted(*):hover");
test_invalid_selector("::slotted(*):read-only");
test_invalid_selector("::slotted(*)::slotted(*)");
test_invalid_selector("::slotted(*)::before::slotted(*)");
test_invalid_selector("::slotted(*) span");
test_valid_selector("::slotted(*)");
test_valid_selector("::slotted(div)");
test_valid_selector("::slotted([attr]:hover)");
test_valid_selector("::slotted(:not(.a))");
test_valid_forgiving_selector("::slotted(*):is()");
test_valid_forgiving_selector("::slotted(*):is(:hover)");
test_valid_forgiving_selector("::slotted(*):is(#id)");
test_valid_forgiving_selector("::slotted(*):where()");
test_valid_forgiving_selector("::slotted(*):where(:hover)");
test_valid_forgiving_selector("::slotted(*):where(#id)");
test_valid_forgiving_selector("::slotted(*):where(::before)");
// Allow tree-abiding pseudo elements after ::slotted
test_valid_selector("::slotted(*)::before");
test_valid_selector("::slotted(*)::after");
test_valid_selector("::slotted(*)::details-content");
test_valid_selector("::slotted(*)::file-selector-button");
test_valid_selector("::slotted(*)::placeholder");
test_valid_selector("::slotted(*)::marker");
// Other pseudo elements not valid after ::slotted
test_invalid_selector("::slotted(*)::first-line");
test_invalid_selector("::slotted(*)::first-letter");
test_invalid_selector("::slotted(*)::selection");
</script>