Merge pull request #1529 from lightpanda-io/get_elements_by_tag_name_ns

Get elements by tag name ns
This commit is contained in:
Karl Seguin
2026-02-12 16:41:56 +08:00
committed by GitHub
6 changed files with 228 additions and 76 deletions

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container" xmlns="http://www.w3.org/1999/xhtml">
<div id="div1">div1</div>
<p id="p1">p1</p>
<div id="div2">div2</div>
</div>
<svg id="svgContainer" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle id="circle1" cx="50" cy="50" r="40"/>
<rect id="rect1" x="10" y="10" width="30" height="30"/>
<circle id="circle2" cx="25" cy="25" r="10"/>
</svg>
<div id="mixed">
<div id="htmlDiv" xmlns="http://www.w3.org/1999/xhtml">HTML div</div>
<svg xmlns="http://www.w3.org/2000/svg">
<circle id="svgCircle" cx="10" cy="10" r="5"/>
</svg>
</div>
<script id=basic>
{
const htmlNS = "http://www.w3.org/1999/xhtml";
const svgNS = "http://www.w3.org/2000/svg";
// Test HTML namespace
const htmlDivs = document.getElementsByTagNameNS(htmlNS, 'div');
testing.expectEqual(true, htmlDivs instanceof HTMLCollection);
testing.expectEqual(5, htmlDivs.length); // container, div1, div2, mixed, htmlDiv
const htmlPs = document.getElementsByTagNameNS(htmlNS, 'p');
testing.expectEqual(1, htmlPs.length);
testing.expectEqual('p1', htmlPs[0].id);
}
</script>
<script id=svgNamespace>
{
const svgNS = "http://www.w3.org/2000/svg";
const circles = document.getElementsByTagNameNS(svgNS, 'circle');
testing.expectEqual(3, circles.length); // circle1, circle2, svgCircle
testing.expectEqual('circle1', circles[0].id);
testing.expectEqual('circle2', circles[1].id);
testing.expectEqual('svgCircle', circles[2].id);
const rects = document.getElementsByTagNameNS(svgNS, 'rect');
testing.expectEqual(1, rects.length);
testing.expectEqual('rect1', rects[0].id);
}
</script>
<script id=nullNamespace>
{
// Null namespace should match elements with null namespace
const nullNsElements = document.getElementsByTagNameNS(null, 'div');
testing.expectEqual(0, nullNsElements.length); // Our divs are in HTML namespace
}
</script>
<script id=wildcardNamespace>
{
// Wildcard namespace "*" should match all namespaces
const allDivs = document.getElementsByTagNameNS('*', 'div');
testing.expectEqual(5, allDivs.length); // All divs regardless of namespace
}
</script>
<script id=wildcardLocalName>
{
const htmlNS = "http://www.w3.org/1999/xhtml";
// Wildcard local name should match all elements in that namespace
const allHtmlElements = document.getElementsByTagNameNS(htmlNS, '*');
testing.expectEqual(true, allHtmlElements.length > 0);
}
</script>
<script id=caseSensitive>
{
const htmlNS = "http://www.w3.org/1999/xhtml";
// getElementsByTagNameNS is case-sensitive for local names
const lowerDivs = document.getElementsByTagNameNS(htmlNS, 'div');
const upperDivs = document.getElementsByTagNameNS(htmlNS, 'DIV');
testing.expectEqual(5, lowerDivs.length);
testing.expectEqual(0, upperDivs.length); // Should be 0 because it's case-sensitive
}
</script>
<script id=unknownNamespace>
{
// Unknown namespace should still work
const unknownNs = document.getElementsByTagNameNS('http://example.com/unknown', 'div');
testing.expectEqual(0, unknownNs.length);
}
</script>
<script id=emptyResult>
{
const htmlNS = "http://www.w3.org/1999/xhtml";
const svgNS = "http://www.w3.org/2000/svg";
testing.expectEqual(0, document.getElementsByTagNameNS(htmlNS, 'nonexistent').length);
testing.expectEqual(0, document.getElementsByTagNameNS(svgNS, 'nonexistent').length);
}
</script>
<script id=elementMethod>
{
const htmlNS = "http://www.w3.org/1999/xhtml";
const container = document.getElementById('container');
// getElementsByTagNameNS on element should only search descendants
const divsInContainer = container.getElementsByTagNameNS(htmlNS, 'div');
testing.expectEqual(2, divsInContainer.length); // div1, div2 (not container itself)
testing.expectEqual('div1', divsInContainer[0].id);
testing.expectEqual('div2', divsInContainer[1].id);
}
</script>

View File

@@ -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, .{});

View File

@@ -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, .{});

View File

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

View File

@@ -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),

View File

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