Merge pull request #1613 from egrs/lookup-namespace-uri
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

implement Node.lookupNamespaceURI() and isDefaultNamespace()
This commit is contained in:
Karl Seguin
2026-02-20 20:48:05 +08:00
committed by GitHub
4 changed files with 117 additions and 17 deletions

View File

@@ -111,6 +111,7 @@ _element_shadow_roots: Element.ShadowRootLookup = .empty,
_node_owner_documents: Node.OwnerDocumentLookup = .empty, _node_owner_documents: Node.OwnerDocumentLookup = .empty,
_element_assigned_slots: Element.AssignedSlotLookup = .empty, _element_assigned_slots: Element.AssignedSlotLookup = .empty,
_element_scroll_positions: Element.ScrollPositionLookup = .empty, _element_scroll_positions: Element.ScrollPositionLookup = .empty,
_element_namespace_uris: Element.NamespaceUriLookup = .empty,
/// Lazily-created inline event listeners (or listeners provided as attributes). /// Lazily-created inline event listeners (or listeners provided as attributes).
/// Avoids bloating all elements with extra function fields for rare usage. /// Avoids bloating all elements with extra function fields for rare usage.

View File

@@ -160,6 +160,14 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8
const normalized_name = if (ns == .html) std.ascii.lowerString(&page.buf, name) else name; const normalized_name = if (ns == .html) std.ascii.lowerString(&page.buf, name) else name;
const node = try page.createElementNS(ns, normalized_name, null); const node = try page.createElementNS(ns, normalized_name, null);
// Store original URI for unknown namespaces so lookupNamespaceURI can return it
if (ns == .unknown) {
if (namespace) |uri| {
const duped = try page.dupeString(uri);
try page._element_namespace_uris.put(page.arena, node.as(Element), duped);
}
}
// Track owner document if it's not the main document // Track owner document if it's not the main document
if (self != page.document) { if (self != page.document) {
try page.setNodeOwnerDocument(node, self); try page.setNodeOwnerDocument(node, self);

View File

@@ -49,6 +49,7 @@ pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMT
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
pub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);
pub const ScrollPosition = struct { pub const ScrollPosition = struct {
x: u32 = 0, x: u32 = 0,
@@ -364,6 +365,64 @@ pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
return self._namespace.toUri(); return self._namespace.toUri();
} }
pub fn getNamespaceUri(self: *Element, page: *Page) ?[]const u8 {
if (self._namespace != .unknown) return self._namespace.toUri();
return page._element_namespace_uris.get(self);
}
pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, page: *Page) ?[]const u8 {
// Hardcoded reserved prefixes
if (prefix) |p| {
if (std.mem.eql(u8, p, "xml")) return "http://www.w3.org/XML/1998/namespace";
if (std.mem.eql(u8, p, "xmlns")) return "http://www.w3.org/2000/xmlns/";
}
// Step 1: check element's own namespace/prefix
if (self.getNamespaceUri(page)) |ns_uri| {
const el_prefix = self._prefix();
const match = if (prefix == null and el_prefix == null)
true
else if (prefix != null and el_prefix != null)
std.mem.eql(u8, prefix.?, el_prefix.?)
else
false;
if (match) return ns_uri;
}
// Step 2: search xmlns attributes
if (self._attributes) |attrs| {
var iter = attrs.iterator();
while (iter.next()) |entry| {
if (prefix == null) {
if (entry._name.eql(comptime .wrap("xmlns"))) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
} else {
const name = entry._name.str();
if (std.mem.startsWith(u8, name, "xmlns:")) {
if (std.mem.eql(u8, name["xmlns:".len..], prefix.?)) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
}
}
}
}
// Step 3: recurse to parent element
const parent = self.asNode().parentElement() orelse return null;
return parent.lookupNamespaceURIForElement(prefix, page);
}
fn _prefix(self: *const Element) ?[]const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
return name[0..pos];
}
return null;
}
pub fn getLocalName(self: *Element) []const u8 { pub fn getLocalName(self: *Element) []const u8 {
const name = self.getTagNameLower(); const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| { if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
@@ -534,17 +593,26 @@ pub fn setAttributeNS(
value: String, value: String,
page: *Page, page: *Page,
) !void { ) !void {
if (maybe_namespace) |namespace| { const attr_name = if (maybe_namespace) |namespace| blk: {
// For xmlns namespace, store the full qualified name (e.g. "xmlns:bar")
// so lookupNamespaceURI can find namespace declarations.
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
break :blk qualified_name;
}
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) { if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace }); log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
} }
} break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
const local_name = if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..] qualified_name[idx + 1 ..]
else else
qualified_name; qualified_name;
return self.setAttribute(.wrap(local_name), value, page); } else blk: {
break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..]
else
qualified_name;
};
return self.setAttribute(.wrap(attr_name), value, page);
} }
pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void { pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {
@@ -1560,15 +1628,7 @@ pub const JsApi = struct {
return buf.written(); return buf.written();
} }
pub const prefix = bridge.accessor(_prefix, null, .{}); pub const prefix = bridge.accessor(Element._prefix, null, .{});
fn _prefix(self: *Element) ?[]const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
return name[0..pos];
}
return null;
}
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true }); pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });
fn _setAttribute(self: *Element, name: String, value: js.Value, page: *Page) !void { fn _setAttribute(self: *Element, name: String, value: js.Value, page: *Page) !void {

View File

@@ -338,6 +338,35 @@ pub fn getNodeType(self: *const Node) u8 {
}; };
} }
pub fn lookupNamespaceURI(self: *Node, prefix_arg: ?[]const u8, page: *Page) ?[]const u8 {
const prefix: ?[]const u8 = if (prefix_arg) |p| (if (p.len == 0) null else p) else null;
switch (self._type) {
.element => |el| return el.lookupNamespaceURIForElement(prefix, page),
.document => |doc| {
const de = doc.getDocumentElement() orelse return null;
return de.lookupNamespaceURIForElement(prefix, page);
},
.document_type, .document_fragment => return null,
.attribute => |attr| {
const owner = attr.getOwnerElement() orelse return null;
return owner.lookupNamespaceURIForElement(prefix, page);
},
.cdata => {
const parent = self.parentElement() orelse return null;
return parent.lookupNamespaceURIForElement(prefix, page);
},
}
}
pub fn isDefaultNamespace(self: *Node, namespace_arg: ?[]const u8, page: *Page) bool {
const namespace: ?[]const u8 = if (namespace_arg) |ns| (if (ns.len == 0) null else ns) else null;
const default_ns = self.lookupNamespaceURI(null, page);
if (default_ns == null and namespace == null) return true;
if (default_ns != null and namespace != null) return std.mem.eql(u8, default_ns.?, namespace.?);
return false;
}
pub fn isEqualNode(self: *Node, other: *Node) bool { pub fn isEqualNode(self: *Node, other: *Node) bool {
if (self == other) { if (self == other) {
return true; return true;
@@ -1016,6 +1045,8 @@ pub const JsApi = struct {
pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{}); pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{});
pub const getRootNode = bridge.function(Node.getRootNode, .{}); pub const getRootNode = bridge.function(Node.getRootNode, .{});
pub const isEqualNode = bridge.function(Node.isEqualNode, .{}); pub const isEqualNode = bridge.function(Node.isEqualNode, .{});
pub const lookupNamespaceURI = bridge.function(Node.lookupNamespaceURI, .{});
pub const isDefaultNamespace = bridge.function(Node.isDefaultNamespace, .{});
fn _baseURI(_: *Node, page: *const Page) []const u8 { fn _baseURI(_: *Node, page: *const Page) []const u8 {
return page.base(); return page.base();