Skip to content

Commit 97053be

Browse files
committed
LibWeb: Fix NodeIterator retargeting during removals
Align NodeIterator with the current DOM spec traversal and pre-removal algorithms. The spec now covers the cases we were previously working around to match the behaviour of other engines. The algorithm we had implemented to protect against removal during filtering matches the spec algorithm quite closely, with slightly different factoring. See: * whatwg/dom#1479 * whatwg/dom#1477 This fixes the case where removing an ancestor of the reference or candidate reference, with the pointer before it and no following node inside the root, should retarget to the previous in-tree node instead of leaving the iterator on a detached node.
1 parent a4584d0 commit 97053be

4 files changed

Lines changed: 122 additions & 124 deletions

File tree

Libraries/LibWeb/DOM/NodeIterator.cpp

Lines changed: 106 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ namespace Web::DOM {
1515

1616
GC_DEFINE_ALLOCATOR(NodeIterator);
1717

18-
NodeIterator::NodeIterator(JS::Realm& realm, Node& root)
18+
NodeIterator::NodeIterator(JS::Realm& realm, GC::Ref<Node> root, NodePointer reference)
1919
: PlatformObject(realm)
2020
, m_root(root)
21-
, m_reference({ root })
21+
, m_reference(reference)
2222
{
23-
root.document().register_node_iterator({}, *this);
23+
m_root->document().register_node_iterator({}, *this);
2424
}
2525

2626
NodeIterator::~NodeIterator() = default;
@@ -44,17 +44,17 @@ void NodeIterator::visit_edges(Cell::Visitor& visitor)
4444
visitor.visit(m_root);
4545
visitor.visit(m_reference.node);
4646

47-
if (m_traversal_pointer.has_value())
48-
visitor.visit(m_traversal_pointer->node);
47+
if (m_candidate_reference.has_value())
48+
visitor.visit(m_candidate_reference->node);
4949
}
5050

5151
// https://dom.spec.whatwg.org/#dom-document-createnodeiterator
5252
GC::Ref<NodeIterator> NodeIterator::create(JS::Realm& realm, Node& root, unsigned what_to_show, GC::Ptr<NodeFilter> filter)
5353
{
5454
// 1. Let iterator be a new NodeIterator object.
55-
// 2. Set iterator’s root and iterator’s reference to root.
56-
// 3. Set iterator’s pointer before reference to true.
57-
auto iterator = realm.create<NodeIterator>(realm, root);
55+
// 2. Set iterator’s root to root.
56+
// 3. Set iterator’s reference to (root, true).
57+
auto iterator = realm.create<NodeIterator>(realm, root, NodePointer { root, true });
5858

5959
// 4. Set iterator’s whatToShow to whatToShow.
6060
iterator->m_what_to_show = what_to_show;
@@ -76,65 +76,82 @@ void NodeIterator::detach()
7676
// https://dom.spec.whatwg.org/#concept-nodeiterator-traverse
7777
JS::ThrowCompletionOr<GC::Ptr<Node>> NodeIterator::traverse(Direction direction)
7878
{
79-
// 1. Let node be iterator’s reference.
80-
// 2. Let beforeNode be iterator’s pointer before reference.
81-
m_traversal_pointer = m_reference;
79+
// 1. Set iterator’s candidate reference to iterator’s reference.
80+
m_candidate_reference = m_reference;
8281

83-
GC::Ptr<Node> candidate;
82+
// 2. Let result be null.
83+
GC::Ptr<Node> result;
8484

8585
// 3. While true:
8686
while (true) {
87-
// 4. Branch on direction:
87+
// 1. If type is "next":
8888
if (direction == Direction::Next) {
89-
// next
90-
// If beforeNode is false, then set node to the first node following node in iterator’s iterator collection.
91-
// If there is no such node, then return null.
92-
if (!m_traversal_pointer->is_before_node) {
93-
auto* next_node = m_traversal_pointer->node->next_in_pre_order(m_root.ptr());
94-
if (!next_node)
95-
return nullptr;
96-
m_traversal_pointer->node = *next_node;
97-
} else {
98-
// If beforeNode is true, then set it to false.
99-
m_traversal_pointer->is_before_node = false;
89+
// 1. If iterator’s candidate reference’s pointer before is false:
90+
if (!m_candidate_reference->pointer_before) {
91+
// 1. Let following be the first node following candidate reference’s node in iterator’s iterator collection.
92+
// If there is no such node, then break.
93+
auto* following = m_candidate_reference->node->next_in_pre_order(m_root);
94+
if (!following)
95+
break;
96+
97+
// 2. Set candidate reference to (following, false).
98+
m_candidate_reference = { *following, false };
10099
}
101-
} else {
102-
// previous
103-
// If beforeNode is true, then set node to the first node preceding node in iterator’s iterator collection.
104-
// If there is no such node, then return null.
105-
if (m_traversal_pointer->is_before_node) {
106-
if (m_traversal_pointer->node.ptr() == m_root.ptr())
107-
return nullptr;
108-
auto* previous_node = m_traversal_pointer->node->previous_in_pre_order();
109-
if (!previous_node)
110-
return nullptr;
111-
m_traversal_pointer->node = *previous_node;
112-
} else {
113-
// If beforeNode is false, then set it to true.
114-
m_traversal_pointer->is_before_node = true;
100+
// 2. Otherwise, set candidate reference to (candidate reference’s node, false).
101+
else {
102+
m_candidate_reference = { m_candidate_reference->node, false };
115103
}
116104
}
105+
// 2. Otherwise:
106+
else {
107+
// 1. If iterator’s candidate reference’s pointer before is true:
108+
if (m_candidate_reference->pointer_before) {
109+
// 1. Let preceding be the first node preceding candidate reference’s node in iterator’s iterator collection.
110+
// If there is no such node, then break.
111+
if (m_candidate_reference->node == m_root)
112+
break;
113+
auto* preceding = m_candidate_reference->node->previous_in_pre_order();
114+
if (!preceding)
115+
break;
116+
117+
// 2. Set iterator’s candidate reference to (preceding, true).
118+
m_candidate_reference = { *preceding, true };
119+
}
120+
// 2. Otherwise, set candidate reference to (candidate reference’s node, true).
121+
else {
122+
m_candidate_reference = { m_candidate_reference->node, true };
123+
}
124+
}
125+
126+
// 3. Let node be candidate reference’s node.
127+
GC::Ref<Node> node = m_candidate_reference->node;
117128

118-
// NOTE: If the NodeFilter deletes the iterator's current traversal pointer,
119-
// we will automatically retarget it. However, in that case, we're expected
120-
// to return the node passed to the filter, not the adjusted traversal pointer's
121-
// node after the filter returns!
122-
candidate = m_traversal_pointer->node;
129+
// 4. Let filterResult be the result of filtering node within iterator. If this throws an exception,
130+
// then set iterator’s candidate reference to null and rethrow that exception.
131+
auto filter_result = filter(node);
132+
if (filter_result.is_error()) {
133+
m_candidate_reference.clear();
134+
return filter_result.release_error();
135+
}
136+
137+
// 5. If filterResult is FILTER_ACCEPT:
138+
if (filter_result.value() == NodeFilter::Result::FILTER_ACCEPT) {
139+
// 1. Set iterator’s reference to iterator’s candidate reference.
140+
m_reference = *m_candidate_reference;
123141

124-
// 2. Let result be the result of filtering node within iterator.
125-
auto result = TRY(filter(*m_traversal_pointer->node));
142+
// 2. Set result to node.
143+
result = node;
126144

127-
// 3. If result is FILTER_ACCEPT, then break.
128-
if (result == NodeFilter::Result::FILTER_ACCEPT)
145+
// 3. Break.
129146
break;
147+
}
130148
}
131149

132-
// 4. Set iterator’s reference to node.
133-
// 5. Set iterator’s pointer before reference to beforeNode.
134-
m_reference = m_traversal_pointer.release_value();
150+
// 4. Set iterator’s candidate reference to null.
151+
m_candidate_reference.clear();
135152

136-
// 6. Return node.
137-
return candidate;
153+
// 5. Return result.
154+
return result;
138155
}
139156

140157
// https://dom.spec.whatwg.org/#concept-traversal-filter
@@ -183,76 +200,60 @@ JS::ThrowCompletionOr<NodeFilter::Result> NodeIterator::filter(Node& node)
183200
// https://dom.spec.whatwg.org/#dom-nodeiterator-nextnode
184201
JS::ThrowCompletionOr<GC::Ptr<Node>> NodeIterator::next_node()
185202
{
203+
// The nextNode() method steps are to return the result of traversing with this and "next".
186204
return traverse(Direction::Next);
187205
}
188206

189207
// https://dom.spec.whatwg.org/#dom-nodeiterator-previousnode
190208
JS::ThrowCompletionOr<GC::Ptr<Node>> NodeIterator::previous_node()
191209
{
210+
// The previousNode() method steps are to return the result of traversing with this and "previous".
192211
return traverse(Direction::Previous);
193212
}
194213

195-
void NodeIterator::run_pre_removing_steps_with_node_pointer(Node& to_be_removed_node, NodePointer& pointer)
214+
// https://dom.spec.whatwg.org/#nodeiterator-adjust
215+
NodeIterator::NodePointer NodeIterator::adjust_node_pointer(NodePointer node_pointer, Node& to_be_removed_node)
196216
{
197-
// NOTE: This function tries to match the behavior of other engines, but not the DOM specification
198-
// as it's a known issue that the spec doesn't match how major browsers behave.
199-
// Spec bug: https://github.com/whatwg/dom/issues/907
200-
201-
if (!to_be_removed_node.is_descendant_of(root()))
202-
return;
203-
204-
if (!to_be_removed_node.is_inclusive_ancestor_of(pointer.node))
205-
return;
206-
207-
if (pointer.is_before_node) {
208-
if (auto* node = to_be_removed_node.next_in_pre_order(root())) {
209-
while (node && node->is_descendant_of(to_be_removed_node))
210-
node = node->next_in_pre_order(root());
211-
if (node)
212-
pointer.node = *node;
213-
return;
214-
}
215-
if (auto* node = to_be_removed_node.previous_in_pre_order()) {
216-
if (to_be_removed_node.is_ancestor_of(pointer.node)) {
217-
while (node && node->is_descendant_of(to_be_removed_node))
218-
node = node->previous_in_pre_order();
219-
}
220-
if (node) {
221-
pointer = {
222-
.node = *node,
223-
.is_before_node = false,
224-
};
225-
}
226-
}
227-
return;
217+
// 1. If toBeRemovedNode is not an inclusive ancestor of nodePointer’s node, or toBeRemovedNode is an inclusive
218+
// ancestor of nodeIterator’s root, then return nodePointer.
219+
if (!to_be_removed_node.is_inclusive_ancestor_of(node_pointer.node) || to_be_removed_node.is_inclusive_ancestor_of(root()))
220+
return node_pointer;
221+
222+
// 2. If nodePointer’s pointer before is true:
223+
if (node_pointer.pointer_before) {
224+
// 1. Let next be toBeRemovedNode’s first following node that is an inclusive descendant of nodeIterator’s root
225+
// and is not an inclusive descendant of toBeRemovedNode, if there is such a node; otherwise null.
226+
auto* next = to_be_removed_node.next_in_pre_order(root());
227+
while (next && next->is_descendant_of(to_be_removed_node))
228+
next = next->next_in_pre_order(root());
229+
230+
// 2. If next is non-null, then return (next, true).
231+
if (next)
232+
return { *next, true };
228233
}
229234

230-
if (auto* node = to_be_removed_node.previous_in_pre_order()) {
231-
if (to_be_removed_node.is_ancestor_of(pointer.node)) {
232-
while (node && node->is_descendant_of(to_be_removed_node))
233-
node = node->previous_in_pre_order();
234-
}
235-
if (node)
236-
pointer.node = *node;
237-
return;
238-
}
239-
auto* node = to_be_removed_node.next_in_pre_order(root());
240-
if (to_be_removed_node.is_ancestor_of(pointer.node)) {
241-
while (node && node->is_descendant_of(to_be_removed_node))
242-
node = node->previous_in_pre_order();
243-
}
244-
if (node)
245-
pointer.node = *node;
235+
// 3. Let newNode be toBeRemovedNode’s parent, if toBeRemovedNode’s previous sibling is null; otherwise the
236+
// inclusive descendant of toBeRemovedNode’s previous sibling that appears last in tree order.
237+
// NOTE: This is exactly toBeRemovedNode’s previous node in pre-order. It is never null here, because step 1
238+
// guarantees toBeRemovedNode is a proper descendant of the root and thus has a parent.
239+
auto* new_node = to_be_removed_node.previous_in_pre_order();
240+
VERIFY(new_node);
241+
242+
// 4. Return (newNode, false).
243+
return { *new_node, false };
246244
}
247245

248246
// https://dom.spec.whatwg.org/#nodeiterator-pre-removing-steps
249247
void NodeIterator::run_pre_removing_steps(Node& to_be_removed_node)
250248
{
251-
// NOTE: If we're in the middle of traversal, we have to adjust the traversal pointer in response to node removal.
252-
if (m_traversal_pointer.has_value())
253-
run_pre_removing_steps_with_node_pointer(to_be_removed_node, *m_traversal_pointer);
254-
255-
run_pre_removing_steps_with_node_pointer(to_be_removed_node, m_reference);
249+
// 1. Set nodeIterator’s reference to the result of adjusting a node pointer given nodeIterator’s reference,
250+
// nodeIterator, and toBeRemovedNode.
251+
m_reference = adjust_node_pointer(m_reference, to_be_removed_node);
252+
253+
// 2. If nodeIterator’s candidate reference is non-null, then set nodeIterator’s candidate reference to the result
254+
// of adjusting a node pointer given nodeIterator’s candidate reference, nodeIterator, and toBeRemovedNode.
255+
if (m_candidate_reference.has_value())
256+
m_candidate_reference = adjust_node_pointer(*m_candidate_reference, to_be_removed_node);
256257
}
257258

258259
}

Libraries/LibWeb/DOM/NodeIterator.h

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class NodeIterator final : public Bindings::PlatformObject {
2525

2626
GC::Ref<Node> root() { return m_root; }
2727
GC::Ref<Node> reference_node() { return m_reference.node; }
28-
bool pointer_before_reference_node() const { return m_reference.is_before_node; }
28+
bool pointer_before_reference_node() const { return m_reference.pointer_before; }
2929
unsigned what_to_show() const { return m_what_to_show; }
3030

3131
GC::Ptr<NodeFilter> filter() const;
@@ -38,7 +38,14 @@ class NodeIterator final : public Bindings::PlatformObject {
3838
void run_pre_removing_steps(Node&);
3939

4040
private:
41-
explicit NodeIterator(JS::Realm&, Node& root);
41+
// https://dom.spec.whatwg.org/#node-pointer
42+
// A node pointer is a tuple consisting of a node (a node) and a pointer before (a boolean).
43+
struct NodePointer {
44+
GC::Ref<Node> node;
45+
bool pointer_before { true };
46+
};
47+
48+
explicit NodeIterator(JS::Realm&, GC::Ref<Node> root, NodePointer);
4249

4350
virtual void initialize(JS::Realm&) override;
4451
virtual void visit_edges(Cell::Visitor&) override;
@@ -56,21 +63,13 @@ class NodeIterator final : public Bindings::PlatformObject {
5663
// https://dom.spec.whatwg.org/#concept-traversal-root
5764
GC::Ref<Node> m_root;
5865

59-
struct NodePointer {
60-
GC::Ref<Node> node;
61-
62-
// https://dom.spec.whatwg.org/#nodeiterator-pointer-before-reference
63-
bool is_before_node { true };
64-
};
65-
66-
void run_pre_removing_steps_with_node_pointer(Node&, NodePointer&);
66+
NodePointer adjust_node_pointer(NodePointer, Node& to_be_removed_node);
6767

6868
// https://dom.spec.whatwg.org/#nodeiterator-reference
6969
NodePointer m_reference;
7070

71-
// While traversal is ongoing, we keep track of the current node pointer.
72-
// This allows us to adjust it during traversal if calling the filter ends up removing the node from the DOM.
73-
Optional<NodePointer> m_traversal_pointer;
71+
// https://dom.spec.whatwg.org/#nodeiterator-candidate-reference
72+
Optional<NodePointer> m_candidate_reference;
7473

7574
// https://dom.spec.whatwg.org/#concept-traversal-whattoshow
7675
unsigned m_what_to_show { 0 };

Tests/LibWeb/Text/expected/wpt-import/dom/traversal/NodeIterator-removal-during-filtering.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ Harness status: OK
22

33
Found 4 tests
44

5-
3 Pass
6-
1 Fail
5+
4 Pass
76
Pass Removing the accepted node during its own filtering retargets the reference and returns the filtered node
87
Pass Removing an already-visited subtree during filtering does not disrupt forward traversal
98
Pass referenceNode during filtering reflects the last accepted node, not the candidate
10-
Fail Removing an ancestor of the in-flight position during filtering, pointer before it, with no following node within root
9+
Pass Removing an ancestor of the in-flight position during filtering, pointer before it, with no following node within root

Tests/LibWeb/Text/expected/wpt-import/dom/traversal/NodeIterator-removal.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ Harness status: OK
22

33
Found 25 tests
44

5-
24 Pass
6-
1 Fail
5+
25 Pass
76
Pass Test removing node paras[0]
87
Pass Test removing node paras[0].firstChild
98
Pass Test removing node paras[1].firstChild
@@ -28,4 +27,4 @@ Pass Test removing node xmlComment
2827
Pass Test removing node foreignComment
2928
Pass Test removing node xmlDoctype
3029
Pass Removing an inclusive ancestor of reference, pointer before reference is true, with a following node within root
31-
Fail Removing an inclusive ancestor of reference, pointer before reference is true, with no following node within root
30+
Pass Removing an inclusive ancestor of reference, pointer before reference is true, with no following node within root

0 commit comments

Comments
 (0)