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
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
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();