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);
}