fix(js): implement a real NodeIterator instead of aliasing TreeWalker (#467)
createNodeIterator returned a TreeWalker with a flag on it. The two are
different interfaces, and the alias got two things wrong.
An iterator's pointer starts *before* its root, so the first nextNode()
returns the root itself; a TreeWalker starts *at* its root and never
returns it. So createNodeIterator(el, SHOW_ELEMENT) silently skipped
exactly the element it was rooted at:
<div id=root><a></a></div> -> ["A"] (Chrome: ["DIV","A"])
It also exposed the wrong surface: no referenceNode,
pointerBeforeReferenceNode or detach (older library code still calls
detach and hit "not a function"), while leaking currentNode, firstChild,
parentNode and the rest of TreeWalker's movers.
Implemented DOM 6.2 "traverse" directly. Keeping the pointer as
(referenceNode, pointerBeforeReferenceNode) rather than a single cursor
is what makes a direction change re-yield the current node instead of
stepping over it, and a failed traversal leaves both untouched so the
iterator stays resumable either way.
The backward step is a new `DomTree::prev_in_subtree`, mirroring
`next_in_subtree`. It can live entirely in the DOM layer because a
NodeIterator prunes nothing -- unlike the TreeWalker's backward walk
(#462), where FILTER_REJECT forces the filter to be consulted at every
step of the descent.
FILTER_REJECT still behaves as FILTER_SKIP here, which the existing test
keeps covering; its expectation gains the root node the alias used to
drop. The TreeWalker's now-dead `_rejectIsSkip` branch is removed.
This commit is contained in:
1 parent
485497393d
commit
ab3ca26046
4 files changed
+224
-11
No files matched your search
@@ -1538,7 +1538,106 @@ mod tests {
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!(["P", "A"]));
|
||||
// The rejected <section> is skipped but not pruned, so <p> still shows.
|
||||
// The leading root is #467: an iterator yields the node it is rooted at.
|
||||
assert_eq!(result, serde_json::json!(["DIV", "P", "A"]));
|
||||
}
|
||||
|
||||
/// Issue #467: a NodeIterator starts *before* its root, so the first
|
||||
/// nextNode() returns the root itself. Aliasing createTreeWalker silently
|
||||
/// dropped exactly the element the iterator was rooted at.
|
||||
#[test]
|
||||
fn node_iterator_yields_the_root_node_first() {
|
||||
let mut rt = setup_runtime(r#"<div id="root"><a></a></div>"#);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
|
||||
const seen = [];
|
||||
let node;
|
||||
while ((node = it.nextNode())) seen.push(node.tagName);
|
||||
return seen;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!(["DIV", "A"]));
|
||||
}
|
||||
|
||||
/// Issue #467: the NodeIterator interface surface, and that TreeWalker-only
|
||||
/// members are not exposed on it.
|
||||
#[test]
|
||||
fn node_iterator_exposes_its_own_interface() {
|
||||
let mut rt = setup_runtime(r#"<div id="root"><a></a></div>"#);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
|
||||
const before = [it.referenceNode === root, it.pointerBeforeReferenceNode];
|
||||
it.nextNode();
|
||||
return [
|
||||
before,
|
||||
typeof it.detach,
|
||||
it.detach() === undefined,
|
||||
typeof it.previousNode,
|
||||
it.root === root,
|
||||
it.whatToShow,
|
||||
// TreeWalker-only members must not leak onto a NodeIterator.
|
||||
typeof it.currentNode,
|
||||
typeof it.firstChild,
|
||||
typeof it.parentNode,
|
||||
// The pointer advanced past the root it just returned.
|
||||
[it.referenceNode.tagName, it.pointerBeforeReferenceNode],
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
[true, true],
|
||||
"function",
|
||||
true,
|
||||
"function",
|
||||
true,
|
||||
1,
|
||||
"undefined",
|
||||
"undefined",
|
||||
"undefined",
|
||||
["DIV", false]
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue #467: previousNode() retraces the iterator, and the root is the
|
||||
/// last node it yields going backwards.
|
||||
#[test]
|
||||
fn node_iterator_previous_node_retraces_the_walk() {
|
||||
let mut rt = setup_runtime(r#"<div id="root"><a><b></b></a><c></c></div>"#);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const root = document.getElementById('root');
|
||||
const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
|
||||
const forward = [];
|
||||
let node;
|
||||
while ((node = it.nextNode())) forward.push(node.tagName);
|
||||
const backward = [];
|
||||
while ((node = it.previousNode())) backward.push(node.tagName);
|
||||
return [forward, backward];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Forward ends on <c>; going back re-yields <c> (the pointer sits after
|
||||
// it), then the rest in reverse, root included.
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
["DIV", "A", "B", "C"],
|
||||
["C", "B", "A", "DIV"]
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user