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

@ -344,6 +344,58 @@ impl Range {
.chain(iter::once(end_clone))
.flat_map(move |node| node.content_boxes(can_gc))
}
/// <https://dom.spec.whatwg.org/#concept-range-bp-set>
#[allow(clippy::neg_cmp_op_on_partial_ord)]
fn set_the_start_or_end(
&self,
node: &Node,
offset: u32,
start_or_end: StartOrEnd,
) -> ErrorResult {
// Step 1. If node is a doctype, then throw an "InvalidNodeTypeError" DOMException.
if node.is_doctype() {
return Err(Error::InvalidNodeType);
}
// Step 2. If offset is greater than nodes length, then throw an "IndexSizeError" DOMException.
if offset > node.len() {
return Err(Error::IndexSize);
}
// Step 3. Let bp be the boundary point (node, offset).
// NOTE: We don't need this part.
match start_or_end {
// If these steps were invoked as "set the start"
StartOrEnd::Start => {
// Step 4.1 If ranges root is not equal to nodes root, or if bp is after the ranges end,
// set ranges end to bp.
// Step 4.2 Set ranges start to bp.
self.set_start(node, offset);
if !(self.start() <= self.end()) {
self.set_end(node, offset);
}
},
// If these steps were invoked as "set the end"
StartOrEnd::End => {
// Step 4.1 If ranges root is not equal to nodes root, or if bp is before the ranges start,
// set ranges start to bp.
// Step 4.2 Set ranges end to bp.
self.set_end(node, offset);
if !(self.end() >= self.start()) {
self.set_start(node, offset);
}
},
}
Ok(())
}
}
enum StartOrEnd {
Start,
End,
}
impl RangeMethods<crate::DomTypeHolder> for Range {
@ -365,43 +417,13 @@ impl RangeMethods<crate::DomTypeHolder> for Range {
}
/// <https://dom.spec.whatwg.org/#dom-range-setstart>
#[allow(clippy::neg_cmp_op_on_partial_ord)]
fn SetStart(&self, node: &Node, offset: u32) -> ErrorResult {
if node.is_doctype() {
// Step 1.
Err(Error::InvalidNodeType)
} else if offset > node.len() {
// Step 2.
Err(Error::IndexSize)
} else {
// Step 3.
self.set_start(node, offset);
if !(self.start() <= self.end()) {
// Step 4.
self.set_end(node, offset);
}
Ok(())
}
self.set_the_start_or_end(node, offset, StartOrEnd::Start)
}
/// <https://dom.spec.whatwg.org/#dom-range-setend>
#[allow(clippy::neg_cmp_op_on_partial_ord)]
fn SetEnd(&self, node: &Node, offset: u32) -> ErrorResult {
if node.is_doctype() {
// Step 1.
Err(Error::InvalidNodeType)
} else if offset > node.len() {
// Step 2.
Err(Error::IndexSize)
} else {
// Step 3.
self.set_end(node, offset);
if !(self.end() >= self.start()) {
// Step 4.
self.set_start(node, offset);
}
Ok(())
}
self.set_the_start_or_end(node, offset, StartOrEnd::End)
}
/// <https://dom.spec.whatwg.org/#dom-range-setstartbefore>
@ -1204,6 +1226,7 @@ impl WeakRangeVec {
}
/// Used for steps 2-3. when removing a node.
///
/// <https://dom.spec.whatwg.org/#concept-node-remove>
pub(crate) fn drain_to_parent(&self, context: &UnbindContext, child: &Node) {
if self.is_empty() {