// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const std = @import("std"); const log = @import("../../log.zig"); const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const reflect = @import("../reflect.zig"); const EventTarget = @import("EventTarget.zig"); const collections = @import("collections.zig"); pub const CData = @import("CData.zig"); pub const Element = @import("Element.zig"); pub const Document = @import("Document.zig"); pub const HTMLDocument = @import("HTMLDocument.zig"); pub const Children = @import("children.zig").Children; pub const DocumentFragment = @import("DocumentFragment.zig"); pub const DocumentType = @import("DocumentType.zig"); pub const ShadowRoot = @import("ShadowRoot.zig"); const Allocator = std.mem.Allocator; const LinkedList = std.DoublyLinkedList; const Node = @This(); _type: Type, _proto: *EventTarget, _parent: ?*Node = null, _children: ?*Children = null, _child_link: LinkedList.Node = .{}, // Lookup for nodes that have a different owner document than page.document pub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document); pub const Type = union(enum) { cdata: *CData, element: *Element, document: *Document, document_type: *DocumentType, attribute: *Element.Attribute, document_fragment: *DocumentFragment, }; pub fn asEventTarget(self: *Node) *EventTarget { return self._proto; } // Returns the node as a more specific type. Will crash if node is not a `T`. // Use `is` to optionally get the node as T pub fn as(self: *Node, comptime T: type) *T { return self.is(T).?; } // Return the node as a more specific type or `null` if the node is not a `T`. pub fn is(self: *Node, comptime T: type) ?*T { const type_name = @typeName(T); switch (self._type) { .element => |el| { if (T == Element) { return el; } if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.")) { return el.is(T); } }, .cdata => |cd| { if (T == CData) { return cd; } if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.cdata.")) { return cd.is(T); } }, .attribute => |attr| { if (T == Element.Attribute) { return attr; } }, .document => |doc| { if (T == Document) { return doc; } if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.htmldocument.")) { return doc.is(T); } }, .document_type => |dt| { if (T == DocumentType) { return dt; } }, .document_fragment => |doc| { if (T == DocumentFragment) { return doc; } if (T == ShadowRoot) { return doc.is(ShadowRoot); } }, } 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 } { // Case-insensitive match per HTML spec. // "beforeend" was the most common case in my tests; we might adjust the order // depending on which ones websites prefer most. if (std.ascii.eqlIgnoreCase(position, "beforeend")) { return .{ self, null }; } if (std.ascii.eqlIgnoreCase(position, "afterbegin")) { // Get the first child; null indicates there are no children. return .{ self, self.firstChild() }; } if (std.ascii.eqlIgnoreCase(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.ascii.eqlIgnoreCase(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(); } pub fn lastChild(self: *const Node) ?*Node { const children = self._children orelse return null; return children.last(); } pub fn nextSibling(self: *const Node) ?*Node { return linkToNodeOrNull(self._child_link.next); } pub fn previousSibling(self: *const Node) ?*Node { return linkToNodeOrNull(self._child_link.prev); } pub fn parentNode(self: *const Node) ?*Node { return self._parent; } pub fn parentElement(self: *const Node) ?*Element { const parent = self._parent orelse return null; return parent.is(Element); } // Validates that a node can be inserted as a child of parent. fn validateNodeInsertion(parent: *Node, node: *Node) !void { // Check if parent is a valid type to have children if (parent._type != .document and parent._type != .element and parent._type != .document_fragment) { return error.HierarchyError; } // Check if node contains parent (would create a cycle) if (node.contains(parent)) { return error.HierarchyError; } if (node._type == .attribute) { return error.HierarchyError; } // Doctype nodes can only be inserted into a Document if (node._type == .document_type and parent._type != .document) { return error.HierarchyError; } } pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node { if (child.is(DocumentFragment)) |_| { try page.appendAllChildren(child, self); return child; } try validateNodeInsertion(self, child); page.domChanged(); // If the child is currently connected, and if its new parent is connected, // then we can remove + add a bit more efficiently (we don't have to fully // disconnect then reconnect) const child_connected = child.isConnected(); // Check if we're adopting the node to a different document const child_owner = child.ownerDocument(page); const parent_owner = self.ownerDocument(page) orelse self.as(Document); const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner; if (child._parent) |parent| { // we can signal removeNode that the child will remain connected // (when it's appended to self) so that it can be a bit more efficient. page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() }); } // Adopt the node tree if moving between documents if (adopting_to_new_document) { try page.adoptNodeTree(child, parent_owner); } try page.appendNode(self, child, .{ .child_already_connected = child_connected, .adopting_to_new_document = adopting_to_new_document, }); return child; } pub fn childNodes(self: *Node, page: *Page) !*collections.ChildNodes { return collections.ChildNodes.init(self, page); } pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void { switch (self._type) { .element, .document_fragment => { var it = self.childrenIterator(); while (it.next()) |child| { // ignore comments and processing instructions. if (child.is(CData.Comment) != null or child.is(CData.ProcessingInstruction) != null) { continue; } try child.getTextContent(writer); } }, .cdata => |c| try writer.writeAll(c._data.str()), .document => {}, .document_type => {}, .attribute => |attr| try writer.writeAll(attr._value.str()), } } pub fn getTextContentAlloc(self: *Node, allocator: Allocator) error{WriteFailed}![:0]const u8 { var buf = std.Io.Writer.Allocating.init(allocator); try self.getTextContent(&buf.writer); try buf.writer.writeByte(0); const data = buf.written(); return data[0 .. data.len - 1 :0]; } /// Returns the "child text content" which is the concatenation of the data /// of all the Text node children of the node, in tree order. /// This differs from textContent which includes all descendant text. /// See: https://dom.spec.whatwg.org/#concept-child-text-content pub fn getChildTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void { var it = self.childrenIterator(); while (it.next()) |child| { if (child.is(CData.Text)) |text| { try writer.writeAll(text._proto._data.str()); } } } pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void { switch (self._type) { .element => |el| { if (data.len == 0) { return el.replaceChildren(&.{}, page); } return el.replaceChildren(&.{.{ .text = data }}, page); }, // Per spec, setting textContent on CharacterData runs replaceData(0, length, value) .cdata => |c| try c.replaceData(0, c.getLength(), data, page), .document => {}, .document_type => {}, .document_fragment => |frag| { if (data.len == 0) { return frag.replaceChildren(&.{}, page); } return frag.replaceChildren(&.{.{ .text = data }}, page); }, .attribute => |attr| return attr.setValue(.wrap(data), page), } } pub fn getNodeName(self: *const Node, buf: []u8) []const u8 { return switch (self._type) { .element => |el| el.getTagNameSpec(buf), .cdata => |cd| switch (cd._type) { .text => "#text", .cdata_section => "#cdata-section", .comment => "#comment", .processing_instruction => |pi| pi._target, }, .document => "#document", .document_type => |dt| dt.getName(), .document_fragment => "#document-fragment", .attribute => |attr| attr._name.str(), }; } pub fn getNodeType(self: *const Node) u8 { return switch (self._type) { .element => 1, .attribute => 2, .cdata => |cd| switch (cd._type) { .text => 3, .cdata_section => 4, .processing_instruction => 7, .comment => 8, }, .document => 9, .document_type => 10, .document_fragment => 11, }; } pub fn lookupNamespaceURI(self: *Node, prefix_arg: ?[]const u8, page: *Page) ?[]const u8 { const prefix: ?[]const u8 = if (prefix_arg) |p| (if (p.len == 0) null else p) else null; switch (self._type) { .element => |el| return el.lookupNamespaceURIForElement(prefix, page), .document => |doc| { const de = doc.getDocumentElement() orelse return null; return de.lookupNamespaceURIForElement(prefix, page); }, .document_type, .document_fragment => return null, .attribute => |attr| { const owner = attr.getOwnerElement() orelse return null; return owner.lookupNamespaceURIForElement(prefix, page); }, .cdata => { const parent = self.parentElement() orelse return null; return parent.lookupNamespaceURIForElement(prefix, page); }, } } pub fn isDefaultNamespace(self: *Node, namespace_arg: ?[]const u8, page: *Page) bool { const namespace: ?[]const u8 = if (namespace_arg) |ns| (if (ns.len == 0) null else ns) else null; const default_ns = self.lookupNamespaceURI(null, page); if (default_ns == null and namespace == null) return true; if (default_ns != null and namespace != null) return std.mem.eql(u8, default_ns.?, namespace.?); return false; } pub fn isEqualNode(self: *Node, other: *Node) bool { if (self == other) { return true; } // Make sure types match. if (self.getNodeType() != other.getNodeType()) { return false; } // TODO: Compare `localName` and prefix. return switch (self._type) { .element => self.as(Element).isEqualNode(other.as(Element)), .attribute => self.as(Element.Attribute).isEqualNode(other.as(Element.Attribute)), .cdata => self.as(CData).isEqualNode(other.as(CData)), .document_fragment => self.as(DocumentFragment).isEqualNode(other.as(DocumentFragment)), .document_type => self.as(DocumentType).isEqualNode(other.as(DocumentType)), .document => { // Document comparison is complex and rarely used in practice log.warn(.not_implemented, "Node.isEqualNode", .{ .type = "document", }); return false; }, }; } pub fn isInShadowTree(self: *Node) bool { var node = self._parent; while (node) |n| { if (n.is(ShadowRoot) != null) { return true; } node = n._parent; } return false; } pub fn isConnected(self: *const Node) bool { // Walk up to find the root node var root = self; while (root._parent) |parent| { root = parent; } switch (root._type) { .document => return true, .document_fragment => |df| { const sr = df.is(ShadowRoot) orelse return false; return sr._host.asNode().isConnected(); }, else => return false, } } const GetRootNodeOpts = struct { composed: bool = false, }; pub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node { const opts = opts_ orelse GetRootNodeOpts{}; var root = self; while (root._parent) |parent| { root = parent; } // If composed is true, traverse through shadow boundaries if (opts.composed) { while (true) { const shadow_root = @constCast(root).is(ShadowRoot) orelse break; root = shadow_root.getHost().asNode(); while (root._parent) |parent| { root = parent; } } } return root; } pub fn contains(self: *const Node, child_: ?*const Node) bool { const child = child_ orelse return false; if (self == child) { // yes, this is correct return true; } var parent = child._parent; while (parent) |p| { if (p == self) { return true; } parent = p._parent; } return false; } pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document { // A document node does not have an owner. if (self._type == .document) { return null; } // The root of the tree that a node belongs to is its owner. var current = self; while (current._parent) |parent| { current = parent; } // If the root is a document, then that's our owner. if (current._type == .document) { return current._type.document; } // Otherwise, this is a detached node. Check if it has a specific owner // document registered (for nodes created via non-main documents). if (page._node_owner_documents.get(@constCast(self))) |owner| { return owner; } // Default to the main document for detached nodes without a specific owner. return page.document; } pub fn ownerPage(self: *const Node, default: *Page) *Page { const doc = self.ownerDocument(default) orelse return default; return doc._page orelse default; } pub fn isSameDocumentAs(self: *const Node, other: *const Node, page: *const Page) bool { // Get the root document for each node const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(page); const other_doc = if (other._type == .document) other._type.document else other.ownerDocument(page); return self_doc == other_doc; } pub fn hasChildNodes(self: *const Node) bool { return self.firstChild() != null; } pub fn isSameNode(self: *const Node, other: ?*Node) bool { return self == other; } pub fn removeChild(self: *Node, child: *Node, page: *Page) !*Node { var it = self.childrenIterator(); while (it.next()) |n| { if (n == child) { page.domChanged(); page.removeNode(self, child, .{ .will_be_reconnected = false }); return child; } } return error.NotFound; } pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page) !*Node { const ref_node = ref_node_ orelse { return self.appendChild(new_node, page); }; // special case: if nodes are the same, ignore the change. if (new_node == ref_node_) { page.domChanged(); if (page.hasMutationObservers()) { const parent = new_node._parent.?; const previous_sibling = new_node.previousSibling(); const next_sibling = new_node.nextSibling(); const replaced = [_]*Node{new_node}; page.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling); } return new_node; } if (ref_node._parent == null or ref_node._parent.? != self) { return error.NotFound; } if (new_node.is(DocumentFragment)) |_| { try page.insertAllChildrenBefore(new_node, self, ref_node); return new_node; } try validateNodeInsertion(self, new_node); const child_already_connected = new_node.isConnected(); // Check if we're adopting the node to a different document const child_owner = new_node.ownerDocument(page); const parent_owner = self.ownerDocument(page) orelse self.as(Document); const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner; page.domChanged(); const will_be_reconnected = self.isConnected(); if (new_node._parent) |parent| { page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected }); } // Adopt the node tree if moving between documents if (adopting_to_new_document) { try page.adoptNodeTree(new_node, parent_owner); } try page.insertNodeRelative( self, new_node, .{ .before = ref_node }, .{ .child_already_connected = child_already_connected, .adopting_to_new_document = adopting_to_new_document, }, ); return new_node; } pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page) !*Node { if (old_child._parent == null or old_child._parent.? != self) { return error.HierarchyError; } try validateNodeInsertion(self, new_child); _ = try self.insertBefore(new_child, old_child, page); // Special case: if we replace a node by itself, we don't remove it. // insertBefore is an noop in this case. if (new_child != old_child) { page.removeNode(self, old_child, .{ .will_be_reconnected = false }); } return old_child; } pub fn getNodeValue(self: *const Node) ?String { return switch (self._type) { .cdata => |c| c.getData(), .attribute => |attr| attr._value, .element => null, .document => null, .document_type => null, .document_fragment => null, }; } pub fn setNodeValue(self: *const Node, value: ?String, page: *Page) !void { switch (self._type) { // Per spec, setting nodeValue on CharacterData runs replaceData(0, length, value) .cdata => |c| { const new_value: []const u8 = if (value) |v| v.str() else ""; try c.replaceData(0, c.getLength(), new_value, page); }, .attribute => |attr| try attr.setValue(value, page), .element => {}, .document => {}, .document_type => {}, .document_fragment => {}, } } pub fn format(self: *Node, writer: *std.Io.Writer) !void { // // If you need extra debugging: // return @import("../dump.zig").deep(self, .{}, writer); return switch (self._type) { .cdata => |cd| cd.format(writer), .element => |el| writer.print("{f}", .{el}), .document => writer.writeAll(""), .document_type => writer.writeAll(""), .document_fragment => writer.writeAll(""), .attribute => |attr| writer.print("{f}", .{attr}), }; } // Returns an iterator the can be used to iterate through the node's children // For internal use. pub fn childrenIterator(self: *Node) NodeIterator { const children = self._children orelse { return .{ .node = null }; }; return .{ .node = children.first(), }; } pub fn getChildrenCount(self: *Node) usize { return switch (self._type) { .element, .document, .document_fragment => self.getLength(), .document_type, .attribute, .cdata => return 0, }; } pub fn getLength(self: *Node) u32 { switch (self._type) { .cdata => |cdata| { return @intCast(cdata.getData().len); }, .element, .document, .document_fragment => { var count: u32 = 0; var it = self.childrenIterator(); while (it.next()) |_| { count += 1; } return count; }, .document_type, .attribute => return 0, } } pub fn getChildIndex(self: *Node, target: *const Node) ?u32 { var i: u32 = 0; var it = self.childrenIterator(); while (it.next()) |child| { if (child == target) { return i; } i += 1; } return null; } pub fn getChildAt(self: *Node, index: u32) ?*Node { var i: u32 = 0; var it = self.childrenIterator(); while (it.next()) |child| { if (i == index) { return child; } i += 1; } return null; } pub fn getData(self: *const Node) String { return switch (self._type) { .cdata => |c| c.getData(), else => .empty, }; } pub fn setData(self: *Node, data: []const u8, page: *Page) !void { switch (self._type) { .cdata => |c| try c.setData(data, page), else => {}, } } pub fn normalize(self: *Node, page: *Page) !void { var buffer: std.ArrayList(u8) = .empty; return self._normalize(page.call_arena, &buffer, page); } const CloneError = error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError, CloneError, IFrameLoadError, TooManyContexts, LinkLoadError, StyleLoadError, TypeError, CompilationError, JsException, }; pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node { const deep = deep_ orelse false; switch (self._type) { .cdata => |cd| { const data = cd.getData().str(); return switch (cd._type) { .text => page.createTextNode(data), .cdata_section => page.createCDATASection(data), .comment => page.createComment(data), .processing_instruction => |pi| page.createProcessingInstruction(pi._target, data), }; }, .element => |el| return el.clone(deep, page), .document => return error.NotSupported, .document_type => |dt| { const cloned = dt.clone(page) catch return error.CloneError; return cloned.asNode(); }, .document_fragment => |frag| return frag.cloneFragment(deep, page), .attribute => |attr| { const cloned = attr.clone(page) catch return error.CloneError; return cloned._proto; }, } } /// Clone a node for the purpose of appending to a parent. /// Returns null if the cloned node was already attached somewhere by a custom element /// constructor, indicating that the constructor's decision should be respected. /// /// This helper is used when iterating over children to clone them. The typical pattern is: /// while (child_it.next()) |child| { /// if (try child.cloneNodeForAppending(true, page)) |cloned| { /// try page.appendNode(parent, cloned, opts); /// } /// } /// /// The only case where a cloned node would already have a parent is when a custom element /// constructor (which runs during cloning per the HTML spec) explicitly attaches the element /// somewhere. In that case, we respect the constructor's decision and return null to signal /// that the cloned node should not be appended to our intended parent. pub fn cloneNodeForAppending(self: *Node, deep: bool, page: *Page) CloneError!?*Node { const cloned = try self.cloneNode(deep, page); if (cloned._parent != null) { return null; } return cloned; } pub fn compareDocumentPosition(self: *Node, other: *Node) u16 { const DISCONNECTED: u16 = 0x01; const PRECEDING: u16 = 0x02; const FOLLOWING: u16 = 0x04; const CONTAINS: u16 = 0x08; const CONTAINED_BY: u16 = 0x10; const IMPLEMENTATION_SPECIFIC: u16 = 0x20; if (self == other) { return 0; } // Check if either node is disconnected const self_root = self.getRootNode(.{}); const other_root = other.getRootNode(.{}); if (self_root != other_root) { // Nodes are in different trees - disconnected // Use pointer comparison for implementation-specific ordering return DISCONNECTED | IMPLEMENTATION_SPECIFIC | if (@intFromPtr(self) < @intFromPtr(other)) FOLLOWING else PRECEDING; } // Check if one contains the other if (self.contains(other)) { return FOLLOWING | CONTAINED_BY; } if (other.contains(self)) { return PRECEDING | CONTAINS; } // Neither contains the other - find common ancestor and compare positions // Walk up from self to build ancestor chain var self_ancestors: [256]*const Node = undefined; var ancestor_count: usize = 0; var current: ?*const Node = self; while (current) |node| : (current = node._parent) { if (ancestor_count >= self_ancestors.len) break; self_ancestors[ancestor_count] = node; ancestor_count += 1; } const ancestors = self_ancestors[0..ancestor_count]; // Walk up from other until we find common ancestor current = other; while (current) |node| : (current = node._parent) { // Check if this node is in self's ancestor chain for (ancestors, 0..) |ancestor, i| { if (ancestor != node) { continue; } // Found common ancestor // Compare the children that are ancestors of self and other if (i == 0) { // self is directly under the common ancestor // Find other's ancestor that's a child of the common ancestor if (other == node) { // other is the common ancestor, so self follows it return FOLLOWING; } var other_ancestor = other; while (other_ancestor._parent) |p| { if (p == node) break; other_ancestor = p; } return if (isNodeBefore(self, other_ancestor)) FOLLOWING else PRECEDING; } const self_ancestor = self_ancestors[i - 1]; // Find other's ancestor that's a child of the common ancestor var other_ancestor = other; if (other == node) { // other is the common ancestor, so self is contained by it return PRECEDING | CONTAINS; } while (other_ancestor._parent) |p| { if (p == node) break; other_ancestor = p; } return if (isNodeBefore(self_ancestor, other_ancestor)) FOLLOWING else PRECEDING; } } // Shouldn't reach here if both nodes are in the same tree return DISCONNECTED; } // faster to compare the linked list node links directly fn isNodeBefore(node1: *const Node, node2: *const Node) bool { var current = node1._child_link.next; const target = &node2._child_link; while (current) |link| { if (link == target) return true; current = link.next; } return false; } fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), page: *Page) !void { var it = self.childrenIterator(); while (it.next()) |child| { try child._normalize(allocator, buffer, page); } var child = self.firstChild(); while (child) |current_node| { var next_node = current_node.nextSibling(); const text_node = current_node.is(CData.Text) orelse { child = next_node; continue; }; if (text_node._proto.getData().len == 0) { page.removeNode(self, current_node, .{ .will_be_reconnected = false }); child = next_node; continue; } if (next_node) |next| { if (next.is(CData.Text)) |_| { try buffer.appendSlice(allocator, text_node.getWholeText()); while (next_node) |node_to_merge| { const next_text_node = node_to_merge.is(CData.Text) orelse break; try buffer.appendSlice(allocator, next_text_node.getWholeText()); const to_remove = node_to_merge; next_node = node_to_merge.nextSibling(); page.removeNode(self, to_remove, .{ .will_be_reconnected = false }); } text_node._proto._data = try page.dupeSSO(buffer.items); buffer.clearRetainingCapacity(); } } child = next_node; } } pub const GetElementsByTagNameResult = union(enum) { tag: collections.NodeLive(.tag), tag_name: collections.NodeLive(.tag_name), all_elements: collections.NodeLive(.all_elements), }; // Not exposed in the WebAPI, but used by both Element and Document pub fn getElementsByTagName(self: *Node, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult { if (tag_name.len > 256) { // 256 seems generous. return error.InvalidTagName; } if (std.mem.eql(u8, tag_name, "*")) { return .{ .all_elements = collections.NodeLive(.all_elements).init(self, {}, page), }; } const lower = std.ascii.lowerString(&page.buf, tag_name); if (Node.Element.Tag.parseForMatch(lower)) |known| { // optimized for known tag names, comparis return .{ .tag = collections.NodeLive(.tag).init(self, known, page), }; } const arena = page.arena; const filter = try String.init(arena, lower, .{}); return .{ .tag_name = collections.NodeLive(.tag_name).init(self, filter, page) }; } // Not exposed in the WebAPI, but used by both Element and Document pub fn getElementsByTagNameNS(self: *Node, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) { if (local_name.len > 256) { return error.InvalidTagName; } // Parse namespace - "*" means wildcard (null), null means Element.Namespace.null const ns: ?Element.Namespace = if (namespace) |ns_str| if (std.mem.eql(u8, ns_str, "*")) null else Element.Namespace.parse(ns_str) else Element.Namespace.null; return collections.NodeLive(.tag_name_ns).init(self, .{ .namespace = ns, .local_name = try String.init(page.arena, local_name, .{}), }, page); } // Not exposed in the WebAPI, but used by both Element and Document pub fn getElementsByClassName(self: *Node, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) { const arena = page.arena; // Parse space-separated class names var class_names: std.ArrayList([]const u8) = .empty; var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r "); while (it.next()) |name| { try class_names.append(arena, try page.dupeString(name)); } return collections.NodeLive(.class_name).init(self, class_names.items, page); } // Writes a JSON representation of the node and its children pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void { // stupid json api requires this to be const, // so we @constCast it because our stringify re-uses code that can be // used to iterate nodes, e.g. the NodeIterator return @import("../dump.zig").toJSON(@constCast(self), writer); } const NodeIterator = struct { node: ?*Node, pub fn next(self: *NodeIterator) ?*Node { const node = self.node orelse return null; self.node = linkToNodeOrNull(node._child_link.next); return node; } }; // Turns a linked list node into a Node pub fn linkToNode(n: *LinkedList.Node) *Node { return @fieldParentPtr("_child_link", n); } pub fn linkToNodeOrNull(n_: ?*LinkedList.Node) ?*Node { return if (n_) |n| linkToNode(n) else null; } pub const JsApi = struct { pub const bridge = js.Bridge(Node); pub const Meta = struct { pub const name = "Node"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; pub const enumerable = false; }; pub const ELEMENT_NODE = bridge.property(1, .{ .template = true }); pub const ATTRIBUTE_NODE = bridge.property(2, .{ .template = true }); pub const TEXT_NODE = bridge.property(3, .{ .template = true }); pub const CDATA_SECTION_NODE = bridge.property(4, .{ .template = true }); pub const ENTITY_REFERENCE_NODE = bridge.property(5, .{ .template = true }); pub const ENTITY_NODE = bridge.property(6, .{ .template = true }); pub const PROCESSING_INSTRUCTION_NODE = bridge.property(7, .{ .template = true }); pub const COMMENT_NODE = bridge.property(8, .{ .template = true }); pub const DOCUMENT_NODE = bridge.property(9, .{ .template = true }); pub const DOCUMENT_TYPE_NODE = bridge.property(10, .{ .template = true }); pub const DOCUMENT_FRAGMENT_NODE = bridge.property(11, .{ .template = true }); pub const NOTATION_NODE = bridge.property(12, .{ .template = true }); pub const DOCUMENT_POSITION_DISCONNECTED = bridge.property(0x01, .{ .template = true }); pub const DOCUMENT_POSITION_PRECEDING = bridge.property(0x02, .{ .template = true }); pub const DOCUMENT_POSITION_FOLLOWING = bridge.property(0x04, .{ .template = true }); pub const DOCUMENT_POSITION_CONTAINS = bridge.property(0x08, .{ .template = true }); pub const DOCUMENT_POSITION_CONTAINED_BY = bridge.property(0x10, .{ .template = true }); pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20, .{ .template = true }); pub const nodeName = bridge.accessor(struct { fn wrap(self: *const Node, page: *Page) []const u8 { return self.getNodeName(&page.buf); } }.wrap, null, .{}); pub const nodeType = bridge.accessor(Node.getNodeType, null, .{}); pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{}); fn _textContext(self: *Node, page: *const Page) !?[]const u8 { // cdata and attributes can return value directly, avoiding the copy switch (self._type) { .element, .document_fragment => { var buf = std.Io.Writer.Allocating.init(page.call_arena); try self.getTextContent(&buf.writer); return buf.written(); }, .cdata => |cdata| return cdata._data.str(), .attribute => |attr| return attr._value.str(), .document => return null, .document_type => return null, } } pub const firstChild = bridge.accessor(Node.firstChild, null, .{}); pub const lastChild = bridge.accessor(Node.lastChild, null, .{}); pub const nextSibling = bridge.accessor(Node.nextSibling, null, .{}); pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{}); pub const parentNode = bridge.accessor(Node.parentNode, null, .{}); pub const parentElement = bridge.accessor(Node.parentElement, null, .{}); pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true }); pub const childNodes = bridge.accessor(Node.childNodes, null, .{ .cache = .{ .private = "child_nodes" } }); pub const isConnected = bridge.accessor(Node.isConnected, null, .{}); pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{}); pub const hasChildNodes = bridge.function(Node.hasChildNodes, .{}); pub const isSameNode = bridge.function(Node.isSameNode, .{}); pub const contains = bridge.function(Node.contains, .{}); pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true }); pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{}); pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true }); pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true }); pub const normalize = bridge.function(Node.normalize, .{}); pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true }); pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{}); pub const getRootNode = bridge.function(Node.getRootNode, .{}); pub const isEqualNode = bridge.function(Node.isEqualNode, .{}); pub const lookupNamespaceURI = bridge.function(Node.lookupNamespaceURI, .{}); pub const isDefaultNamespace = bridge.function(Node.isDefaultNamespace, .{}); fn _baseURI(_: *Node, page: *const Page) []const u8 { return page.base(); } pub const baseURI = bridge.accessor(_baseURI, null, .{}); }; pub const Build = struct { // Calls `func_name` with `args` on the most specific type where it is // implement. This could be on the Node itself (as a last-resort); pub fn call(self: *const Node, comptime func_name: []const u8, args: anytype) !void { inline for (@typeInfo(Node.Type).@"union".fields) |f| { // The inner type has its own "call" method. Defer to it. if (@field(Node.Type, f.name) == self._type) { const S = reflect.Struct(f.type); if (@hasDecl(S, "Build")) { if (@hasDecl(S.Build, "call")) { const sub = @field(self._type, f.name); if (try S.Build.call(sub, func_name, args)) { return; } } // The inner type implements this function. Call it and we're done. if (@hasDecl(S, func_name)) { return @call(.auto, @field(f.type, func_name), args); } } } } if (@hasDecl(Node.Build, func_name)) { // Our last resort - the node implements this function. return @call(.auto, @field(Node.Build, func_name), args); } } }; pub const NodeOrText = union(enum) { node: *Node, text: []const u8, pub fn format(self: *const NodeOrText, writer: *std.io.Writer) !void { switch (self.*) { .node => |n| try n.format(writer), .text => |text| { try writer.writeByte('\''); try writer.writeAll(text); try writer.writeByte('\''); }, } } pub fn toNode(self: *const NodeOrText, page: *Page) !*Node { return switch (self.*) { .node => |n| n, .text => |txt| page.createTextNode(txt), }; } /// DOM spec: first following sibling of `node` that is not in `nodes`. pub fn viableNextSibling(node: *Node, nodes: []const NodeOrText) ?*Node { var sibling = node.nextSibling() orelse return null; blk: while (true) { for (nodes) |n| { switch (n) { .node => |nn| if (sibling == nn) { sibling = sibling.nextSibling() orelse return null; continue :blk; }, .text => {}, } } else { return sibling; } } return null; } }; const testing = @import("../../testing.zig"); test "WebApi: Node" { try testing.htmlRunner("node", .{}); }