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;
}
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: {
// <pt-name-selector> = '*' | <custom-ident>
// https://drafts.csswg.org/css-view-transitions-1/#typedef-pt-name-selector

View file

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

View file

@ -294,6 +294,11 @@ String Selector::PseudoElementSelector::serialize() const
}
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.append('(');
if (pt_name_selector.is_universal)

View file

@ -31,7 +31,7 @@ public:
FlyString value {};
};
using Value = Variant<Empty, PTNameSelector>;
using Value = Variant<Empty, PTNameSelector, NonnullRefPtr<Selector>>;
explicit PseudoElementSelector(PseudoElement type, Value value = {})
: m_type(type)
@ -60,10 +60,13 @@ public:
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:
PseudoElement m_type;
String m_name;
Variant<Empty, PTNameSelector> m_value;
Value m_value;
};
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) {
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();

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());
// 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.

View file

@ -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 == "<pt-name-selector>"sv) {
parameter_type = "PTNameSelector"_string;
} else if (function_syntax == "<compound-selector>"sv) {
parameter_type = "CompoundSelector"_string;
} else {
warnln("Unrecognized pseudo-element parameter type: `{}`", function_syntax);
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
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
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

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>