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..ff6f71d8 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -219,47 +219,16 @@ pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
return null;
}
-const GetElementsByTagNameResult = union(enum) {
- tag: collections.NodeLive(.tag),
- tag_name: collections.NodeLive(.tag_name),
- all_elements: collections.NodeLive(.all_elements),
-};
-pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
- if (tag_name.len > 256) {
- // 256 seems generous.
- return error.InvalidTagName;
- }
+pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
+ return self.asNode().getElementsByTagName(tag_name, page);
+}
- if (std.mem.eql(u8, tag_name, "*")) {
- return .{
- .all_elements = collections.NodeLive(.all_elements).init(self.asNode(), {}, 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.asNode(), known, page),
- };
- }
-
- const arena = page.arena;
- const filter = try String.init(arena, lower, .{});
- 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) {
+ return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
}
pub fn getElementsByClassName(self: *Document, 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.asNode(), class_names.items, page);
+ return self.asNode().getElementsByClassName(class_name, page);
}
pub fn getElementsByName(self: *Document, name: []const u8, page: *Page) !collections.NodeLive(.name) {
@@ -914,7 +883,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 +954,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..a118ea11 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -1105,47 +1105,16 @@ fn calculateSiblingPosition(node: *Node) f64 {
return position * 5.0; // 5px per node
}
-const GetElementsByTagNameResult = union(enum) {
- tag: collections.NodeLive(.tag),
- tag_name: collections.NodeLive(.tag_name),
- all_elements: collections.NodeLive(.all_elements),
-};
-pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
- if (tag_name.len > 256) {
- // 256 seems generous.
- return error.InvalidTagName;
- }
+pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
+ return self.asNode().getElementsByTagName(tag_name, page);
+}
- if (std.mem.eql(u8, tag_name, "*")) {
- return .{
- .all_elements = collections.NodeLive(.all_elements).init(self.asNode(), {}, page),
- };
- }
-
- const lower = std.ascii.lowerString(&page.buf, tag_name);
- if (Tag.parseForMatch(lower)) |known| {
- // optimized for known tag names
- return .{
- .tag = collections.NodeLive(.tag).init(self.asNode(), known, page),
- };
- }
-
- const arena = page.arena;
- const filter = try String.init(arena, lower, .{});
- 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) {
+ return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
}
pub fn getElementsByClassName(self: *Element, 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.asNode(), class_names.items, page);
+ return self.asNode().getElementsByClassName(class_name, page);
}
pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
@@ -1531,6 +1500,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/Node.zig b/src/browser/webapi/Node.zig
index 7482e042..e93b9b25 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -845,6 +845,69 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), pag
}
}
+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,
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 } },