From 255b45d07b214b733fbc72921f3dfce12d6ef274 Mon Sep 17 00:00:00 2001 From: nikneym Date: Wed, 24 Sep 2025 20:21:08 +0300 Subject: [PATCH 01/10] initial `insertAdjacentHTML` attempt --- src/browser/dom/element.zig | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 02d01c4b..13e4049a 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -231,6 +231,71 @@ pub const Element = struct { } } + /// Parses the given `input` string and inserts it's children to an element at given `position`. + /// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML + /// + /// TODO: Support for XML parsing and `TrustedHTML` instances. + pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void { + const self_node = parser.elementToNode(self); + const doc = parser.nodeOwnerDocument(self_node) orelse { + return parser.DOMError.WrongDocument; + }; + + // Parse the fragment. + const fragment = try parser.documentParseFragmentFromStr(doc, input); + const fragment_node = parser.documentFragmentToNode(fragment); + + // We always get it wrapped like so: + // { ... } + const html = parser.nodeFirstChild(fragment_node) orelse return; + const head = parser.nodeFirstChild(html) orelse return; + const body = parser.nodeNextSibling(head) orelse return; + + const children = try parser.nodeGetChildNodes(body); + const len = parser.nodeListLength(children); + + if (std.mem.eql(u8, position, "beforeend")) { + for (0..len) |_| { + const child = parser.nodeListItem(children, 0) orelse continue; + _ = try parser.nodeInsertBefore(self_node, child, null); + } + } else if (std.mem.eql(u8, position, "afterbegin")) { + const target = parser.nodeFirstChild(self_node) orelse self_node; + for (0..len) |_| { + const child = parser.nodeListItem(children, 0) orelse continue; + _ = try parser.nodeInsertBefore(target, child, null); + } + } else if (std.mem.eql(u8, position, "beforebegin")) { + const parent = parser.nodeParentNode(self_node) orelse { + return error.NoModificationAllowed; + }; + + // Make sure parent is not Document. + // Should also check for document_fragment and document_type? + if (parser.nodeType(parent) == .document) { + return error.NoModificationAllowed; + } + + for (0..len) |_| { + const child = parser.nodeListItem(children, 0) orelse continue; + _ = try parser.nodeInsertBefore(parent, child, self_node); + } + } else if (std.mem.eql(u8, position, "afterend")) { + const parent = parser.nodeParentNode(self_node) orelse { + return error.NoModificationAllowed; + }; + + if (parser.nodeType(parent) == .document) { + return error.NoModificationAllowed; + } + + for (0..len) |_| { + const child = parser.nodeListItem(children, 0) orelse continue; + _ = try parser.nodeInsertBefore(parent, child, null); + } + } + } + // The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector. // Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null. pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element { From 923491a510400ff66815b92b79e4bb56f6499f8b Mon Sep 17 00:00:00 2001 From: nikneym Date: Wed, 24 Sep 2025 20:21:48 +0300 Subject: [PATCH 02/10] make `ref_node` of `nodeInsertBefore` nullable --- src/browser/netsurf.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 823d6546..114d4578 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -1363,7 +1363,7 @@ pub fn nodeHasChildNodes(node: *Node) bool { return res; } -pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: *Node) !*Node { +pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: ?*Node) !*Node { var res: ?*Node = null; const err = nodeVtable(node).dom_node_insert_before.?(node, new_node, ref_node, &res); try DOMErr(err); From 902b8fc789cdfe5df1e0cf627729b763f2870c79 Mon Sep 17 00:00:00 2001 From: nikneym Date: Wed, 24 Sep 2025 20:26:05 +0300 Subject: [PATCH 03/10] add `insertAdjacentHTML` test --- src/tests/dom/element.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tests/dom/element.html b/src/tests/dom/element.html index f656d5ca..686a654b 100644 --- a/src/tests/dom/element.html +++ b/src/tests/dom/element.html @@ -289,3 +289,17 @@ linkElement.rel = "stylesheet"; testing.expectEqual("stylesheet", linkElement.rel); + + From 3eb0d57d5b37e260b213209dedf9a71cdd2550c3 Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 25 Sep 2025 14:41:50 +0300 Subject: [PATCH 04/10] correct element insertation in `insertAdjacentHTML` * also DRY since the loop is repeated multiple times. --- src/browser/dom/element.zig | 75 ++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 13e4049a..219e741c 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -242,57 +242,56 @@ pub const Element = struct { }; // Parse the fragment. + // Should return error.Syntax on fail? const fragment = try parser.documentParseFragmentFromStr(doc, input); const fragment_node = parser.documentFragmentToNode(fragment); // We always get it wrapped like so: // { ... } - const html = parser.nodeFirstChild(fragment_node) orelse return; - const head = parser.nodeFirstChild(html) orelse return; - const body = parser.nodeNextSibling(head) orelse return; + // None of the following can be null. + const html = parser.nodeFirstChild(fragment_node).?; + const body = parser.nodeLastChild(html).?; const children = try parser.nodeGetChildNodes(body); - const len = parser.nodeListLength(children); - if (std.mem.eql(u8, position, "beforeend")) { - for (0..len) |_| { - const child = parser.nodeListItem(children, 0) orelse continue; - _ = try parser.nodeInsertBefore(self_node, child, null); - } - } else if (std.mem.eql(u8, position, "afterbegin")) { - const target = parser.nodeFirstChild(self_node) orelse self_node; - for (0..len) |_| { - const child = parser.nodeListItem(children, 0) orelse continue; - _ = try parser.nodeInsertBefore(target, child, null); - } - } else if (std.mem.eql(u8, position, "beforebegin")) { - const parent = parser.nodeParentNode(self_node) orelse { - return error.NoModificationAllowed; - }; - - // Make sure parent is not Document. - // Should also check for document_fragment and document_type? - if (parser.nodeType(parent) == .document) { - return error.NoModificationAllowed; + // * `target_node` is `*Node` (where we actually insert), + // * `prev_node` is `?*Node`. + const target_node, const prev_node = blk: { + // Prefer case-sensitive match. + // "beforeend" was the most common case in my tests; we might adjust the order + // depending on which ones websites prefer most. + if (std.mem.eql(u8, position, "beforeend")) { + break :blk .{ self_node, null }; } - for (0..len) |_| { - const child = parser.nodeListItem(children, 0) orelse continue; - _ = try parser.nodeInsertBefore(parent, child, self_node); - } - } else if (std.mem.eql(u8, position, "afterend")) { - const parent = parser.nodeParentNode(self_node) orelse { - return error.NoModificationAllowed; - }; - - if (parser.nodeType(parent) == .document) { - return error.NoModificationAllowed; + if (std.mem.eql(u8, position, "afterbegin")) { + // Get the first child; null indicates there are no children. + const first_child = parser.nodeFirstChild(self_node); + break :blk .{ self_node, first_child }; } - for (0..len) |_| { - const child = parser.nodeListItem(children, 0) orelse continue; - _ = try parser.nodeInsertBefore(parent, child, null); + if (std.mem.eql(u8, position, "beforebegin")) { + // The node must have a parent node in order to use this variant. + const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed; + break :blk .{ parent, self_node }; } + + if (std.mem.eql(u8, position, "afterend")) { + // The node must have a parent node in order to use this variant. + const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed; + // Get the next sibling or null; null indicates our node is the only one. + const sibling = parser.nodeNextSibling(self_node); + break :blk .{ parent, sibling }; + } + + // Thrown if: + // * position is not one of the four listed values. + // * The input is XML that is not well-formed. + return error.Syntax; + }; + + while (parser.nodeListItem(children, 0)) |child| { + _ = try parser.nodeInsertBefore(target_node, child, prev_node); } } From b7ba993ba6182c179583eb578327b7fa99b95e10 Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 25 Sep 2025 14:42:58 +0300 Subject: [PATCH 05/10] improve `insertAdjacentHTML` test --- src/tests/dom/element.html | 39 ++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/tests/dom/element.html b/src/tests/dom/element.html index 686a654b..a9a30453 100644 --- a/src/tests/dom/element.html +++ b/src/tests/dom/element.html @@ -290,16 +290,39 @@ testing.expectEqual("stylesheet", linkElement.rel); + +
+
+ +

content

+
+
+ From a850a902cead5c7cffa1dab55f0ce34836b37cfc Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 25 Sep 2025 15:04:26 +0300 Subject: [PATCH 06/10] make sure parent is not Document in `beforebegin` and `afterend` --- src/browser/dom/element.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index 219e741c..fbd51229 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -273,12 +273,22 @@ pub const Element = struct { if (std.mem.eql(u8, position, "beforebegin")) { // The node must have a parent node in order to use this variant. const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed; + // Parent cannot be Document. + // Should have checks for document_fragment and document_type? + if (parser.nodeType(parent) == .document) { + return error.NoModificationAllowed; + } + break :blk .{ parent, self_node }; } if (std.mem.eql(u8, position, "afterend")) { // The node must have a parent node in order to use this variant. const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed; + // Parent cannot be Document. + if (parser.nodeType(parent) == .document) { + return error.NoModificationAllowed; + } // Get the next sibling or null; null indicates our node is the only one. const sibling = parser.nodeNextSibling(self_node); break :blk .{ parent, sibling }; From 2438a0e60b755ffc8340c95f0e6c8f5c58fd645c Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 25 Sep 2025 19:17:08 +0300 Subject: [PATCH 07/10] fix comment --- src/browser/dom/element.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index fbd51229..fedf0a44 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -231,7 +231,7 @@ pub const Element = struct { } } - /// Parses the given `input` string and inserts it's children to an element at given `position`. + /// Parses the given `input` string and inserts its children to an element at given `position`. /// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML /// /// TODO: Support for XML parsing and `TrustedHTML` instances. From a59c32757e6b81515295d9e2b1cb209ee5b8144a Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 25 Sep 2025 19:29:44 +0300 Subject: [PATCH 08/10] assert that nodes exist --- src/browser/dom/element.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index fedf0a44..0f6aef27 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -249,8 +249,13 @@ pub const Element = struct { // We always get it wrapped like so: // { ... } // None of the following can be null. - const html = parser.nodeFirstChild(fragment_node).?; - const body = parser.nodeLastChild(html).?; + const maybe_html = parser.nodeFirstChild(fragment_node); + std.debug.assert(maybe_html != null); + const html = maybe_html.?; + + const maybe_body = parser.nodeLastChild(html); + std.debug.assert(maybe_body != null); + const body = maybe_body.?; const children = try parser.nodeGetChildNodes(body); From 6912175e7ed8b3872179ab5fd03ccdd4acc8f4cb Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 25 Sep 2025 19:30:10 +0300 Subject: [PATCH 09/10] prefer `$` instead of `document.querySelector` --- src/tests/dom/element.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/dom/element.html b/src/tests/dom/element.html index a9a30453..4364ee87 100644 --- a/src/tests/dom/element.html +++ b/src/tests/dom/element.html @@ -300,7 +300,7 @@