diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index bf2590d0a..a98beff68 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -2563,16 +2563,17 @@ pub fn moveAllChildren(self: *Frame, source: *Node, parent: *Node, ref_node: ?*N // Nodes moved into a not-yet-started script run the script's // children-changed steps first (whatwg/html#10188), then each inserted // node's own ready work runs, in tree order, with everything in place. + // Ready work only happens on becoming connected, so a detached + // destination has none. if (parent.isConnected()) { if (parent.is(Element.Html.Script)) |script| { if (!script._executed) { try self.nodeIsReady(false, parent); } } - } - - for (moved.items) |child| { - try self.nodeIsReadySubtree(child); + for (moved.items) |child| { + try self.nodeIsReadySubtree(child); + } } } @@ -2681,17 +2682,17 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No // Inserting into a not-yet-started script runs the script's // children-changed steps first, then the inserted nodes' own ready work // (whatwg/html#10188: the outer script executes before an inner one). + // Ready work only happens on becoming connected, so a detached parent + // has none. if (opts.run_ready and parent_is_connected) { if (parent.is(Element.Html.Script)) |script| { if (!script._executed) { try self.nodeIsReady(false, parent); } } - } - // nodeIsReady resolves the node's owning frame itself (only for the few node - // types that have ready work), so pass the incumbent `self`. - if (opts.run_ready) { + // nodeIsReady resolves the node's owning frame itself (only for the few node + // types that have ready work), so pass the incumbent `self`. try self.nodeIsReadySubtree(child); } @@ -2931,9 +2932,16 @@ fn nodeIsReadySubtree(self: *Frame, node: *Node) !void { if (node._type != .element or node.firstChild() == null) { return self.nodeIsReady(false, node); } + + // Scripts can mutate the tree. Safe to do this since nodeIsReady re-checks + // connectivity. + var elements: std.ArrayList(*Node) = .empty; var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{}); while (tw.next()) |el| { - try self.nodeIsReady(false, el.asNode()); + try elements.append(self.call_arena, el.asNode()); + } + for (elements.items) |el| { + try self.nodeIsReady(false, el); } } diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index efbd73411..0439417b8 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1429,8 +1429,6 @@ pub fn getElementsByClassName(self: *Node, class_name: []const u8, frame: *Frame }, frame); } -/// Shared implementation of replaceChildren for Element, Document, and DocumentFragment. -/// Validates all nodes, removes existing children, then appends new children. // ParentNode.append/prepend with several nodes convert them into a fragment // first, so the insertion happens as one operation: an earlier script must // observe its later siblings inserted (and can remove them before they run). @@ -1440,29 +1438,40 @@ pub fn appendNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void _ = try self.appendChild(child, frame); return; } - const fragment = (try DocumentFragment.init(frame)).asNode(); + const fragment = try DocumentFragment.init(frame); + const fragment_node = fragment.asNode(); + // The fragment is internal — JS never sees it, and no mutation record + // targets it — so it can be reclaimed once its children have moved out. + // If conversion or insertion failed, nodes left inside stay parented to + // it (per spec), so it must live on. + defer if (fragment_node.firstChild() == null) frame._factory.destroy(fragment); for (nodes) |node_or_text| { const child = try node_or_text.toNode(frame); - _ = try fragment.appendChild(child, frame); + _ = try fragment_node.appendChild(child, frame); } - _ = try self.appendChild(fragment, frame); + _ = try self.appendChild(fragment_node, frame); } pub fn prependNodes(self: *Node, nodes: []const NodeOrText, frame: *Frame) !void { - const reference = self.firstChild(); if (nodes.len == 1) { const child = try nodes[0].toNode(frame); - _ = try self.insertBefore(child, reference, frame); + _ = try self.insertBefore(child, self.firstChild(), frame); return; } - const fragment = (try DocumentFragment.init(frame)).asNode(); + const fragment = try DocumentFragment.init(frame); + const fragment_node = fragment.asNode(); + defer if (fragment_node.firstChild() == null) frame._factory.destroy(fragment); for (nodes) |node_or_text| { const child = try node_or_text.toNode(frame); - _ = try fragment.appendChild(child, frame); + _ = try fragment_node.appendChild(child, frame); } - _ = try self.insertBefore(fragment, reference, frame); + // The reference child is evaluated after converting nodes into the + // fragment: one of the arguments may be the current first child. + _ = try self.insertBefore(fragment_node, self.firstChild(), frame); } +/// 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, frame: *Frame) !void { // First pass: validate all nodes and collect them // We need to collect because DocumentFragments contribute their children, not themselves