fix(js): honour NodeFilter.FILTER_REJECT in TreeWalker (#461)
`_accept` collapsed every non-ACCEPT filter result into a single boolean, so FILTER_REJECT behaved exactly like FILTER_SKIP: the node was not returned, but the walk still descended into its subtree. Per DOM 6.1, FILTER_REJECT rejects a node *and* its descendants. This was unreachable until #440 exposed the real NodeFilter constants, which made the canonical prune idiom if (node.tagName === 'SCRIPT') return NodeFilter.FILTER_REJECT; start yielding nodes from inside the pruned subtree instead of failing loudly — sanitizers and text extractors built on TreeWalker silently got extra content. Split the filter into a three-valued `_filter` (keeping `_accept` as the boolean wrapper the sibling/child movers use) and give nextNode a second traversal step for the reject case. That step lives in the DOM layer as `DomTree::next_after_subtree`, mirroring how #447 put `next_in_subtree` there: both share the ancestor climb, and REJECT costs no more JS/native crossings than SKIP. NodeIterator shares this walker but has no subtree pruning (DOM 6.2: FILTER_REJECT behaves as FILTER_SKIP), so `createNodeIterator` marks the walker `_rejectIsSkip` and keeps the plain step. Covered by its own test so the shared implementation cannot drift.
This commit is contained in:
1 parent
2974a71f3c
commit
1a5c27a592
4 files changed
+141
-9
No files matched your search
@@ -564,7 +564,28 @@ impl DomTree {
|
||||
if let Some(child) = current_node.first_child {
|
||||
return Some(child);
|
||||
}
|
||||
Self::climb_to_next_sibling(&inner, root, current)
|
||||
}
|
||||
|
||||
/// Returns the node after the whole subtree rooted at `current`, in document
|
||||
/// order, without leaving the subtree rooted at `root`.
|
||||
///
|
||||
/// This is `next_in_subtree` minus the descend-into-children step, which is
|
||||
/// what `NodeFilter.FILTER_REJECT` needs: it rejects a node *and* its
|
||||
/// descendants, unlike `FILTER_SKIP`, which only skips the node itself and
|
||||
/// is served by `next_in_subtree`.
|
||||
pub fn next_after_subtree(&self, root: NodeId, current: NodeId) -> Option<NodeId> {
|
||||
let inner = self.inner.borrow();
|
||||
Self::climb_to_next_sibling(&inner, root, current)
|
||||
}
|
||||
|
||||
/// Follow `current`'s next sibling, climbing ancestors until one has a next
|
||||
/// sibling — without stepping outside `root`.
|
||||
fn climb_to_next_sibling(
|
||||
inner: &DomTreeInner,
|
||||
root: NodeId,
|
||||
current: NodeId,
|
||||
) -> Option<NodeId> {
|
||||
let mut node_id = current;
|
||||
for _ in 0..=inner.nodes.len() {
|
||||
if node_id == root {
|
||||
@@ -1027,6 +1048,29 @@ mod tests {
|
||||
assert_eq!(tree.next_in_subtree(root, second), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_next_after_subtree_skips_descendants() {
|
||||
// Same shape as above: root > [first > nested, second]. Stepping past
|
||||
// `first` must land on `second`, not descend into `nested` — that is
|
||||
// what NodeFilter.FILTER_REJECT needs.
|
||||
let tree = DomTree::new();
|
||||
let root = tree.new_node(NodeData::Text { contents: "root".into() });
|
||||
let first = tree.new_node(NodeData::Text { contents: "first".into() });
|
||||
let nested = tree.new_node(NodeData::Text { contents: "nested".into() });
|
||||
let second = tree.new_node(NodeData::Text { contents: "second".into() });
|
||||
tree.append_child(tree.document(), root);
|
||||
tree.append_child(root, first);
|
||||
tree.append_child(first, nested);
|
||||
tree.append_child(root, second);
|
||||
|
||||
assert_eq!(tree.next_after_subtree(root, first), Some(second));
|
||||
// A leaf behaves identically to next_in_subtree: there is no subtree.
|
||||
assert_eq!(tree.next_after_subtree(root, nested), Some(second));
|
||||
assert_eq!(tree.next_after_subtree(root, second), None);
|
||||
// Rejecting the root itself exhausts the walk rather than escaping it.
|
||||
assert_eq!(tree.next_after_subtree(root, root), None);
|
||||
}
|
||||
|
||||
// Builds a chain of `depth` nested <div> elements under the document and
|
||||
// returns (outermost_div, innermost_div). Depth this large overflows a
|
||||
// recursive tree walk and aborts the process, so it guards the iterative
|
||||
|
||||
Reference in new issue
Block a user