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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 } },