feat(webapi): implement Node.lookupPrefix

Implement the Node.lookupPrefix(namespace) method per the WHATWG DOM
"locate a namespace prefix" algorithm: match the element's own
namespace and prefix, then scan its xmlns: attribute declarations for
one whose value is the namespace, then recurse to the parent element.

This is the inverse of the existing lookupNamespaceURI and completes
the namespace-introspection trio alongside isDefaultNamespace.
This commit is contained in:
Rohit
2026-06-03 21:03:27 +05:30
parent 720e610542
commit e830d1da4e
3 changed files with 97 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="host" xmlns:foo="http://example.com/foo" xmlns:bar="http://example.com/bar">
<span id="child"></span>
</div>
<script id="lookupPrefix">
{
const host = document.getElementById('host');
// Resolves a prefix declared via an xmlns: attribute on the element.
testing.expectEqual('foo', host.lookupPrefix('http://example.com/foo'));
testing.expectEqual('bar', host.lookupPrefix('http://example.com/bar'));
// A descendant inherits the declaration by walking up to the ancestor.
const child = document.getElementById('child');
testing.expectEqual('foo', child.lookupPrefix('http://example.com/foo'));
// Unknown namespace returns null.
testing.expectEqual(null, host.lookupPrefix('http://example.com/missing'));
}
</script>
<script id="lookupPrefix-null-empty">
{
const host = document.getElementById('host');
// null or empty namespace always returns null.
testing.expectEqual(null, host.lookupPrefix(null));
testing.expectEqual(null, host.lookupPrefix(''));
}
</script>
<script id="lookupPrefix-own-prefix">
{
// An element whose own prefix declares the namespace returns that prefix.
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg:rect');
testing.expectEqual('svg', svg.prefix);
testing.expectEqual('svg', svg.lookupPrefix('http://www.w3.org/2000/svg'));
// Detached element with no matching declaration returns null.
const div = document.createElement('div');
testing.expectEqual(null, div.lookupPrefix('http://example.com/foo'));
}
</script>

View File

@@ -422,6 +422,34 @@ pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, frame:
return parent.lookupNamespaceURIForElement(prefix, frame);
}
// Locate a namespace prefix: the inverse of lookupNamespaceURIForElement.
// Given a namespace URI, find the prefix that declares it.
pub fn lookupPrefixForElement(self: *Element, namespace: []const u8, frame: *Frame) ?[]const u8 {
// Step 1: element's own namespace/prefix
if (self.getNamespaceUri(frame)) |ns_uri| {
if (self._prefix()) |el_prefix| {
if (std.mem.eql(u8, ns_uri, namespace)) {
return el_prefix;
}
}
}
// Step 2: search xmlns: attribute declarations for one whose value is the namespace
if (self._attributes) |attrs| {
var iter = attrs.iterator();
while (iter.next()) |entry| {
const name = entry._name.str();
if (std.mem.startsWith(u8, name, "xmlns:") and std.mem.eql(u8, entry._value.str(), namespace)) {
return name["xmlns:".len..];
}
}
}
// Step 3: recurse to parent element
const parent = self.asNode().parentElement() orelse return null;
return parent.lookupPrefixForElement(namespace, frame);
}
fn _prefix(self: *const Element) ?[]const u8 {
const name = self.getTagNameLower();
if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {

View File

@@ -379,6 +379,28 @@ pub fn lookupNamespaceURI(self: *Node, prefix_arg: ?[]const u8, frame: *Frame) ?
}
}
pub fn lookupPrefix(self: *Node, namespace_arg: ?[]const u8, frame: *Frame) ?[]const u8 {
const namespace = namespace_arg orelse return null;
if (namespace.len == 0) return null;
switch (self._type) {
.element => |el| return el.lookupPrefixForElement(namespace, frame),
.document => |doc| {
const de = doc.getDocumentElement() orelse return null;
return de.lookupPrefixForElement(namespace, frame);
},
.document_type, .document_fragment => return null,
.attribute => |attr| {
const owner = attr.getOwnerElement() orelse return null;
return owner.lookupPrefixForElement(namespace, frame);
},
.cdata => {
const parent = self.parentElement() orelse return null;
return parent.lookupPrefixForElement(namespace, frame);
},
}
}
pub fn isDefaultNamespace(self: *Node, namespace_arg: ?[]const u8, frame: *Frame) bool {
const namespace: ?[]const u8 = if (namespace_arg) |ns| (if (ns.len == 0) null else ns) else null;
const default_ns = self.lookupNamespaceURI(null, frame);
@@ -1197,6 +1219,7 @@ pub const JsApi = struct {
pub const getRootNode = bridge.function(Node.getRootNode, .{});
pub const isEqualNode = bridge.function(Node.isEqualNode, .{});
pub const lookupNamespaceURI = bridge.function(Node.lookupNamespaceURI, .{});
pub const lookupPrefix = bridge.function(Node.lookupPrefix, .{});
pub const isDefaultNamespace = bridge.function(Node.isDefaultNamespace, .{});
fn _baseURI(_: *Node, frame: *const Frame) []const u8 {