LibWeb: Add ::slotted() pseudo-element support

Implements `::slotted()` to enough extent we could pass the imported WPT
test and make substantial layout correctness improvement on
https://www.rottentomatoes.com/
This commit is contained in:
Aliaksandr Kalenik 2025-09-03 14:58:35 +02:00 committed by Sam Atkins
parent 8986e1f1ec
commit 4c7da460dc
8 changed files with 115 additions and 5 deletions

View File

@ -137,6 +137,13 @@ Selector::Selector(Vector<CompoundSelector>&& compound_selectors)
void Selector::collect_ancestor_hashes()
{
if (is_slotted()) {
// Ancestor filtering is not supported for slotted selectors, because those
// are supposed to be collected for element inside a slot, while being
// matched against slot element.
return;
}
size_t next_hash_index = 0;
auto append_unique_hash = [&](u32 hash) -> bool {
if (next_hash_index >= m_ancestor_hashes.size())

View File

@ -228,6 +228,8 @@ public:
size_t sibling_invalidation_distance() const;
bool is_slotted() const { return m_pseudo_element.has_value() && m_pseudo_element->type() == PseudoElement::Slotted; }
private:
explicit Selector(Vector<CompoundSelector>&&);

View File

@ -1136,6 +1136,10 @@ static inline bool matches(CSS::Selector::SimpleSelector const& component, DOM::
case CSS::Selector::SimpleSelector::Type::PseudoClass:
return matches_pseudo_class(component.pseudo_class(), element, shadow_host, context, scope, selector_kind);
case CSS::Selector::SimpleSelector::Type::PseudoElement:
if (component.pseudo_element().type() == CSS::PseudoElement::Slotted) {
VERIFY(context.slotted_element);
return matches(component.pseudo_element().compound_selector(), *context.slotted_element, shadow_host, context);
}
// Pseudo-element matching/not-matching is handled in the top level matches().
return true;
case CSS::Selector::SimpleSelector::Type::Nesting:

View File

@ -19,6 +19,7 @@ enum class SelectorKind {
struct MatchContext {
GC::Ptr<CSS::CSSStyleSheet const> style_sheet_for_rule {};
GC::Ptr<DOM::Element const> subject {};
GC::Ptr<DOM::Element const> slotted_element {}; // Only set when matching a ::slotted() pseudo-element
bool collect_per_element_selector_involvement_metadata { false };
CSS::PseudoClassBitmap attempted_pseudo_class_matches {};
};

View File

@ -526,14 +526,17 @@ Vector<MatchingRule const*> StyleComputer::collect_matching_rules(DOM::Element c
auto from_user_agent_or_user_stylesheet = rule_to_run.cascade_origin == CascadeOrigin::UserAgent || rule_to_run.cascade_origin == CascadeOrigin::User;
// NOTE: Inside shadow trees, we only match rules that are defined in the shadow tree's style sheets.
// The key exception is the shadow tree's *shadow host*, which needs to match :host rules from inside the shadow root.
// Also note that UA or User style sheets don't have a scope, so they are always relevant.
// Exceptions are:
// - the shadow tree's *shadow host*, which needs to match :host rules from inside the shadow root.
// - ::slotted() rules, which need to match elements assigned to slots from inside the shadow root.
// - UA or User style sheets don't have a scope, so they are always relevant.
// FIXME: We should reorganize the data so that the document-level StyleComputer doesn't cache *all* rules,
// but instead we'd have some kind of "style scope" at the document level, and also one for each shadow root.
// Then we could only evaluate rules from the current style scope.
bool rule_is_relevant_for_current_scope = rule_root == shadow_root
|| (element_shadow_root && rule_root == element_shadow_root)
|| from_user_agent_or_user_stylesheet;
|| from_user_agent_or_user_stylesheet
|| rule_to_run.slotted;
if (!rule_is_relevant_for_current_scope)
return;
@ -554,7 +557,7 @@ Vector<MatchingRule const*> StyleComputer::collect_matching_rules(DOM::Element c
}
} else {
for (auto const& rule : rules) {
if (!rule.contains_pseudo_element && filter_namespace_rule(element_namespace_uri, rule))
if ((rule.slotted || !rule.contains_pseudo_element) && filter_namespace_rule(element_namespace_uri, rule))
add_rule_to_run(rule);
}
}
@ -580,6 +583,14 @@ Vector<MatchingRule const*> StyleComputer::collect_matching_rules(DOM::Element c
add_rules_from_cache(*rule_cache);
}
if (auto assigned_slot = element.assigned_slot_internal()) {
if (auto const* slot_shadow_root = as_if<DOM::ShadowRoot>(assigned_slot->root())) {
if (auto const* rule_cache = rule_cache_for_cascade_origin(cascade_origin, qualified_layer_name, slot_shadow_root)) {
add_rules_to_run(rule_cache->slotted_rules);
}
}
}
Vector<MatchingRule const*> matching_rules;
matching_rules.ensure_capacity(rules_to_run.size());
@ -602,7 +613,19 @@ Vector<MatchingRule const*> StyleComputer::collect_matching_rules(DOM::Element c
ScopeGuard guard = [&] {
attempted_pseudo_class_matches |= context.attempted_pseudo_class_matches;
};
if (!SelectorEngine::matches(selector, element, shadow_host_to_use, context, pseudo_element))
if (selector.is_slotted()) {
if (!element.assigned_slot_internal())
continue;
// We're collecting rules for element, which is assigned to a slot.
// For ::slotted() matching, slot should be used as a subject instead of element,
// while element itself is saved in matching context, so selector engine could
// switch back to it when matching inside ::slotted() argument.
auto const& slot = *element.assigned_slot_internal();
context.slotted_element = &element;
context.subject = &slot;
if (!SelectorEngine::matches(selector, slot, shadow_host_to_use, context, PseudoElement::Slotted))
continue;
} else if (!SelectorEngine::matches(selector, element, shadow_host_to_use, context, pseudo_element))
continue;
matching_rules.append(&rule_to_run);
}
@ -2901,6 +2924,7 @@ void StyleComputer::make_rule_cache_for_cascade_origin(CascadeOrigin cascade_ori
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoElement) {
matching_rule.contains_pseudo_element = true;
pseudo_element = simple_selector.pseudo_element().type();
matching_rule.slotted = pseudo_element == PseudoElement::Slotted;
}
}
if (!contains_root_pseudo_class) {
@ -3404,6 +3428,10 @@ bool StyleComputer::have_has_selectors() const
void RuleCache::add_rule(MatchingRule const& matching_rule, Optional<PseudoElement> pseudo_element, bool contains_root_pseudo_class)
{
if (matching_rule.slotted) {
slotted_rules.append(matching_rule);
return;
}
// NOTE: We traverse the simple selectors in reverse order to make sure that class/ID buckets are preferred over tag buckets
// in the common case of div.foo or div#foo selectors.
auto add_to_id_bucket = [&](FlyString const& name) {

View File

@ -87,6 +87,7 @@ struct MatchingRule {
u32 specificity { 0 };
CascadeOrigin cascade_origin;
bool contains_pseudo_element { false };
bool slotted { false };
// Helpers to deal with the fact that `rule` might be a CSSStyleRule or a CSSNestedDeclarations
CSSStyleProperties const& declaration() const;
@ -117,6 +118,7 @@ struct RuleCache {
HashMap<FlyString, Vector<MatchingRule>, AK::ASCIICaseInsensitiveFlyStringTraits> rules_by_attribute_name;
Array<Vector<MatchingRule>, to_underlying(CSS::PseudoElement::KnownPseudoElementCount)> rules_by_pseudo_element;
Vector<MatchingRule> root_rules;
Vector<MatchingRule> slotted_rules;
Vector<MatchingRule> other_rules;
HashMap<FlyString, NonnullRefPtr<Animations::KeyframeEffect::KeyFrameSet>> rules_by_animation_keyframes;

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Scoping Module Level 1 - A green box reference</title>
<link rel="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"/>
</head>
<body>
<p>Test passes if you see a single 100px by 100px green box below.</p>
<div style="width: 100px; height: 100px; background: green;"></div>
</body>
</html>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Scoping Module Level 1 - :slotted pseudo element must allow selecting elements assigned to a slot element</title>
<link rel="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"/>
<link rel="help" href="http://www.w3.org/TR/css-scoping-1/#slotted-pseudo">
<link rel="match" href="../../../../expected/wpt-import/css/css-scoping/reference/green-box.html"/>
</head>
<body>
<style>
my-host {
display: block;
width: 100px;
height: 100px;
color: red;
background: green;
}
my-host > div, nested-host {
display: block;
width: 100px;
height: 25px;
}
</style>
<p>Test passes if you see a single 100px by 100px green box below.</p>
<my-host>
<div class="green">FAIL1</div>
<myelem><span>FAIL2</span></myelem>
<nested-host>
<span>FAIL3</span>
</nested-host>
<another-host>
<b>FAIL4</b>
</another-host>
</my-host>
<script>
try {
var shadowHost = document.querySelector('my-host');
shadowRoot = shadowHost.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<slot></slot><style> ::slotted(.green), ::slotted(myelem) { color:green; } </style>';
shadowHost = document.querySelector('nested-host');
shadowRoot = shadowHost.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<style> .mydiv ::slotted(*) { color:green; } </style><div class=mydiv><slot></slot></div>';
shadowHost = document.querySelector('another-host');
shadowRoot = shadowHost.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<style> ::slotted(*) { color:green; } </style><slot></slot>';
} catch (exception) {
document.body.appendChild(document.createTextNode(exception));
}
</script>
</body>
</html>