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

+
+
+ + + 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

+
+
+ + + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 402409539..4eb08af70 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -360,6 +360,65 @@ 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.parse(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 target_node, const prev_node = try self.asNode().findAdjacentNodes(position); + + var iter = body.childrenIterator(); + while (iter.next()) |child_node| { + _ = try target_node.insertBefore(child_node, prev_node, page); + } +} + +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) { @@ -992,6 +1051,9 @@ 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 }); + 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();