From 0369b490b843c6c24b25d58627ea481f858a833c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 9 May 2025 12:20:50 +0800 Subject: [PATCH] Add before and after functions Element and CharData both have a before & after function. Also, changed the existing functions that took a Node but should take a Node or Text, i.e append, prepend and replaceChildren. These functions, along with the new before/after all take a new NodeOrText union. --- src/browser/dom/character_data.zig | 10 ++ src/browser/dom/document.zig | 17 +--- src/browser/dom/element.zig | 49 +++++++--- src/browser/dom/node.zig | 147 +++++++++++++++++++++++------ 4 files changed, 167 insertions(+), 56 deletions(-) diff --git a/src/browser/dom/character_data.zig b/src/browser/dom/character_data.zig index 7a4e0f3a..937267d8 100644 --- a/src/browser/dom/character_data.zig +++ b/src/browser/dom/character_data.zig @@ -112,6 +112,16 @@ pub const CharacterData = struct { return true; } + + pub fn _before(self: *parser.CharacterData, nodes: []const Node.NodeOrText) !void { + const ref_node = parser.characterDataToNode(self); + return Node.before(ref_node, nodes); + } + + pub fn _after(self: *parser.CharacterData, nodes: []const Node.NodeOrText) !void { + const ref_node = parser.characterDataToNode(self); + return Node.after(ref_node, nodes); + } }; // Tests diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 22e1c0a4..45dc3103 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -227,28 +227,17 @@ pub const Document = struct { return css.querySelectorAll(allocator, parser.documentToNode(self), selector); } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _prepend(self: *parser.Document, nodes: []const *parser.Node) !void { + pub fn _prepend(self: *parser.Document, nodes: []const Node.NodeOrText) !void { return Node.prepend(parser.documentToNode(self), nodes); } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _append(self: *parser.Document, nodes: []const *parser.Node) !void { + pub fn _append(self: *parser.Document, nodes: []const Node.NodeOrText) !void { return Node.append(parser.documentToNode(self), nodes); } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _replaceChildren(self: *parser.Document, nodes: []const *parser.Node) !void { + pub fn _replaceChildren(self: *parser.Document, nodes: []const Node.NodeOrText) !void { return Node.replaceChildren(parser.documentToNode(self), nodes); } - - pub fn deinit(_: *parser.Document, _: std.mem.Allocator) void {} }; const testing = @import("../../testing.zig"); diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 8ee77de7..982cebf0 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -313,24 +313,25 @@ pub const Element = struct { return css.querySelectorAll(state.arena, parser.elementToNode(self), selector); } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _prepend(self: *parser.Element, nodes: []const *parser.Node) !void { + pub fn _prepend(self: *parser.Element, nodes: []const Node.NodeOrText) !void { return Node.prepend(parser.elementToNode(self), nodes); } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _append(self: *parser.Element, nodes: []const *parser.Node) !void { + pub fn _append(self: *parser.Element, nodes: []const Node.NodeOrText) !void { return Node.append(parser.elementToNode(self), nodes); } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _replaceChildren(self: *parser.Element, nodes: []const *parser.Node) !void { + pub fn _before(self: *parser.Element, nodes: []const Node.NodeOrText) !void { + const ref_node = parser.elementToNode(self); + return Node.before(ref_node, nodes); + } + + pub fn _after(self: *parser.Element, nodes: []const Node.NodeOrText) !void { + const ref_node = parser.elementToNode(self); + return Node.after(ref_node, nodes); + } + + pub fn _replaceChildren(self: *parser.Element, nodes: []const Node.NodeOrText) !void { return Node.replaceChildren(parser.elementToNode(self), nodes); } @@ -529,4 +530,28 @@ test "Browser.DOM.Element" { .{ "el.matches('#9000')", "false" }, .{ "el.matches('.notok')", "false" }, }, .{}); + + // before + try runner.testCases(&.{ + .{ "const before_container = document.createElement('div');", "undefined" }, + .{ "document.append(before_container);", "undefined" }, + .{ "const b1 = document.createElement('div');", "undefined" }, + .{ "before_container.append(b1);", "undefined" }, + + .{ "const b1_a = document.createElement('p');", "undefined" }, + .{ "b1.before(b1_a, 'over 9000');", "undefined" }, + .{ "before_container.innerHTML", "

over 9000
" }, + }, .{}); + + // after + try runner.testCases(&.{ + .{ "const after_container = document.createElement('div');", "undefined" }, + .{ "document.append(after_container);", "undefined" }, + .{ "const a1 = document.createElement('div');", "undefined" }, + .{ "after_container.append(a1);", "undefined" }, + + .{ "const a1_a = document.createElement('p');", "undefined" }, + .{ "a1.after('over 9000', a1_a);", "undefined" }, + .{ "after_container.innerHTML", "
over 9000

" }, + }, .{}); } diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 22f41dce..79e89cf2 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -337,65 +337,72 @@ pub const Node = struct { // For now, it checks only if new nodes are not self. // TODO implements the others contraints. // see https://dom.spec.whatwg.org/#concept-node-tree - pub fn hierarchy(self: *parser.Node, nodes: []const *parser.Node) !bool { - if (nodes.len == 0) return true; - - for (nodes) |node| if (self == node) return false; - + pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool { + for (nodes) |n| { + if (n.is(self)) { + return false; + } + } return true; } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn prepend(self: *parser.Node, nodes: []const *parser.Node) !void { - if (nodes.len == 0) return; + pub fn prepend(self: *parser.Node, nodes: []const NodeOrText) !void { + if (nodes.len == 0) { + return; + } // check hierarchy - if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; + if (!hierarchy(self, nodes)) { + return parser.DOMError.HierarchyRequest; + } - const first = try parser.nodeFirstChild(self); - if (first == null) { + const doc = (try parser.nodeOwnerDocument(self)) orelse return; + + if (try parser.nodeFirstChild(self)) |first| { for (nodes) |node| { - _ = try parser.nodeAppendChild(self, node); + _ = try parser.nodeInsertBefore(self, try node.toNode(doc), first); } return; } for (nodes) |node| { - _ = try parser.nodeInsertBefore(self, node, first.?); + _ = try parser.nodeAppendChild(self, try node.toNode(doc)); } } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn append(self: *parser.Node, nodes: []const *parser.Node) !void { - if (nodes.len == 0) return; + pub fn append(self: *parser.Node, nodes: []const NodeOrText) !void { + if (nodes.len == 0) { + return; + } // check hierarchy - if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; + if (!hierarchy(self, nodes)) { + return parser.DOMError.HierarchyRequest; + } + const doc = (try parser.nodeOwnerDocument(self)) orelse return; for (nodes) |node| { - _ = try parser.nodeAppendChild(self, node); + _ = try parser.nodeAppendChild(self, try node.toNode(doc)); } } - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn replaceChildren(self: *parser.Node, nodes: []const *parser.Node) !void { - if (nodes.len == 0) return; + pub fn replaceChildren(self: *parser.Node, nodes: []const NodeOrText) !void { + if (nodes.len == 0) { + return; + } // check hierarchy - if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; + if (!hierarchy(self, nodes)) { + return parser.DOMError.HierarchyRequest; + } // remove existing children try removeChildren(self); + const doc = (try parser.nodeOwnerDocument(self)) orelse return; // add new children for (nodes) |node| { - _ = try parser.nodeAppendChild(self, node); + _ = try parser.nodeAppendChild(self, try node.toNode(doc)); } } @@ -414,7 +421,87 @@ pub const Node = struct { } } - pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {} + pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void { + const parent = try parser.nodeParentNode(self) orelse return; + const doc = (try parser.nodeOwnerDocument(parent)) orelse return; + + var sibling: ?*parser.Node = self; + // have to find the first sibling that isn't in nodes + CHECK: while (sibling) |s| { + for (nodes) |n| { + if (n.is(s)) { + sibling = try parser.nodePreviousSibling(s); + continue :CHECK; + } + } + break; + } + + if (sibling == null) { + sibling = try parser.nodeFirstChild(parent); + } + + if (sibling) |ref_node| { + for (nodes) |node| { + _ = try parser.nodeInsertBefore(parent, try node.toNode(doc), ref_node); + } + return; + } + + return Node.prepend(self, nodes); + } + + pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void { + const parent = try parser.nodeParentNode(self) orelse return; + const doc = (try parser.nodeOwnerDocument(parent)) orelse return; + + // have to find the first sibling that isn't in nodes + var sibling = try parser.nodeNextSibling(self); + CHECK: while (sibling) |s| { + for (nodes) |n| { + if (n.is(s)) { + sibling = try parser.nodeNextSibling(s); + continue :CHECK; + } + } + break; + } + + if (sibling) |ref_node| { + for (nodes) |node| { + _ = try parser.nodeInsertBefore(parent, try node.toNode(doc), ref_node); + } + return; + } + + for (nodes) |node| { + _ = try parser.nodeAppendChild(parent, try node.toNode(doc)); + } + } + + // A lot of functions take either a node or text input. + // The text input is to be converted into a Text node. + pub const NodeOrText = union(enum) { + text: []const u8, + node: *parser.Node, + + fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node { + return switch (self) { + .node => |n| n, + .text => |txt| @ptrCast(try parser.documentCreateTextNode(doc, txt)), + }; + } + + // Whether the node represented by the NodeOrText is the same as the + // given Node. Always false for text values as these represent as-of-yet + // created Text nodes. + fn is(self: NodeOrText, other: *parser.Node) bool { + return switch (self) { + .text => false, + .node => |n| n == other, + }; + } + }; }; const testing = @import("../../testing.zig");