diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index ff1ca2a..8e70cb3 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -2646,18 +2646,38 @@ class Document extends Node { } return null; }, + // DOM 6.1 "previousNode", implemented as specified (issue #462). The old + // version looked at exactly one candidate — the previous sibling's + // deepest last child — and returned null the moment it was filtered out, + // so a backward walk died mid-tree the way nextNode used to before #432. + // + // Unlike nextNode this stays in JS rather than using a DOM traversal op: + // the descent into last children has to stop on FILTER_REJECT, so the + // filter is consulted at every step anyway and there is no run of + // crossings for a native helper to collapse. previousNode() { let node = this.currentNode; - if (node === this.root) return null; - let sibling = node.previousSibling; - if (sibling) { - while (sibling.lastChild) sibling = sibling.lastChild; - if (this._accept(sibling)) { this.currentNode = sibling; return sibling; } - } - let parent = node.parentNode; - if (parent && parent !== this.root && this._accept(parent)) { - this.currentNode = parent; - return parent; + while (node !== this.root) { + let sibling = node.previousSibling; + while (sibling) { + node = sibling; + let verdict = this._filter(node); + // Descend to the deepest last descendant, but never into a rejected + // subtree — that is what makes REJECT prune backwards as well. + while (verdict !== 2 && node.lastChild) { + node = node.lastChild; + verdict = this._filter(node); + } + if (verdict === 1) { this.currentNode = node; return node; } + sibling = node.previousSibling; + } + const parent = node.parentNode; + // Reaching root (or a detached node) ends the walk: root is never + // returned by a backward traversal. + if (!parent || node === this.root) return null; + node = parent; + if (node === this.root) return null; + if (this._filter(node) === 1) { this.currentNode = node; return node; } } return null; }, diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index 6e2f001..172a70e 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1410,6 +1410,106 @@ mod tests { assert_eq!(result, serde_json::json!([["A"], ["P", "A"]])); } + /// Issue #462: previousNode() must walk reverse document order until a node + /// is accepted, not give up as soon as the first candidate is filtered out. + #[test] + fn previous_node_walks_reverse_document_order() { + let mut rt = setup_runtime(r#"
"#); + rt.run_page_init(); + let result = rt + .evaluate( + r#" + const root = document.getElementById('root'); + const w = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { + acceptNode(node) { + return node.tagName === 'B' + ? NodeFilter.FILTER_SKIP + : NodeFilter.FILTER_ACCEPT; + } + }); + const forward = []; + let node; + while ((node = w.nextNode())) forward.push(node.tagName); + const backward = []; + while ((node = w.previousNode())) backward.push(node.tagName); + return [forward, backward]; + "#, + ) + .unwrap(); + // From lives inside the rejected