Don't drain ranges across shadow boundaries (#37281)

The [live range pre remove
steps](https://dom.spec.whatwg.org/#live-range-pre-remove-steps) state
that:

> For each [live range](https://dom.spec.whatwg.org/#concept-live-range)
whose [start
node](https://dom.spec.whatwg.org/#concept-range-start-node) is an
[inclusive
descendant](https://dom.spec.whatwg.org/#concept-tree-inclusive-descendant)
of node, set its
[start](https://dom.spec.whatwg.org/#concept-range-start) to (parent,
index).

Elements in a shadow tree are not inclusive descendants of their hosts -
meaning we should not bubble ranges across shadow boundaries.

Includes a small fix to `Node::ranges_is_empty` which makes servo do
less work on DOM mutations, as well as some changes to `range.rs` to
match the spec better. I made these changes during debugging and they
don't feel like they're worth their own PR.

Testing: Covered by WPT

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-06-06 09:54:02 +02:00 committed by GitHub
parent 836316c844
commit 430f65584d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 55 deletions

View file

@ -752,10 +752,9 @@ impl Node {
}
pub(crate) fn ranges_is_empty(&self) -> bool {
match self.rare_data().as_ref() {
Some(data) => data.ranges.is_empty(),
None => false,
}
self.rare_data()
.as_ref()
.is_none_or(|data| data.ranges.is_empty())
}
#[inline]
@ -2665,7 +2664,9 @@ impl Node {
fn remove(node: &Node, parent: &Node, suppress_observers: SuppressObserver, can_gc: CanGc) {
parent.owner_doc().add_script_and_layout_blocker();
// Step 2.
// Step 1. Let parent be nodes parent.
// Step 2. Assert: parent is non-null.
// NOTE: We get parent as an argument instead
assert!(
node.GetParentNode()
.is_some_and(|node_parent| &*node_parent == parent)
@ -2677,11 +2678,21 @@ impl Node {
if parent.ranges_is_empty() {
None
} else {
// Step 1.
// Step 1. Let parent be nodes parent.
// Step 2. Assert: parent is not null.
// NOTE: We already have the parent.
// Step 3. Let index be nodes index.
let index = node.index();
// Steps 2-3 are handled in Node::unbind_from_tree.
// Steps 4-5.
// Steps 4-5 are handled in Node::unbind_from_tree.
// Step 6. For each live range whose start node is parent and start offset is greater than index,
// decrease its start offset by 1.
// Step 7. For each live range whose end node is parent and end offset is greater than index,
// decrease its end offset by 1.
parent.ranges().decrease_above(parent, index, 1);
// Parent had ranges, we needed the index, let's keep track of
// it to avoid computing it for other ranges when calling
// unbind_from_tree recursively.
@ -3925,7 +3936,12 @@ impl VirtualMethods for Node {
/// <https://dom.spec.whatwg.org/#concept-node-remove>
fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
self.super_type().unwrap().unbind_from_tree(context, can_gc);
if !self.ranges_is_empty() {
// Ranges should only drain to the parent from inclusive non-shadow
// including descendants. If we're in a shadow tree at this point then the
// unbind operation happened further up in the tree and we should not
// drain any ranges.
if !self.is_in_a_shadow_tree() && !self.ranges_is_empty() {
self.ranges().drain_to_parent(context, self);
}
}