diff options
author | Xiaocheng Hu <xiaochengh.work@gmail.com> | 2025-04-28 18:56:53 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-28 10:56:53 +0000 |
commit | 02b38adf43d2cd50f4fc0c2e3a2d7e1ad232219e (patch) | |
tree | a0bb16641bfb808d2e2a6ab34973920ae3b7abf0 /components/script | |
parent | cf41012257809aa08860a1d4cd231ad585cbe488 (diff) | |
download | servo-02b38adf43d2cd50f4fc0c2e3a2d7e1ad232219e.tar.gz servo-02b38adf43d2cd50f4fc0c2e3a2d7e1ad232219e.zip |
Rewrite node insertion algorithm to match the spec (#35999)
Per [spec](https://dom.spec.whatwg.org/#concept-node-insert), adoption
of new node should be done while inserting the node. This patch moves
the call site of `adopt` to inside `insert` to match it.
It also rewrites some existing code to better match the spec without any
behavioral changes.
---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by
`[X]` when the step is complete, and replace `___` with appropriate
data: -->
- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [X] These changes fix #___ (GitHub issue number if applicable)
<!-- Either: -->
- [X] There are tests for these changes OR
- [ ] These changes do not require tests because ___
<!-- Also, please make sure that "Allow edits from maintainers" checkbox
is checked, so that we can help you if you get stuck somewhere along the
way.-->
<!-- Pull requests that do not address these steps are welcome, but they
will require additional verification as part of the review process. -->
---------
Signed-off-by: Xiaocheng Hu <xiaochengh.work@gmail.com>
Diffstat (limited to 'components/script')
-rw-r--r-- | components/script/dom/node.rs | 110 |
1 files changed, 72 insertions, 38 deletions
diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 45a107ae673..b56126076da 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -2246,9 +2246,6 @@ impl Node { }; // Step 4. - Node::adopt(node, &parent.owner_document(), can_gc); - - // Step 5. Node::insert( node, parent, @@ -2257,7 +2254,7 @@ impl Node { can_gc, ); - // Step 6. + // Step 5. Ok(DomRoot::from_ref(node)) } @@ -2269,49 +2266,64 @@ impl Node { suppress_observers: SuppressObserver, can_gc: CanGc, ) { - node.owner_doc().add_script_and_layout_blocker(); - debug_assert!(*node.owner_doc() == *parent.owner_doc()); debug_assert!(child.is_none_or(|child| Some(parent) == child.GetParentNode().as_deref())); - // Step 1. - let count = if node.is::<DocumentFragment>() { - node.children_count() + // Step 1. Let nodes be node’s children, if node is a DocumentFragment node; otherwise « node ». + rooted_vec!(let mut new_nodes); + let new_nodes = if let NodeTypeId::DocumentFragment(_) = node.type_id() { + new_nodes.extend(node.children().map(|node| Dom::from_ref(&*node))); + new_nodes.r() } else { - 1 + from_ref(&node) }; - // Step 2. - if let Some(child) = child { - if !parent.ranges_is_empty() { - let index = child.index(); - // Steps 2.1-2. - parent.ranges().increase_above(parent, index, count); - } + + // Step 2. Let count be nodes’s size. + let count = new_nodes.len(); + + // Step 3. If count is 0, then return. + if count == 0 { + return; } - rooted_vec!(let mut new_nodes); - let new_nodes = if let NodeTypeId::DocumentFragment(_) = node.type_id() { - // Step 3. - new_nodes.extend(node.children().map(|kid| Dom::from_ref(&*kid))); - // Step 4. - for kid in &*new_nodes { + + // Script and layout blockers must be added after any early return. + // `node.owner_doc()` may change during the algorithm. + let parent_document = parent.owner_doc(); + let from_document = node.owner_doc(); + from_document.add_script_and_layout_blocker(); + parent_document.add_script_and_layout_blocker(); + + // Step 4. If node is a DocumentFragment node: + if let NodeTypeId::DocumentFragment(_) = node.type_id() { + // Step 4.1. Remove its children with the suppress observers flag set. + for kid in new_nodes { Node::remove(kid, node, SuppressObserver::Suppressed, can_gc); } - // Step 5. - vtable_for(node).children_changed(&ChildrenMutation::replace_all(new_nodes.r(), &[])); + vtable_for(node).children_changed(&ChildrenMutation::replace_all(new_nodes, &[])); + // Step 4.2. Queue a tree mutation record for node with « », nodes, null, and null. let mutation = LazyCell::new(|| Mutation::ChildList { added: None, - removed: Some(new_nodes.r()), + removed: Some(new_nodes), prev: None, next: None, }); MutationObserver::queue_a_mutation_record(node, mutation); + } - new_nodes.r() - } else { - // Step 3. - from_ref(&node) - }; - // Step 6. + // Step 5. If child is non-null: + // 1. For each live range whose start node is parent and start offset is + // greater than child’s index, increase its start offset by count. + // 2. For each live range whose end node is parent and end offset is + // greater than child’s index, increase its end offset by count. + if let Some(child) = child { + if !parent.ranges_is_empty() { + parent + .ranges() + .increase_above(parent, child.index(), count.try_into().unwrap()); + } + } + + // Step 6. Let previousSibling be child’s previous sibling or parent’s last child if child is null. let previous_sibling = match suppress_observers { SuppressObserver::Unsuppressed => match child { Some(child) => child.GetPreviousSibling(), @@ -2319,9 +2331,14 @@ impl Node { }, SuppressObserver::Suppressed => None, }; - // Step 7. + + // Step 7. For each node in nodes, in tree order: for kid in new_nodes { - // Step 7.1. + // Step 7.1. Adopt node into parent’s node document. + Node::adopt(kid, &parent.owner_document(), can_gc); + + // Step 7.2. If child is null, then append node to parent’s children. + // Step 7.3. Otherwise, insert node into parent’s children before child’s index. parent.add_child(kid, child, can_gc); // Step 7.4 If parent is a shadow host whose shadow root’s slot assignment is "named" @@ -2350,12 +2367,17 @@ impl Node { kid.GetRootNode(&GetRootNodeOptions::empty()) .assign_slottables_for_a_tree(); - // Step 7.7. + // Step 7.7. For each shadow-including inclusive descendant inclusiveDescendant of node, + // in shadow-including tree order: for descendant in kid .traverse_preorder(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) { + // Step 7.7.1. Run the insertion steps with inclusiveDescendant. + // This is done in `parent.add_child()`. + // Step 7.7.2, whatwg/dom#833 + // Enqueue connected reactions for custom elements or try upgrade. if descendant.is_custom() { if descendant.is_connected() { ScriptThread::enqueue_callback_reaction( @@ -2369,13 +2391,18 @@ impl Node { } } } + if let SuppressObserver::Unsuppressed = suppress_observers { + // Step 9. Run the children changed steps for parent. + // TODO(xiaochengh): If we follow the spec and move it out of the if block, some WPT fail. Investigate. vtable_for(parent).children_changed(&ChildrenMutation::insert( previous_sibling.as_deref(), new_nodes, child, )); + // Step 8. If suppress observers flag is unset, then queue a tree mutation record for parent + // with nodes, « », previousSibling, and child. let mutation = LazyCell::new(|| Mutation::ChildList { added: Some(new_nodes), removed: None, @@ -2408,7 +2435,7 @@ impl Node { // 2) post_connection_steps from Node::insert, // we use a delayed task that will run as soon as Node::insert removes its // script/layout blocker. - node.owner_doc().add_delayed_task(task!(PostConnectionSteps: move || { + parent_document.add_delayed_task(task!(PostConnectionSteps: move || { // Step 12. For each node of staticNodeList, if node is connected, then run the // post-connection steps with node. for node in static_node_list.iter().map(Trusted::root).filter(|n| n.is_connected()) { @@ -2416,7 +2443,8 @@ impl Node { } })); - node.owner_doc().remove_script_and_layout_blocker(); + parent_document.remove_script_and_layout_blocker(); + from_document.remove_script_and_layout_blocker(); } /// <https://dom.spec.whatwg.org/#concept-node-replace-all> @@ -3239,10 +3267,16 @@ impl NodeMethods<crate::DomTypeHolder> for Node { // Step 9. let previous_sibling = child.GetPreviousSibling(); - // Step 10. + // NOTE: All existing browsers assume that adoption is performed here, which does not follow the DOM spec. + // However, if we follow the spec and delay adoption to inside `Node::insert()`, then the mutation records will + // be different, and we will fail WPT dom/nodes/MutationObserver-childList.html. let document = self.owner_document(); Node::adopt(node, &document, can_gc); + // Step 10. Let removedNodes be the empty set. + // Step 11. If child’s parent is non-null: + // 1. Set removedNodes to « child ». + // 2. Remove child with the suppress observers flag set. let removed_child = if node != child { // Step 11. Node::remove(child, self, SuppressObserver::Suppressed, can_gc); |