Merge pull request #471 from mnaza/fix/treewalker-movers-469
fix(js): descend on FILTER_SKIP in TreeWalker child/sibling movers (#469)
This commit is contained in:
3 files changed
+425
-28
No files matched your search
Vendored
+49
-28
@@ -2710,42 +2710,63 @@ class Document extends Node {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
firstChild() {
|
||||
let child = this.currentNode.firstChild;
|
||||
while (child) {
|
||||
if (this._accept(child)) { this.currentNode = child; return child; }
|
||||
child = child.nextSibling;
|
||||
// DOM 6.1 "traverse children" (issue #469). The movers used to step
|
||||
// straight to the next sibling when a node was not accepted, so a
|
||||
// FILTER_SKIP node hid its children instead of exposing them. `edge` and
|
||||
// `step` pick the direction: first/next for forward, last/previous for
|
||||
// backward.
|
||||
_traverseChildren(edge, step) {
|
||||
let node = this.currentNode[edge];
|
||||
while (node) {
|
||||
const verdict = this._filter(node);
|
||||
if (verdict === 1) { this.currentNode = node; return node; }
|
||||
// Only SKIP leaves the children eligible; REJECT prunes the subtree.
|
||||
if (verdict === 3) {
|
||||
const child = node[edge];
|
||||
if (child) { node = child; continue; }
|
||||
}
|
||||
// Subtree exhausted: step sideways, climbing out without passing
|
||||
// root or the node the walk started from.
|
||||
while (node) {
|
||||
const sibling = node[step];
|
||||
if (sibling) { node = sibling; break; }
|
||||
const parent = node.parentNode;
|
||||
if (!parent || parent === this.root || parent === this.currentNode) return null;
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
lastChild() {
|
||||
let child = this.currentNode.lastChild;
|
||||
while (child) {
|
||||
if (this._accept(child)) { this.currentNode = child; return child; }
|
||||
child = child.previousSibling;
|
||||
// DOM 6.1 "traverse siblings" (issue #469).
|
||||
_traverseSiblings(edge, step) {
|
||||
let node = this.currentNode;
|
||||
if (node === this.root) return null;
|
||||
for (;;) {
|
||||
let sibling = node[step];
|
||||
while (sibling) {
|
||||
node = sibling;
|
||||
const verdict = this._filter(node);
|
||||
if (verdict === 1) { this.currentNode = node; return node; }
|
||||
// Descend into a skipped sibling's subtree; a rejected one is
|
||||
// off-limits, and a childless one has nothing to descend into.
|
||||
sibling = node[edge];
|
||||
if (verdict === 2 || !sibling) sibling = node[step];
|
||||
}
|
||||
node = node.parentNode;
|
||||
if (!node || node === this.root) return null;
|
||||
// An accepted parent is where the walk would go next, so there is no
|
||||
// sibling to return.
|
||||
if (this._filter(node) === 1) return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
nextSibling() {
|
||||
let sibling = this.currentNode.nextSibling;
|
||||
while (sibling) {
|
||||
if (this._accept(sibling)) { this.currentNode = sibling; return sibling; }
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
previousSibling() {
|
||||
let sibling = this.currentNode.previousSibling;
|
||||
while (sibling) {
|
||||
if (this._accept(sibling)) { this.currentNode = sibling; return sibling; }
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
firstChild() { return this._traverseChildren('firstChild', 'nextSibling'); },
|
||||
lastChild() { return this._traverseChildren('lastChild', 'previousSibling'); },
|
||||
nextSibling() { return this._traverseSiblings('firstChild', 'nextSibling'); },
|
||||
previousSibling() { return this._traverseSiblings('lastChild', 'previousSibling'); },
|
||||
// DOM 6.1 "parentNode" (issue #475). The old version looked only at the
|
||||
// immediate parent, so it couldn't climb past a skipped ancestor; it also
|
||||
// excluded `root` as a result yet stepped to root's own parent when
|
||||
// currentNode was root — returning a node OUTSIDE the walker's subtree.
|
||||
// currentNode was root, returning a node OUTSIDE the walker's subtree.
|
||||
// The loop's `node !== this.root` guard is what keeps the walk inside
|
||||
// root while still allowing root itself to be returned.
|
||||
parentNode() {
|
||||
|
||||
@@ -1920,6 +1920,99 @@ mod tests {
|
||||
assert_eq!(result.value.unwrap(), serde_json::json!([1, 1, 400]));
|
||||
}
|
||||
|
||||
/// Issue #469: FILTER_SKIP leaves a skipped node's children eligible, so
|
||||
/// firstChild()/lastChild() must descend into them. FILTER_REJECT must not.
|
||||
#[test]
|
||||
fn tree_walker_child_movers_descend_on_skip_but_not_on_reject() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><a></a><b></b></section></div>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
function mover(verdict, method) {
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION' ? verdict : NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
const found = w[method]();
|
||||
return found ? found.tagName : null;
|
||||
}
|
||||
return [
|
||||
mover(NodeFilter.FILTER_SKIP, 'firstChild'),
|
||||
mover(NodeFilter.FILTER_SKIP, 'lastChild'),
|
||||
mover(NodeFilter.FILTER_REJECT, 'firstChild'),
|
||||
mover(NodeFilter.FILTER_REJECT, 'lastChild'),
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// SKIP descends into <section>; REJECT prunes it and finds nothing else.
|
||||
assert_eq!(result, serde_json::json!(["A", "B", null, null]));
|
||||
}
|
||||
|
||||
/// Issue #469: nextSibling()/previousSibling() must descend into a skipped
|
||||
/// sibling's subtree rather than stepping straight over it.
|
||||
#[test]
|
||||
fn tree_walker_sibling_movers_descend_into_skipped_siblings() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><p id="start"></p><section><a></a></section><q></q></div>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
function mover(verdict, method, from) {
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION' ? verdict : NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
w.currentNode = document.getElementById(from);
|
||||
const found = w[method]();
|
||||
return found ? found.tagName : null;
|
||||
}
|
||||
return [
|
||||
// <section> is skipped, so its child <a> is the next sibling.
|
||||
mover(NodeFilter.FILTER_SKIP, 'nextSibling', 'start'),
|
||||
// Rejected: the subtree is off-limits, so skip past to <q>.
|
||||
mover(NodeFilter.FILTER_REJECT, 'nextSibling', 'start'),
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!(["A", "Q"]));
|
||||
}
|
||||
|
||||
/// Issue #469: the backward sibling mover descends to *last* children.
|
||||
#[test]
|
||||
fn tree_walker_previous_sibling_descends_to_last_child() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<div id="root"><section><a></a><b></b></section><p id="start"></p></div>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode(node) {
|
||||
return node.tagName === 'SECTION'
|
||||
? NodeFilter.FILTER_SKIP
|
||||
: NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
w.currentNode = document.getElementById('start');
|
||||
const found = w.previousSibling();
|
||||
return found ? found.tagName : null;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Reverse order descends to <section>'s last child, not its first.
|
||||
assert_eq!(result, serde_json::json!("B"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_child_flattens_document_fragment() {
|
||||
let mut rt = setup_runtime(r#"<main id="host"></main>"#);
|
||||
|
||||
Reference in new issue
Block a user