From 9071d98cbebfa9ff1247f698692c34fba8c79019 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 13:11:20 +0300 Subject: [PATCH 1/4] port `insertAdjacentHTML` --- src/browser/webapi/Element.zig | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 402409539..c9c9c31f2 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -360,6 +360,89 @@ pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowR return shadow_root; } +pub fn insertAdjacentHTML( + self: *Element, + position: []const u8, + /// TODO: Add support for XML parsing. + html_or_xml: []const u8, + page: *Page, +) !void { + // Create a new HTMLDocument. + const doc = try page._factory.document(@import("HTMLDocument.zig"){ + ._proto = undefined, + }); + const doc_node = doc.asNode(); + + const Parser = @import("../parser/Parser.zig"); + var parser = Parser.init(page.call_arena, doc_node, page); + parser.parseFragment(html_or_xml); + // Check if there's parsing error. + if (parser.err) |_| return error.Invalid; + + // We always get it wrapped like so: + // { ... } + // None of the following can be null. + const maybe_html_node = doc_node.firstChild(); + std.debug.assert(maybe_html_node != null); + const html_node = maybe_html_node orelse return; + + const maybe_body_node = html_node.lastChild(); + std.debug.assert(maybe_body_node != null); + const body = maybe_body_node orelse return; + + const self_node = self.asNode(); + // * `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 }; + } + + if (std.mem.eql(u8, position, "afterbegin")) { + // Get the first child; null indicates there are no children. + break :blk .{ self_node, self_node.firstChild() }; + } + + if (std.mem.eql(u8, position, "beforebegin")) { + // The node must have a parent node in order to use this variant. + const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + break :blk .{ parent_node, self_node }; + } + + if (std.mem.eql(u8, position, "afterend")) { + // The node must have a parent node in order to use this variant. + const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + // Get the next sibling or null; null indicates our node is the only one. + break :blk .{ parent_node, self_node.nextSibling() }; + } + + // Returned if: + // * position is not one of the four listed values. + // * The input is XML that is not well-formed. + return error.Syntax; + }; + + var iter = body.childrenIterator(); + while (iter.next()) |child_node| { + _ = try target_node.insertBefore(child_node, prev_node, page); + } +} + pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute { if (attr._element) |el| { if (el == self) { @@ -992,6 +1075,7 @@ pub const JsApi = struct { pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true }); pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{}); pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true }); + pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true }); const ShadowRootInit = struct { mode: []const u8, From dc040dfc3767c9c23b9ad607a62bba9379626527 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 15:37:08 +0300 Subject: [PATCH 2/4] add `insertAdjacentElement` and `insertAdjacentText` --- src/browser/webapi/Element.zig | 72 ++++++++++++---------------------- src/browser/webapi/Node.zig | 48 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index c9c9c31f2..4eb08af70 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -375,7 +375,7 @@ pub fn insertAdjacentHTML( const Parser = @import("../parser/Parser.zig"); var parser = Parser.init(page.call_arena, doc_node, page); - parser.parseFragment(html_or_xml); + parser.parse(html_or_xml); // Check if there's parsing error. if (parser.err) |_| return error.Invalid; @@ -390,52 +390,7 @@ pub fn insertAdjacentHTML( std.debug.assert(maybe_body_node != null); const body = maybe_body_node orelse return; - const self_node = self.asNode(); - // * `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 }; - } - - if (std.mem.eql(u8, position, "afterbegin")) { - // Get the first child; null indicates there are no children. - break :blk .{ self_node, self_node.firstChild() }; - } - - if (std.mem.eql(u8, position, "beforebegin")) { - // The node must have a parent node in order to use this variant. - const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; - // Parent cannot be Document. - switch (parent_node._type) { - .document, .document_fragment => return error.NoModificationAllowed, - else => {}, - } - - break :blk .{ parent_node, self_node }; - } - - if (std.mem.eql(u8, position, "afterend")) { - // The node must have a parent node in order to use this variant. - const parent_node = self_node.parentNode() orelse return error.NoModificationAllowed; - // Parent cannot be Document. - switch (parent_node._type) { - .document, .document_fragment => return error.NoModificationAllowed, - else => {}, - } - - // Get the next sibling or null; null indicates our node is the only one. - break :blk .{ parent_node, self_node.nextSibling() }; - } - - // Returned if: - // * position is not one of the four listed values. - // * The input is XML that is not well-formed. - return error.Syntax; - }; + const target_node, const prev_node = try self.asNode().findAdjacentNodes(position); var iter = body.childrenIterator(); while (iter.next()) |child_node| { @@ -443,6 +398,27 @@ pub fn insertAdjacentHTML( } } +pub fn insertAdjacentElement( + self: *Element, + position: []const u8, + element: *Element, + page: *Page, +) !void { + const target_node, const prev_node = try self.asNode().findAdjacentNodes(position); + _ = try target_node.insertBefore(element.asNode(), prev_node, page); +} + +pub fn insertAdjacentText( + self: *Element, + where: []const u8, + data: []const u8, + page: *Page, +) !void { + const text_node = try page.createTextNode(data); + const target_node, const prev_node = try self.asNode().findAdjacentNodes(where); + _ = try target_node.insertBefore(text_node, prev_node, page); +} + pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute { if (attr._element) |el| { if (el == self) { @@ -1076,6 +1052,8 @@ pub const JsApi = struct { pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{}); pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true }); pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true }); + pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true }); + pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true }); const ShadowRootInit = struct { mode: []const u8, diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index ab0c28ec8..1b686c869 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -115,6 +115,54 @@ pub fn is(self: *Node, comptime T: type) ?*T { return null; } +/// Given a position, returns target and previous nodes required for +/// insertAdjacentHTML, insertAdjacentElement and insertAdjacentText. +/// * `target_node` is `*Node` (where we actually insert), +/// * `previous_node` is `?*Node`. +pub fn findAdjacentNodes(self: *Node, position: []const u8) !struct { *Node, ?*Node } { + // 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")) { + return .{ self, null }; + } + + if (std.mem.eql(u8, position, "afterbegin")) { + // Get the first child; null indicates there are no children. + return .{ self, self.firstChild() }; + } + + if (std.mem.eql(u8, position, "beforebegin")) { + // The node must have a parent node in order to use this variant. + const parent_node = self.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + return .{ parent_node, self }; + } + + if (std.mem.eql(u8, position, "afterend")) { + // The node must have a parent node in order to use this variant. + const parent_node = self.parentNode() orelse return error.NoModificationAllowed; + // Parent cannot be Document. + switch (parent_node._type) { + .document, .document_fragment => return error.NoModificationAllowed, + else => {}, + } + + // Get the next sibling or null; null indicates our node is the only one. + return .{ parent_node, self.nextSibling() }; + } + + // Returned if: + // * position is not one of the four listed values. + // * The input is XML that is not well-formed. + return error.Syntax; +} + pub fn firstChild(self: *const Node) ?*Node { const children = self._children orelse return null; return children.first(); From 45e74d3336d89dcbebc7ce5b5db572da98a114f4 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 15:37:36 +0300 Subject: [PATCH 3/4] add `insertAdjacentElement` and `insertAdjacentHTML` tests --- .../document/insert_adjacent_element.html | 54 +++++++++++++++++++ .../tests/document/insert_adjacent_html.html | 44 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/browser/tests/document/insert_adjacent_element.html create mode 100644 src/browser/tests/document/insert_adjacent_html.html diff --git a/src/browser/tests/document/insert_adjacent_element.html b/src/browser/tests/document/insert_adjacent_element.html new file mode 100644 index 000000000..7f897cb1f --- /dev/null +++ b/src/browser/tests/document/insert_adjacent_element.html @@ -0,0 +1,54 @@ + + + Test Document Title + + + + + +
+
+ +

content

+
+
+ + + diff --git a/src/browser/tests/document/insert_adjacent_html.html b/src/browser/tests/document/insert_adjacent_html.html new file mode 100644 index 000000000..cd8d1b19d --- /dev/null +++ b/src/browser/tests/document/insert_adjacent_html.html @@ -0,0 +1,44 @@ + + + Test Document Title + + + + + +
+
+ +

content

+
+
+ + + From b6420f75e29432730768ff09124f3a9c60596df2 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 1 Dec 2025 16:03:28 +0300 Subject: [PATCH 4/4] add `insertAdjacentText` test --- .../tests/document/insert_adjacent_text.html | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/browser/tests/document/insert_adjacent_text.html diff --git a/src/browser/tests/document/insert_adjacent_text.html b/src/browser/tests/document/insert_adjacent_text.html new file mode 100644 index 000000000..c8f9f3371 --- /dev/null +++ b/src/browser/tests/document/insert_adjacent_text.html @@ -0,0 +1,49 @@ + + + Test Document Title + + + + + +
+
+ +

content

+
+
+ + +