Skip to content

Commit f236a65

Browse files
authored
Merge pull request #1092 from lightpanda-io/nikneym/insert-adjacent-html
Support `Element#insertAdjacentHTML`
2 parents 92226a8 + f7b08a1 commit f236a65

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

src/browser/dom/element.zig

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,85 @@ pub const Element = struct {
231231
}
232232
}
233233

234+
/// Parses the given `input` string and inserts its children to an element at given `position`.
235+
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
236+
///
237+
/// TODO: Support for XML parsing and `TrustedHTML` instances.
238+
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
239+
const self_node = parser.elementToNode(self);
240+
const doc = parser.nodeOwnerDocument(self_node) orelse {
241+
return parser.DOMError.WrongDocument;
242+
};
243+
244+
// Parse the fragment.
245+
// Should return error.Syntax on fail?
246+
const fragment = try parser.documentParseFragmentFromStr(doc, input);
247+
const fragment_node = parser.documentFragmentToNode(fragment);
248+
249+
// We always get it wrapped like so:
250+
// <html><head></head><body>{ ... }</body></html>
251+
// None of the following can be null.
252+
const maybe_html = parser.nodeFirstChild(fragment_node);
253+
std.debug.assert(maybe_html != null);
254+
const html = maybe_html orelse return;
255+
256+
const maybe_body = parser.nodeLastChild(html);
257+
std.debug.assert(maybe_body != null);
258+
const body = maybe_body orelse return;
259+
260+
const children = try parser.nodeGetChildNodes(body);
261+
262+
// * `target_node` is `*Node` (where we actually insert),
263+
// * `prev_node` is `?*Node`.
264+
const target_node, const prev_node = blk: {
265+
// Prefer case-sensitive match.
266+
// "beforeend" was the most common case in my tests; we might adjust the order
267+
// depending on which ones websites prefer most.
268+
if (std.mem.eql(u8, position, "beforeend")) {
269+
break :blk .{ self_node, null };
270+
}
271+
272+
if (std.mem.eql(u8, position, "afterbegin")) {
273+
// Get the first child; null indicates there are no children.
274+
const first_child = parser.nodeFirstChild(self_node);
275+
break :blk .{ self_node, first_child };
276+
}
277+
278+
if (std.mem.eql(u8, position, "beforebegin")) {
279+
// The node must have a parent node in order to use this variant.
280+
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
281+
// Parent cannot be Document.
282+
// Should have checks for document_fragment and document_type?
283+
if (parser.nodeType(parent) == .document) {
284+
return error.NoModificationAllowed;
285+
}
286+
287+
break :blk .{ parent, self_node };
288+
}
289+
290+
if (std.mem.eql(u8, position, "afterend")) {
291+
// The node must have a parent node in order to use this variant.
292+
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
293+
// Parent cannot be Document.
294+
if (parser.nodeType(parent) == .document) {
295+
return error.NoModificationAllowed;
296+
}
297+
// Get the next sibling or null; null indicates our node is the only one.
298+
const sibling = parser.nodeNextSibling(self_node);
299+
break :blk .{ parent, sibling };
300+
}
301+
302+
// Thrown if:
303+
// * position is not one of the four listed values.
304+
// * The input is XML that is not well-formed.
305+
return error.Syntax;
306+
};
307+
308+
while (parser.nodeListItem(children, 0)) |child| {
309+
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
310+
}
311+
}
312+
234313
// 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.
235314
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
236315
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {

src/browser/netsurf.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1363,7 +1363,7 @@ pub fn nodeHasChildNodes(node: *Node) bool {
13631363
return res;
13641364
}
13651365

1366-
pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: *Node) !*Node {
1366+
pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: ?*Node) !*Node {
13671367
var res: ?*Node = null;
13681368
const err = nodeVtable(node).dom_node_insert_before.?(node, new_node, ref_node, &res);
13691369
try DOMErr(err);

src/tests/dom/element.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,40 @@
289289
linkElement.rel = "stylesheet";
290290
testing.expectEqual("stylesheet", linkElement.rel);
291291
</script>
292+
293+
<!-- This structure will get mutated by insertAdjacentHTML test -->
294+
<div id="insert-adjacent-html-outer-wrapper">
295+
<div id="insert-adjacent-html-inner-wrapper">
296+
<span></span>
297+
<p>content</p>
298+
</div>
299+
</div>
300+
301+
<script id=insertAdjacentHTML>
302+
// Insert "beforeend".
303+
const wrapper = $("#insert-adjacent-html-inner-wrapper");
304+
wrapper.insertAdjacentHTML("beforeend", "<h1>title</h1>");
305+
let newElement = wrapper.lastElementChild;
306+
testing.expectEqual("H1", newElement.tagName);
307+
testing.expectEqual("title", newElement.innerText);
308+
309+
// Insert "beforebegin".
310+
wrapper.insertAdjacentHTML("beforebegin", "<h2>small title</h2>");
311+
newElement = wrapper.previousElementSibling;
312+
testing.expectEqual("H2", newElement.tagName);
313+
testing.expectEqual("small title", newElement.innerText);
314+
315+
// Insert "afterend".
316+
wrapper.insertAdjacentHTML("afterend", "<div id=\"afterend\">after end</div>");
317+
newElement = wrapper.nextElementSibling;
318+
testing.expectEqual("DIV", newElement.tagName);
319+
testing.expectEqual("after end", newElement.innerText);
320+
testing.expectEqual("afterend", newElement.id);
321+
322+
// Insert "afterbegin".
323+
wrapper.insertAdjacentHTML("afterbegin", "<div class=\"afterbegin\">after begin</div><yy></yy>");
324+
newElement = wrapper.firstElementChild;
325+
testing.expectEqual("DIV", newElement.tagName);
326+
testing.expectEqual("after begin", newElement.innerText);
327+
testing.expectEqual("afterbegin", newElement.className);
328+
</script>

0 commit comments

Comments
 (0)