diff --git a/src/browser/tests/element/get_elements_by_tag_name_ns.html b/src/browser/tests/element/get_elements_by_tag_name_ns.html new file mode 100644 index 00000000..74c140b2 --- /dev/null +++ b/src/browser/tests/element/get_elements_by_tag_name_ns.html @@ -0,0 +1,123 @@ + + + +
+
div1
+

p1

+
div2
+
+ + + + + + + +
+
HTML div
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 013bc015..08b35ae6 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -249,6 +249,23 @@ pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) }; } +pub fn getElementsByTagNameNS(self: *Document, 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.asNode(), .{ + .namespace = ns, + .local_name = try String.init(page.arena, local_name, .{}), + }, page); +} + pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) { const arena = page.arena; @@ -914,7 +931,8 @@ fn validateElementName(name: []const u8) !void { const is_valid = (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or - c == '_' or c == '-' or c == '.' or c == ':'; + c == '_' or c == '-' or c == '.' or c == ':' or + c >= 128; // Allow non-ASCII UTF-8 if (!is_valid) { return error.InvalidCharacterError; @@ -984,6 +1002,7 @@ pub const JsApi = struct { pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); + pub const getElementsByTagNameNS = bridge.function(Document.getElementsByTagNameNS, .{}); pub const getSelection = bridge.function(Document.getSelection, .{}); pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{}); pub const getElementsByName = bridge.function(Document.getElementsByName, .{}); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 3be094e9..679a78f8 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1135,6 +1135,23 @@ pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) ! return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) }; } +pub fn getElementsByTagNameNS(self: *Element, 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 Namespace.null + const ns: ?Namespace = if (namespace) |ns_str| + if (std.mem.eql(u8, ns_str, "*")) null else Namespace.parse(ns_str) + else + Namespace.null; + + return collections.NodeLive(.tag_name_ns).init(self.asNode(), .{ + .namespace = ns, + .local_name = try String.init(page.arena, local_name, .{}), + }, page); +} + pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) { const arena = page.arena; @@ -1531,6 +1548,7 @@ pub const JsApi = struct { pub const getClientRects = bridge.function(Element.getClientRects, .{}); pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); + pub const getElementsByTagNameNS = bridge.function(Element.getElementsByTagNameNS, .{}); pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{}); pub const children = bridge.accessor(Element.getChildren, null, .{}); pub const focus = bridge.function(Element.focus, .{}); diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index 1e2c9bc2..b4c2ccb7 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -27,6 +27,7 @@ const NodeLive = @import("node_live.zig").NodeLive; const Mode = enum { tag, tag_name, + tag_name_ns, class_name, all_elements, child_elements, @@ -42,6 +43,7 @@ const HTMLCollection = @This(); _data: union(Mode) { tag: NodeLive(.tag), tag_name: NodeLive(.tag_name), + tag_name_ns: NodeLive(.tag_name_ns), class_name: NodeLive(.class_name), all_elements: NodeLive(.all_elements), child_elements: NodeLive(.child_elements), @@ -76,6 +78,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator { .tw = switch (self._data) { .tag => |*impl| .{ .tag = impl._tw.clone() }, .tag_name => |*impl| .{ .tag_name = impl._tw.clone() }, + .tag_name_ns => |*impl| .{ .tag_name_ns = impl._tw.clone() }, .class_name => |*impl| .{ .class_name = impl._tw.clone() }, .all_elements => |*impl| .{ .all_elements = impl._tw.clone() }, .child_elements => |*impl| .{ .child_elements = impl._tw.clone() }, @@ -94,6 +97,7 @@ pub const Iterator = GenericIterator(struct { tw: union(Mode) { tag: TreeWalker.FullExcludeSelf, tag_name: TreeWalker.FullExcludeSelf, + tag_name_ns: TreeWalker.FullExcludeSelf, class_name: TreeWalker.FullExcludeSelf, all_elements: TreeWalker.FullExcludeSelf, child_elements: TreeWalker.Children, @@ -108,6 +112,7 @@ pub const Iterator = GenericIterator(struct { return switch (self.list._data) { .tag => |*impl| impl.nextTw(&self.tw.tag), .tag_name => |*impl| impl.nextTw(&self.tw.tag_name), + .tag_name_ns => |*impl| impl.nextTw(&self.tw.tag_name_ns), .class_name => |*impl| impl.nextTw(&self.tw.class_name), .all_elements => |*impl| impl.nextTw(&self.tw.all_elements), .child_elements => |*impl| impl.nextTw(&self.tw.child_elements), diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index afe7a3f4..d341b484 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -33,6 +33,7 @@ const Form = @import("../element/html/Form.zig"); const Mode = enum { tag, tag_name, + tag_name_ns, class_name, name, all_elements, @@ -44,9 +45,15 @@ const Mode = enum { form, }; +pub const TagNameNsFilter = struct { + namespace: ?Element.Namespace, // null means wildcard "*" + local_name: String, +}; + const Filters = union(Mode) { tag: Element.Tag, tag_name: String, + tag_name_ns: TagNameNsFilter, class_name: [][]const u8, name: []const u8, all_elements, @@ -83,7 +90,7 @@ const Filters = union(Mode) { pub fn NodeLive(comptime mode: Mode) type { const Filter = Filters.TypeOf(mode); const TW = switch (mode) { - .tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf, + .tag, .tag_name, .tag_name_ns, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf, .child_elements, .child_tag, .selected_options => TreeWalker.Children, }; return struct { @@ -222,6 +229,18 @@ pub fn NodeLive(comptime mode: Mode) type { const element_tag = el.getTagNameLower(); return std.mem.eql(u8, element_tag, self._filter.str()); }, + .tag_name_ns => { + const el = node.is(Element) orelse return false; + if (self._filter.namespace) |ns| { + if (el._namespace != ns) return false; + } + // ok, namespace matches, check local name + if (self._filter.local_name.eql(comptime .wrap("*"))) { + // wildcard, match-all + return true; + } + return self._filter.local_name.eqlSlice(el.getLocalName()); + }, .class_name => { if (self._filter.len == 0) { return false; @@ -328,6 +347,7 @@ pub fn NodeLive(comptime mode: Mode) type { .name => return page._factory.create(NodeList{ .data = .{ .name = self } }), .tag => HTMLCollection{ ._data = .{ .tag = self } }, .tag_name => HTMLCollection{ ._data = .{ .tag_name = self } }, + .tag_name_ns => HTMLCollection{ ._data = .{ .tag_name_ns = self } }, .class_name => HTMLCollection{ ._data = .{ .class_name = self } }, .all_elements => HTMLCollection{ ._data = .{ .all_elements = self } }, .child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },