diff --git a/src/browser/tests/node/is_equal_node.html b/src/browser/tests/node/is_equal_node.html new file mode 100644 index 000000000..ef192fae2 --- /dev/null +++ b/src/browser/tests/node/is_equal_node.html @@ -0,0 +1,40 @@ + + + +
+ we're no strangers to love + you know the rules + + and so do I +
+ +
+ we're no strangers to love + you know the rules + + and so do I +
+ + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index cb17aaad5..82afee542 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -146,6 +146,10 @@ pub fn getLength(self: *const CData) usize { return self._data.len; } +pub fn isEqualNode(self: *const CData, other: *const CData) bool { + return std.mem.eql(u8, self.getData(), other.getData()); +} + pub fn appendData(self: *CData, data: []const u8, page: *Page) !void { const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data }); try self.setData(new_data, page); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 00b46cdf7..382bc41f5 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -117,6 +117,49 @@ pub fn className(self: *const Element) []const u8 { }; } +pub fn attributesEql(self: *const Element, other: *Element) bool { + if (self._attributes) |attr_list| { + const other_list = other._attributes orelse return false; + return attr_list.eql(other_list); + } + // Make sure no attrs in both sides. + return other._attributes == null; +} + +/// TODO: localName and prefix comparison. +pub fn isEqualNode(self: *Element, other: *Element) bool { + const self_tag = self.getTagNameDump(); + const other_tag = other.getTagNameDump(); + // Compare namespaces and tags. + const dirty = self._namespace != other._namespace or !std.mem.eql(u8, self_tag, other_tag); + if (dirty) { + return false; + } + + // Compare attributes. + if (!self.attributesEql(other)) { + return false; + } + + // Compare children. + var self_iter = self.asNode().childrenIterator(); + var other_iter = other.asNode().childrenIterator(); + var self_count: usize = 0; + var other_count: usize = 0; + while (self_iter.next()) |self_node| : (self_count += 1) { + const other_node = other_iter.next() orelse return false; + other_count += 1; + if (self_node.isEqualNode(other_node)) { + continue; + } + + return false; + } + + // Make sure both have equal number of children. + return self_count == other_count; +} + pub fn getTagNameLower(self: *const Element) []const u8 { switch (self._type) { .html => |he| switch (he._type) { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index f203c7ae5..564bf5990 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -286,6 +286,27 @@ pub fn getNodeType(self: *const Node) u8 { }; } +pub fn isEqualNode(self: *Node, other: *Node) bool { + // 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)), + else => { + log.warn(.browser, "not implemented", .{ + .type = self._type, + .feature = "Node.isEqualNode", + }); + return false; + }, + }; +} + pub fn isInShadowTree(self: *Node) bool { var node = self._parent; while (node) |n| { @@ -822,6 +843,7 @@ pub const JsApi = struct { 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 toString = bridge.function(_toString, .{}); fn _toString(self: *const Node) []const u8 { diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index d7fa36e6e..693b3842f 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -78,6 +78,10 @@ pub fn getOwnerElement(self: *const Attribute) ?*Element { return self._element; } +pub fn isEqualNode(self: *const Attribute, other: *const Attribute) bool { + return std.mem.eql(u8, self.getName(), other.getName()) and std.mem.eql(u8, self.getValue(), other.getValue()); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Attribute); @@ -119,16 +123,45 @@ pub const JsApi = struct { // attribute in the DOM, and, again, we expect that to almost always be null. pub const List = struct { normalize: bool, + /// Length of items in `_list`. Not usize to increase memory usage. + /// Honestly, this is more than enough. + _len: u32 = 0, _list: std.DoublyLinkedList = .{}, pub fn isEmpty(self: *const List) bool { return self._list.first == null; } + pub fn get(self: *const List, name: []const u8, page: *Page) !?[]const u8 { const entry = (try self.getEntry(name, page)) orelse return null; return entry._value.str(); } + pub inline fn length(self: *const List) usize { + return self._len; + } + + /// Compares 2 attribute lists for equality. + pub fn eql(self: *List, other: *List) bool { + if (self.length() != other.length()) { + return false; + } + + var iter = self.iterator(); + search: while (iter.next()) |attr| { + // Iterate over all `other` attributes. + var other_iter = other.iterator(); + while (other_iter.next()) |other_attr| { + if (attr.eql(other_attr)) { + continue :search; // Found match. + } + } + // Iterated over all `other` and not match. + return false; + } + return true; + } + // meant for internal usage, where the name is known to be properly cased pub fn getSafe(self: *const List, name: []const u8) ?[]const u8 { const entry = self.getEntryWithNormalizedName(name) orelse return null; @@ -180,6 +213,7 @@ pub const List = struct { ._value = try String.init(page.arena, value, .{}), }); self._list.append(&entry._node); + self._len += 1; } if (is_id) { @@ -203,6 +237,7 @@ pub const List = struct { ._value = try String.init(page.arena, value, .{}), }); self._list.append(&entry._node); + self._len += 1; } // not efficient, won't be called often (if ever!) @@ -235,6 +270,7 @@ pub const List = struct { ._value = try String.init(page.arena, value, .{}), }); self._list.append(&entry._node); + self._len += 1; } pub fn delete(self: *List, name: []const u8, element: *Element, page: *Page) !void { @@ -252,6 +288,7 @@ pub const List = struct { page.attributeRemove(element, result.normalized, old_value); _ = page._attribute_lookup.remove(@intFromPtr(entry)); self._list.remove(&entry._node); + self._len -= 1; page._factory.destroy(entry); } @@ -311,6 +348,12 @@ pub const List = struct { return @alignCast(@fieldParentPtr("_node", n)); } + /// Returns true if 2 entries are equal. + /// This doesn't compare `_node` fields. + pub fn eql(self: *const Entry, other: *const Entry) bool { + return self._name.eql(other._name) and self._value.eql(other._value); + } + pub fn format(self: *const Entry, writer: *std.Io.Writer) !void { return formatAttribute(self._name.str(), self._value.str(), writer); }