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
@@ -1377,6 +1377,69 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue #461: FILTER_REJECT must prune the rejected node's whole subtree,
|
||||
/// while FILTER_SKIP only skips the node and leaves descendants eligible.
|
||||
/// Collapsing both into "not accepted" let a TreeWalker yield nodes from
|
||||
/// inside a subtree the page explicitly rejected.
|
||||
#[test]
|
||||
fn tree_walker_filter_reject_prunes_the_whole_subtree() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><p>deep</p></section><a></a></div>"#,
|
||||
);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
function walk(verdict) {
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION' ? verdict : NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
const seen = [];
|
||||
let node;
|
||||
while ((node = w.nextNode())) seen.push(node.tagName);
|
||||
return seen;
|
||||
}
|
||||
return [walk(NodeFilter.FILTER_REJECT), walk(NodeFilter.FILTER_SKIP)];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// REJECT drops <p> with its <section> parent; SKIP drops only <section>.
|
||||
assert_eq!(result, serde_json::json!([["A"], ["P", "A"]]));
|
||||
}
|
||||
|
||||
/// Issue #461: NodeIterator has no subtree pruning — DOM 6.2 says
|
||||
/// FILTER_REJECT behaves as FILTER_SKIP there. The shared walker must not
|
||||
/// leak TreeWalker's pruning into it.
|
||||
#[test]
|
||||
fn node_iterator_treats_filter_reject_as_skip() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><p>deep</p></section><a></a></div>"#,
|
||||
);
|
||||
rt.run_page_init();
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION'
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
const seen = [];
|
||||
let node;
|
||||
while ((node = it.nextNode())) seen.push(node.tagName);
|
||||
return seen;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!(["P", "A"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_child_flattens_document_fragment() {
|
||||
let mut rt = setup_runtime(r#"<main id="host"></main>"#);
|
||||
|
||||
Reference in new issue
Block a user