LibWeb: Delay generic :has() sibling scans until sibling roots
Mark elements reached by stepping through sibling combinators inside :has() and use that breadcrumb during generic invalidation walks. Keep the existing conservative sibling scans for mutations outside those marked subtrees so nested :is(), :not(), and nesting cases continue to invalidate correctly. Also keep :has() eager within compounds that contain ::part(). Those selectors retarget the remaining simple selectors to the part host, so deferring :has() there changes which element the pseudo-class runs against and can make ::part(foo):has(.match) spuriously match. Add a counter-based sibling-scan test and a regression test covering the ::part()/ :has() selector orderings.
This commit is contained in:
parent
85ff13870f
commit
7a5b1d9de1
7 changed files with 118 additions and 12 deletions
|
|
@ -297,8 +297,10 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size
|
|||
auto* sibling = element.next_element_sibling();
|
||||
if (!sibling)
|
||||
return false;
|
||||
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata)
|
||||
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata) {
|
||||
const_cast<DOM::Element&>(*sibling).set_in_has_scope(true);
|
||||
const_cast<DOM::Element&>(*sibling).set_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator(true);
|
||||
}
|
||||
if (!matches_compound_selector(selector, compound_index, *sibling, shadow_host, context, scope, SelectorKind::Relative, anchor))
|
||||
return false;
|
||||
return matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor, scope);
|
||||
|
|
@ -308,8 +310,10 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size
|
|||
const_cast<DOM::Element&>(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true);
|
||||
}
|
||||
for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) {
|
||||
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata)
|
||||
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata) {
|
||||
const_cast<DOM::Element&>(*sibling).set_in_has_scope(true);
|
||||
const_cast<DOM::Element&>(*sibling).set_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator(true);
|
||||
}
|
||||
if (!matches_compound_selector(selector, compound_index, *sibling, shadow_host, context, scope, SelectorKind::Relative, anchor))
|
||||
continue;
|
||||
if (matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor, scope))
|
||||
|
|
@ -1513,10 +1517,19 @@ bool matches_compound_selector(CSS::Selector const& selector, int component_list
|
|||
return s.type == CSS::Selector::SimpleSelector::Type::PseudoClass
|
||||
&& s.pseudo_class().type == CSS::PseudoClass::Has;
|
||||
};
|
||||
bool has_part_pseudo_element = false;
|
||||
for (auto const& simple_selector : compound_selector.simple_selectors) {
|
||||
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoElement
|
||||
&& simple_selector.pseudo_element().type() == CSS::PseudoElement::Part) {
|
||||
has_part_pseudo_element = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto defer_has_pseudo_class = !has_part_pseudo_element;
|
||||
|
||||
auto element_for_compound_matching { target };
|
||||
for (auto& simple_selector : compound_selector.simple_selectors.in_reverse()) {
|
||||
if (is_has_pseudo_class(simple_selector))
|
||||
if (defer_has_pseudo_class && is_has_pseudo_class(simple_selector))
|
||||
continue;
|
||||
if (!matches_simple_selector(simple_selector, element_for_compound_matching, shadow_host, context, scope, selector_kind, anchor)) {
|
||||
return false;
|
||||
|
|
@ -1540,11 +1553,13 @@ bool matches_compound_selector(CSS::Selector const& selector, int component_list
|
|||
}
|
||||
}
|
||||
}
|
||||
for (auto& simple_selector : compound_selector.simple_selectors.in_reverse()) {
|
||||
if (!is_has_pseudo_class(simple_selector))
|
||||
continue;
|
||||
if (!matches_simple_selector(simple_selector, element_for_compound_matching, shadow_host, context, scope, selector_kind, anchor)) {
|
||||
return false;
|
||||
if (defer_has_pseudo_class) {
|
||||
for (auto& simple_selector : compound_selector.simple_selectors.in_reverse()) {
|
||||
if (!is_has_pseudo_class(simple_selector))
|
||||
continue;
|
||||
if (!matches_simple_selector(simple_selector, element_for_compound_matching, shadow_host, context, scope, selector_kind, anchor)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto const& element = element_for_compound_matching;
|
||||
|
|
|
|||
|
|
@ -849,10 +849,17 @@ void StyleScope::invalidate_style_of_elements_affected_by_has()
|
|||
|| element.affected_by_has_pseudo_class_in_non_subject_position();
|
||||
};
|
||||
|
||||
auto is_in_subtree_of_has_relative_selector_with_sibling_combinator = [](DOM::Element const& element) {
|
||||
return element.in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator()
|
||||
|| element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator();
|
||||
};
|
||||
|
||||
HashTable<DOM::Element*> elements_already_invalidated_for_has;
|
||||
auto nodes = move(m_pending_nodes_for_style_invalidation_due_to_presence_of_has);
|
||||
bool should_scan_ancestor_siblings = have_has_selectors_with_relative_selector_that_has_sibling_combinator();
|
||||
for (auto& node : nodes) {
|
||||
Vector<DOM::Element*, 16> has_scope_ancestors;
|
||||
bool should_delay_ancestor_sibling_scans = false;
|
||||
for (auto* ancestor = &node; ancestor; ancestor = ancestor->parent_or_shadow_host()) {
|
||||
if (!ancestor->is_element())
|
||||
continue;
|
||||
|
|
@ -864,13 +871,20 @@ void StyleScope::invalidate_style_of_elements_affected_by_has()
|
|||
if (!is_in_has_scope(element))
|
||||
break;
|
||||
|
||||
if (elements_already_invalidated_for_has.set(&element) != AK::HashSetResult::InsertedNewEntry)
|
||||
break;
|
||||
has_scope_ancestors.append(&element);
|
||||
should_delay_ancestor_sibling_scans |= is_in_subtree_of_has_relative_selector_with_sibling_combinator(element);
|
||||
}
|
||||
|
||||
for (auto* element : has_scope_ancestors) {
|
||||
VERIFY(element);
|
||||
|
||||
if (elements_already_invalidated_for_has.set(element) != AK::HashSetResult::InsertedNewEntry)
|
||||
continue;
|
||||
|
||||
++counters.has_ancestor_walk_visits;
|
||||
element.invalidate_style_if_affected_by_has();
|
||||
element->invalidate_style_if_affected_by_has();
|
||||
|
||||
auto* parent = ancestor->parent_or_shadow_host();
|
||||
auto* parent = element->parent_or_shadow_host();
|
||||
if (!parent)
|
||||
return;
|
||||
|
||||
|
|
@ -878,6 +892,8 @@ void StyleScope::invalidate_style_of_elements_affected_by_has()
|
|||
// its style might be affected by the change in descendant node.
|
||||
if (!should_scan_ancestor_siblings)
|
||||
continue;
|
||||
if (should_delay_ancestor_sibling_scans && !is_in_subtree_of_has_relative_selector_with_sibling_combinator(*element))
|
||||
continue;
|
||||
parent->for_each_child_of_type<DOM::Element>([&](auto& ancestor_sibling_element) {
|
||||
++counters.has_ancestor_sibling_element_checks;
|
||||
if (ancestor_sibling_element.affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator()) {
|
||||
|
|
|
|||
|
|
@ -509,6 +509,12 @@ public:
|
|||
|
||||
bool affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator() const { return m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator; }
|
||||
void set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(bool value) { m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator = value; }
|
||||
// Set on any element reached by stepping through a + or ~ combinator while
|
||||
// matching a :has() argument. Lets generic invalidation defer ancestor
|
||||
// sibling scans until it reaches the sibling subtree root. Write-once,
|
||||
// intentionally never cleared.
|
||||
bool in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator() const { return m_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator; }
|
||||
void set_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator(bool value) { m_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator = value; }
|
||||
|
||||
// Set on any element that was traversed during matching of a :has() argument
|
||||
// selector (i.e. the descendant/child/sibling walk inside :has()). Lets the
|
||||
|
|
@ -714,6 +720,7 @@ private:
|
|||
bool m_affected_by_forward_positional_pseudo_class : 1 { false };
|
||||
bool m_affected_by_backward_positional_pseudo_class : 1 { false };
|
||||
bool m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator : 1 { false };
|
||||
bool m_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator : 1 { false };
|
||||
bool m_in_has_scope : 1 { false };
|
||||
bool m_fullscreen_flag : 1 { false };
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
hasAncestorWalkInvocations: 1
|
||||
hasAncestorWalkVisits: 5
|
||||
hasAncestorSiblingElementChecks: 3
|
||||
hasMatchInvocations: 1
|
||||
hasResultCacheHits: 0
|
||||
hasResultCacheMisses: 1
|
||||
styleInvalidations: 1
|
||||
2
Tests/LibWeb/Text/expected/css/part-has-target.txt
Normal file
2
Tests/LibWeb/Text/expected/css/part-has-target.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
rgba(0, 0, 0, 0)
|
||||
rgb(0, 128, 0)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<style>
|
||||
.anchor:has(+ .wrapper .match) { color: red; }
|
||||
</style>
|
||||
<div id="outer">
|
||||
<div id="anchor" class="anchor">anchor</div>
|
||||
<div class="wrapper">
|
||||
<div>
|
||||
<div>
|
||||
<span id="target">target</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>unrelated sibling</div>
|
||||
</div>
|
||||
<script>
|
||||
function printCounters() {
|
||||
let counters = internals.getStyleInvalidationCounters();
|
||||
println(`hasAncestorWalkInvocations: ${counters.hasAncestorWalkInvocations}`);
|
||||
println(`hasAncestorWalkVisits: ${counters.hasAncestorWalkVisits}`);
|
||||
println(`hasAncestorSiblingElementChecks: ${counters.hasAncestorSiblingElementChecks}`);
|
||||
println(`hasMatchInvocations: ${counters.hasMatchInvocations}`);
|
||||
println(`hasResultCacheHits: ${counters.hasResultCacheHits}`);
|
||||
println(`hasResultCacheMisses: ${counters.hasResultCacheMisses}`);
|
||||
println(`styleInvalidations: ${counters.styleInvalidations}`);
|
||||
}
|
||||
|
||||
test(() => {
|
||||
let anchor = document.getElementById("anchor");
|
||||
let target = document.getElementById("target");
|
||||
|
||||
getComputedStyle(anchor).color;
|
||||
internals.resetStyleInvalidationCounters();
|
||||
|
||||
target.classList.add("match");
|
||||
getComputedStyle(anchor).color;
|
||||
printCounters();
|
||||
});
|
||||
</script>
|
||||
19
Tests/LibWeb/Text/input/css/part-has-target.html
Normal file
19
Tests/LibWeb/Text/input/css/part-has-target.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<style>
|
||||
#host::part(p):has(.match) { background-color: red; }
|
||||
#host:has(.match)::part(p) { color: green; }
|
||||
</style>
|
||||
<div id="host">
|
||||
<span class="match"></span>
|
||||
</div>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
test(() => {
|
||||
const host = document.getElementById("host");
|
||||
const shadow = host.attachShadow({ mode: "open" });
|
||||
shadow.innerHTML = `<span id="inner" part="p">text</span>`;
|
||||
const inner = shadow.getElementById("inner");
|
||||
println(getComputedStyle(inner).backgroundColor);
|
||||
println(getComputedStyle(inner).color);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue