mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
Add validation to replaceChildren
Extract Document.replaceChildren, Element.replaceChildren and DocumentFragment.replaceChildren into a common helper, Node.replaceChildren. Fixes an infinite loop in WPT test: /dom/nodes/ParentNode-replaceChildren.html
This commit is contained in:
@@ -342,3 +342,4 @@
|
|||||||
testing.expectEqual('html', doc.lastChild.nodeName);
|
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
139
src/browser/tests/element/replace_children.html
Normal file
139
src/browser/tests/element/replace_children.html
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>element.replaceChildren Tests</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="test">Original content</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script id=error_replace_with_self>
|
||||||
|
{
|
||||||
|
// Test that element.replaceChildren(element) throws HierarchyRequestError
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
doc.body.replaceChildren(doc.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_replace_with_ancestor>
|
||||||
|
{
|
||||||
|
// Test that replacing with an ancestor throws HierarchyRequestError
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const child = doc.createElement('div');
|
||||||
|
doc.body.appendChild(child);
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
child.replaceChildren(doc.body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_basic>
|
||||||
|
{
|
||||||
|
// Test basic element.replaceChildren
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const child1 = doc.createElement('div');
|
||||||
|
const child2 = doc.createElement('span');
|
||||||
|
doc.body.appendChild(child1);
|
||||||
|
|
||||||
|
doc.body.replaceChildren(child2);
|
||||||
|
|
||||||
|
testing.expectEqual(1, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual(child2, doc.body.firstChild);
|
||||||
|
testing.expectEqual(null, child1.parentNode);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_empty>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with no arguments removes all children
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
doc.body.appendChild(doc.createElement('div'));
|
||||||
|
doc.body.appendChild(doc.createElement('span'));
|
||||||
|
|
||||||
|
doc.body.replaceChildren();
|
||||||
|
|
||||||
|
testing.expectEqual(0, doc.body.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_fragment>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with DocumentFragment
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const frag = doc.createDocumentFragment();
|
||||||
|
frag.appendChild(doc.createElement('div'));
|
||||||
|
frag.appendChild(doc.createElement('span'));
|
||||||
|
|
||||||
|
doc.body.replaceChildren(frag);
|
||||||
|
|
||||||
|
testing.expectEqual(2, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual('DIV', doc.body.firstChild.tagName);
|
||||||
|
testing.expectEqual('SPAN', doc.body.lastChild.tagName);
|
||||||
|
testing.expectEqual(0, frag.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=error_fragment_replace_with_self>
|
||||||
|
{
|
||||||
|
// Test that replacing with a fragment containing self throws
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const frag = doc.createDocumentFragment();
|
||||||
|
const child = doc.createElement('div');
|
||||||
|
frag.appendChild(child);
|
||||||
|
|
||||||
|
testing.expectError('HierarchyRequest', () => {
|
||||||
|
child.replaceChildren(frag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_text>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with text
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
doc.body.appendChild(doc.createElement('div'));
|
||||||
|
|
||||||
|
doc.body.replaceChildren('Hello', 'World');
|
||||||
|
|
||||||
|
testing.expectEqual(2, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual('Hello', doc.body.firstChild.textContent);
|
||||||
|
testing.expectEqual('World', doc.body.lastChild.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_mixed>
|
||||||
|
{
|
||||||
|
// Test element.replaceChildren with mixed nodes and text
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const span = doc.createElement('span');
|
||||||
|
span.textContent = 'middle';
|
||||||
|
|
||||||
|
doc.body.replaceChildren('start', span, 'end');
|
||||||
|
|
||||||
|
testing.expectEqual(3, doc.body.childNodes.length);
|
||||||
|
testing.expectEqual('start', doc.body.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('SPAN', doc.body.childNodes[1].tagName);
|
||||||
|
testing.expectEqual('end', doc.body.childNodes[2].textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=replace_children_reparents>
|
||||||
|
{
|
||||||
|
// Test that replaceChildren properly reparents nodes from another parent
|
||||||
|
const doc = document.implementation.createHTMLDocument("title");
|
||||||
|
const div1 = doc.createElement('div');
|
||||||
|
const div2 = doc.createElement('div');
|
||||||
|
const child = doc.createElement('span');
|
||||||
|
|
||||||
|
div1.appendChild(child);
|
||||||
|
testing.expectEqual(div1, child.parentNode);
|
||||||
|
|
||||||
|
div2.replaceChildren(child);
|
||||||
|
testing.expectEqual(div2, child.parentNode);
|
||||||
|
testing.expectEqual(0, div1.childNodes.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -548,35 +548,8 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
try validateDocumentNodes(self, nodes, true);
|
try validateDocumentNodes(self, nodes, false);
|
||||||
|
return self.asNode().replaceChildren(nodes, page);
|
||||||
page.domChanged();
|
|
||||||
const parent = self.asNode();
|
|
||||||
|
|
||||||
// Remove all existing children
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append new children
|
|
||||||
const parent_is_connected = parent.isConnected();
|
|
||||||
for (nodes) |node_or_text| {
|
|
||||||
const child = try node_or_text.toNode(page);
|
|
||||||
|
|
||||||
// DocumentFragments are special - append all their children
|
|
||||||
if (child.is(Node.DocumentFragment)) |_| {
|
|
||||||
try page.appendAllChildren(child, parent);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var child_connected = false;
|
|
||||||
if (child._parent) |previous_parent| {
|
|
||||||
child_connected = child.isConnected();
|
|
||||||
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
|
||||||
}
|
|
||||||
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {
|
pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {
|
||||||
@@ -896,6 +869,10 @@ fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, compti
|
|||||||
if (has_doctype) {
|
if (has_doctype) {
|
||||||
return error.HierarchyError;
|
return error.HierarchyError;
|
||||||
}
|
}
|
||||||
|
if (has_element) {
|
||||||
|
// Doctype cannot be inserted if document already has an element
|
||||||
|
return error.HierarchyError;
|
||||||
|
}
|
||||||
has_doctype = true;
|
has_doctype = true;
|
||||||
},
|
},
|
||||||
.cdata => |cd| switch (cd._type) {
|
.cdata => |cd| switch (cd._type) {
|
||||||
@@ -918,6 +895,10 @@ fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, compti
|
|||||||
if (has_doctype) {
|
if (has_doctype) {
|
||||||
return error.HierarchyError;
|
return error.HierarchyError;
|
||||||
}
|
}
|
||||||
|
if (has_element) {
|
||||||
|
// Doctype cannot be inserted if document already has an element
|
||||||
|
return error.HierarchyError;
|
||||||
|
}
|
||||||
has_doctype = true;
|
has_doctype = true;
|
||||||
},
|
},
|
||||||
.cdata => |cd| switch (cd._type) {
|
.cdata => |cd| switch (cd._type) {
|
||||||
|
|||||||
@@ -143,25 +143,7 @@ pub fn prepend(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *P
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
page.domChanged();
|
return self.asNode().replaceChildren(nodes, page);
|
||||||
var parent = self.asNode();
|
|
||||||
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent_is_connected = parent.isConnected();
|
|
||||||
for (nodes) |node_or_text| {
|
|
||||||
const child = try node_or_text.toNode(page);
|
|
||||||
|
|
||||||
// If the new children has already a parent, remove from it.
|
|
||||||
if (child._parent) |p| {
|
|
||||||
page.removeNode(p, child, .{ .will_be_reconnected = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
|||||||
@@ -784,24 +784,7 @@ pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
page.domChanged();
|
return self.asNode().replaceChildren(nodes, page);
|
||||||
var parent = self.asNode();
|
|
||||||
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
page.removeNode(parent, child, .{ .will_be_reconnected = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent_is_connected = parent.isConnected();
|
|
||||||
for (nodes) |node_or_text| {
|
|
||||||
var child_connected = false;
|
|
||||||
const child = try node_or_text.toNode(page);
|
|
||||||
if (child._parent) |previous_parent| {
|
|
||||||
child_connected = child.isConnected();
|
|
||||||
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
|
||||||
}
|
|
||||||
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||||
|
|||||||
@@ -1005,6 +1005,49 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, page: *Page)
|
|||||||
return collections.NodeLive(.class_name).init(self, class_names.items, page);
|
return collections.NodeLive(.class_name).init(self, class_names.items, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment.
|
||||||
|
/// Validates all nodes, removes existing children, then appends new children.
|
||||||
|
pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, page: *Page) !void {
|
||||||
|
// First pass: validate all nodes and collect them
|
||||||
|
// We need to collect because DocumentFragments contribute their children, not themselves
|
||||||
|
var children_to_add: std.ArrayList(*Node) = .empty;
|
||||||
|
|
||||||
|
for (nodes) |node_or_text| {
|
||||||
|
const child = try node_or_text.toNode(page);
|
||||||
|
|
||||||
|
// DocumentFragments contribute their children, not themselves
|
||||||
|
if (child.is(DocumentFragment)) |frag| {
|
||||||
|
var frag_it = frag.asNode().childrenIterator();
|
||||||
|
while (frag_it.next()) |frag_child| {
|
||||||
|
try validateNodeInsertion(self, frag_child);
|
||||||
|
try children_to_add.append(page.call_arena, frag_child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try validateNodeInsertion(self, child);
|
||||||
|
try children_to_add.append(page.call_arena, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page.domChanged();
|
||||||
|
|
||||||
|
// Remove all existing children
|
||||||
|
var it = self.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
page.removeNode(self, child, .{ .will_be_reconnected = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new children
|
||||||
|
const parent_is_connected = self.isConnected();
|
||||||
|
for (children_to_add.items) |child| {
|
||||||
|
var child_connected = false;
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
child_connected = child.isConnected();
|
||||||
|
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
|
||||||
|
}
|
||||||
|
try page.appendNode(self, child, .{ .child_already_connected = child_connected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Writes a JSON representation of the node and its children
|
// Writes a JSON representation of the node and its children
|
||||||
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
|
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
|
||||||
// stupid json api requires this to be const,
|
// stupid json api requires this to be const,
|
||||||
|
|||||||
Reference in New Issue
Block a user