fix ChildNode after() and replaceWith() sibling ordering

after() captured node.nextSibling() once, which went stale when that
sibling was one of the nodes being inserted. Use viableNextSibling() to
find the first following sibling not in the nodes list per the DOM spec.

replaceWith() in CData had the same stale-reference problem and also
removed self before inserting, unlike Element.replaceWith() which keeps
self as the insertion anchor. Adopt the same anchor pattern: insert
before self, then remove self at the end.

Flips ChildNode-after.html from 33/45 to 45/45 and
ChildNode-replaceWith.html from 27/33 to 33/33.
This commit is contained in:
egrs
2026-02-20 09:06:00 +01:00
parent 282b64278e
commit 867f00e091
3 changed files with 35 additions and 10 deletions

View File

@@ -227,24 +227,30 @@ pub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
pub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const next = node.nextSibling();
const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, next, page);
_ = try parent.insertBefore(child, viable_next, page);
}
}
pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const next = node.nextSibling();
_ = try parent.removeChild(node, page);
const ref_node = self.asNode();
const parent = ref_node.parentNode() orelse return;
var rm_ref_node = true;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, next, page);
if (child == ref_node) {
rm_ref_node = false;
continue;
}
_ = try parent.insertBefore(child, ref_node, page);
}
if (rm_ref_node) {
_ = try parent.removeChild(ref_node, page);
}
}

View File

@@ -870,11 +870,11 @@ pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void
pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const next = node.nextSibling();
const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, next, page);
_ = try parent.insertBefore(child, viable_next, page);
}
}

View File

@@ -1069,6 +1069,25 @@ pub const NodeOrText = union(enum) {
.text => |txt| page.createTextNode(txt),
};
}
/// DOM spec: first following sibling of `node` that is not in `nodes`.
pub fn viableNextSibling(node: *Node, nodes: []const NodeOrText) ?*Node {
var sibling = node.nextSibling() orelse return null;
blk: while (true) {
for (nodes) |n| {
switch (n) {
.node => |nn| if (sibling == nn) {
sibling = sibling.nextSibling() orelse return null;
continue :blk;
},
.text => {},
}
} else {
return sibling;
}
}
return null;
}
};
const testing = @import("../../testing.zig");