Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/browser/tests/document/insert_adjacent_element.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<head id="the_head">
<title>Test Document Title</title>
<script src="../testing.js"></script>
</head>

<body>
<!-- This structure will get mutated by insertAdjacentElement test -->
<div id="insert-adjacent-element-outer-wrapper">
<div id="insert-adjacent-element-inner-wrapper">
<span></span>
<p>content</p>
</div>
</div>
</body>

<script id=insertAdjacentElement>
// Insert "beforeend".
const wrapper = $("#insert-adjacent-element-inner-wrapper");
const h1 = document.createElement("h1");
h1.innerHTML = "title";
wrapper.insertAdjacentElement("beforeend", h1);
let newElement = wrapper.lastElementChild;
testing.expectEqual("H1", newElement.tagName);
testing.expectEqual("title", newElement.innerText);

// Insert "beforebegin".
const h2 = document.createElement("h2");
h2.innerHTML = "small title";
wrapper.insertAdjacentElement("beforebegin", h2);
newElement = wrapper.previousElementSibling;
testing.expectEqual("H2", newElement.tagName);
testing.expectEqual("small title", newElement.innerText);

// Insert "afterend".
const divAfterEnd = document.createElement("div");
divAfterEnd.id = "afterend";
divAfterEnd.innerHTML = "after end";
wrapper.insertAdjacentElement("afterend", divAfterEnd);
newElement = wrapper.nextElementSibling;
testing.expectEqual("DIV", newElement.tagName);
testing.expectEqual("after end", newElement.innerText);
testing.expectEqual("afterend", newElement.id);

// Insert "afterbegin".
const divAfterBegin = document.createElement("div");
divAfterBegin.className = "afterbegin";
divAfterBegin.innerHTML = "after begin";
wrapper.insertAdjacentElement("afterbegin", divAfterBegin);
newElement = wrapper.firstElementChild;
testing.expectEqual("DIV", newElement.tagName);
testing.expectEqual("after begin", newElement.innerText);
testing.expectEqual("afterbegin", newElement.className);
</script>
44 changes: 44 additions & 0 deletions src/browser/tests/document/insert_adjacent_html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<head id="the_head">
<title>Test Document Title</title>
<script src="../testing.js"></script>
</head>

<body>
<!-- This structure will get mutated by insertAdjacentHTML test -->
<div id="insert-adjacent-html-outer-wrapper">
<div id="insert-adjacent-html-inner-wrapper">
<span></span>
<p>content</p>
</div>
</div>
</body>

<script id=insertAdjacentHTML>
// Insert "beforeend".
const wrapper = $("#insert-adjacent-html-inner-wrapper");
wrapper.insertAdjacentHTML("beforeend", "<h1>title</h1>");
let newElement = wrapper.lastElementChild;
testing.expectEqual("H1", newElement.tagName);
testing.expectEqual("title", newElement.innerText);

// Insert "beforebegin".
wrapper.insertAdjacentHTML("beforebegin", "<h2>small title</h2>");
newElement = wrapper.previousElementSibling;
testing.expectEqual("H2", newElement.tagName);
testing.expectEqual("small title", newElement.innerText);

// Insert "afterend".
wrapper.insertAdjacentHTML("afterend", "<div id=\"afterend\">after end</div>");
newElement = wrapper.nextElementSibling;
testing.expectEqual("DIV", newElement.tagName);
testing.expectEqual("after end", newElement.innerText);
testing.expectEqual("afterend", newElement.id);

// Insert "afterbegin".
wrapper.insertAdjacentHTML("afterbegin", "<div class=\"afterbegin\">after begin</div><yy></yy>");
newElement = wrapper.firstElementChild;
testing.expectEqual("DIV", newElement.tagName);
testing.expectEqual("after begin", newElement.innerText);
testing.expectEqual("afterbegin", newElement.className);
</script>
49 changes: 49 additions & 0 deletions src/browser/tests/document/insert_adjacent_text.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<head id="the_head">
<title>Test Document Title</title>
<script src="../testing.js"></script>
</head>

<body>
<!-- This structure will get mutated by insertAdjacentText test -->
<div id="insert-adjacent-text-outer-wrapper">
<div id="insert-adjacent-text-inner-wrapper">
<span></span>
<p>content</p>
</div>
</div>
</body>

<script id=insertAdjacentText>
const wrapper = $("#insert-adjacent-text-inner-wrapper");

// Insert "beforeend".
{
wrapper.insertAdjacentText("beforeend", "atlas, rise!");
const { nodeType, data } = wrapper.lastChild;
testing.expectEqual(nodeType, Node.TEXT_NODE);
testing.expectEqual(data, "atlas, rise!");
}

// Insert "beforebegin".
{
wrapper.insertAdjacentText("beforebegin", "before everything else");
const { nodeType, data } = wrapper.previousSibling;
testing.expectEqual(nodeType, Node.TEXT_NODE);
testing.expectEqual(data, "before everything else");
}

// Insert "afterend".
{
wrapper.insertAdjacentText("afterend", "after end");
const { nodeType, data } = wrapper.nextSibling;
testing.expectEqual(nodeType, Node.TEXT_NODE);
testing.expectEqual(data, "after end");
}

// Insert "afterbegin".
wrapper.insertAdjacentText("afterbegin", "after begin");
const { nodeType, data } = wrapper.firstChild;
testing.expectEqual(nodeType, Node.TEXT_NODE);
testing.expectEqual(data, "after begin");
</script>
62 changes: 62 additions & 0 deletions src/browser/webapi/Element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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:
// <html><head></head><body>{ ... }</body></html>
// 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably some optimization opportunity by using page.insertNodeRelative directly. You'd be able to call page.domChanged(); just once (before the loop), and you could skip the call to page.removeNode (which checks mutationobservers), since you know the children are all newly created (would need to manually set their _parent = null before calling). There might be other small tweaks. Not sure if this API is called enough to warrant it though.

}
}

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) {
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions src/browser/webapi/Node.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading