diff --git a/src/browser/frame/node_factory.zig b/src/browser/frame/node_factory.zig
index 9cd822889..6de08a90d 100644
--- a/src/browser/frame/node_factory.zig
+++ b/src/browser/frame/node_factory.zig
@@ -1087,10 +1087,25 @@ fn populateElementAttributes(frame: *Frame, element: *Element, list: anytype) !v
var attributes = &element._attributes;
try attributes.ensureTotalCapacity(count, frame);
while (list.next()) |attr| {
- try attributes.putNew(attr.name.local.slice(), attr.value.slice(), frame);
+ const name = try parserAttributeName(frame, attr.name);
+ try attributes.putNew(name, attr.value.slice(), frame);
}
}
+// Attributes are keyed by qualified name (no namespace model), so a prefixed
+// attribute (`xlink:href` in foreign content, `xml:id` in XML) must keep its
+// prefix — that is what `getAttribute("xlink:href")` and `Attr.name` see in
+// browsers. The joined name only has to outlive putNew, which canonicalizes
+// it into the frame arena. (Not frame.buf: name normalization writes there.)
+fn parserAttributeName(frame: *Frame, qname: Parser.QualName) ![]const u8 {
+ const local = qname.local.slice();
+ const prefix = (qname.prefix.unwrap() orelse return local).slice();
+ if (prefix.len == 0) {
+ return local;
+ }
+ return std.fmt.allocPrint(frame.local_arena, "{s}:{s}", .{ prefix, local });
+}
+
// Called when `new MyElement()` is invoked directly in JS (not via the
// customElements.define/upgrade path). `new_target` is the constructor
// function that was used with `new`. We find the matching definition in the
diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig
index 2bbe994cb..f735916ad 100644
--- a/src/browser/parser/Parser.zig
+++ b/src/browser/parser/Parser.zig
@@ -25,6 +25,7 @@ const Node = @import("../webapi/Node.zig");
const Element = @import("../webapi/Element.zig");
const CData = @import("../webapi/CData.zig");
+pub const QualName = h5e.QualName;
pub const AttributeIterator = h5e.AttributeIterator;
const Allocator = std.mem.Allocator;
@@ -453,7 +454,8 @@ fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName,
}
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
- return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
+ // An XML element outside any xmlns declaration is in no namespace (null namespace)
+ return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .null);
}
// html5ever_parse_fragment materializes the fragment's context element through
diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html
index 71640464e..9bb5ccde0 100644
--- a/src/browser/tests/domparser.html
+++ b/src/browser/tests/domparser.html
@@ -419,3 +419,26 @@
testing.expectEqual(2, allElements.length);
}
+
+
diff --git a/src/browser/tests/element/svg/animated_string.html b/src/browser/tests/element/svg/animated_string.html
index 7a8520eca..21b3833aa 100644
--- a/src/browser/tests/element/svg/animated_string.html
+++ b/src/browser/tests/element/svg/animated_string.html
@@ -34,35 +34,34 @@
+
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index a1e4364be..a90c7d14a 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -674,7 +674,7 @@ pub fn evaluate(
self: *Document,
expression: []const u8,
context_node: ?*Node,
- resolver: ?js.Function,
+ resolver: ?js.Value,
result_type: ?u16,
result: ?*XPathResult,
frame: *Frame,
@@ -697,7 +697,7 @@ pub fn evaluate(
pub fn createExpression(
_: *const Document,
expression: []const u8,
- resolver: ?js.Function,
+ resolver: ?js.Value,
frame: *Frame,
) !*XPathExpression {
_ = resolver;
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index a11caf0b9..30cc3927b 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -114,6 +114,9 @@ pub const Namespace = enum(u8) {
pub fn parse(namespace_: ?[]const u8) Namespace {
const namespace = namespace_ orelse return .null;
+ if (namespace.len == 0) {
+ return .null;
+ }
if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
// Common case, avoid the string comparison. Recklessly
@branchHint(.likely);
@@ -652,22 +655,40 @@ pub fn getAttribute(self: *const Element, name: String, frame: *Frame) !?String
return self._attributes.get(name, frame);
}
-/// For simplicity, the namespace is currently ignored and only the local name is used.
pub fn getAttributeNS(
self: *const Element,
- maybe_namespace: ?[]const u8,
+ namespace_: ?[]const u8,
local_name: String,
frame: *Frame,
) !?String {
- if (maybe_namespace) |namespace| {
- if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
- log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
+ if (namespace_) |namespace| {
+ // we don't really support namespaces, but if the namespace has a fixed
+ // prefix, we can try to fetch the attribute with it
+ if (try prefixedAttributeName(namespace, local_name.str(), frame)) |prefixed| {
+ if (try self.getAttribute(.wrap(prefixed), frame)) |value| {
+ return value;
+ }
}
}
-
return self.getAttribute(local_name, frame);
}
+fn prefixedAttributeName(namespace: []const u8, local_name: []const u8, frame: *Frame) !?[]const u8 {
+ const prefix = blk: {
+ if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xlink")) {
+ break :blk "xlink";
+ }
+ if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
+ break :blk "xml";
+ }
+ if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
+ break :blk "xmlns";
+ }
+ return null;
+ };
+ return try std.fmt.allocPrint(frame.local_arena, "{s}:{s}", .{ prefix, local_name });
+}
+
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
return self._attributes.getSafe(name);
}
@@ -677,20 +698,13 @@ pub fn hasAttribute(self: *const Element, name: String, frame: *Frame) !bool {
return value != null;
}
-/// Like getAttributeNS, the namespace is currently ignored.
pub fn hasAttributeNS(
self: *const Element,
- maybe_namespace: ?[]const u8,
+ namespace_: ?[]const u8,
local_name: String,
frame: *Frame,
) !bool {
- if (maybe_namespace) |namespace| {
- if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
- log.warn(.not_implemented, "Element.hasAttributeNS", .{ .namespace = namespace });
- }
- }
-
- return self.hasAttribute(local_name, frame);
+ return try self.getAttributeNS(namespace_, local_name, frame) != null;
}
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
@@ -769,30 +783,24 @@ pub fn setAttribute(self: *Element, name: String, value: String, frame: *Frame)
pub fn setAttributeNS(
self: *Element,
- maybe_namespace: ?[]const u8,
+ namespace_: ?[]const u8,
qualified_name: []const u8,
value: String,
frame: *Frame,
) !void {
- 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;
+ const local_start = if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx| blk: {
+ if (idx == 0 or idx == qualified_name.len - 1) {
+ // cannot be at the start or end of the qname
+ return error.InvalidCharacterError;
}
- if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
- log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
+ if (std.mem.indexOfScalarPos(u8, qualified_name, idx + 1, ':') != null) {
+ // and can only have one
+ return error.InvalidCharacterError;
}
- break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
- qualified_name[idx + 1 ..]
- else
- qualified_name;
- } else blk: {
- break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
- qualified_name[idx + 1 ..]
- else
- qualified_name;
- };
+ break :blk idx + 1;
+ } else 0;
+
+ const attr_name = if (namespace_ != null) qualified_name else qualified_name[local_start..];
return self.setAttribute(.wrap(attr_name), value, frame);
}
diff --git a/src/browser/webapi/XPathEvaluator.zig b/src/browser/webapi/XPathEvaluator.zig
index c35a58178..5a5f28fcf 100644
--- a/src/browser/webapi/XPathEvaluator.zig
+++ b/src/browser/webapi/XPathEvaluator.zig
@@ -43,7 +43,7 @@ pub fn evaluate(
_: *const XPathEvaluator,
expression: []const u8,
context_node: *Node,
- resolver: ?js.Function,
+ resolver: ?js.Value,
requested_type: ?u16,
result: ?*XPathResult,
frame: *Frame,
@@ -59,7 +59,7 @@ pub fn evaluate(
pub fn createExpression(
_: *const XPathEvaluator,
expression: []const u8,
- resolver: ?js.Function,
+ resolver: ?js.Value,
frame: *Frame,
) !*XPathExpression {
_ = resolver;
diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig
index 39960ec0b..70abc8412 100644
--- a/src/browser/webapi/element/Svg.zig
+++ b/src/browser/webapi/element/Svg.zig
@@ -194,6 +194,5 @@ pub const JsApi = struct {
const testing = @import("../../../testing.zig");
test "WebApi: Svg" {
- testing.expectLog(&.{ .not_implemented, .not_implemented });
try testing.htmlRunner("element/svg", .{});
}
diff --git a/src/browser/webapi/svg/AnimatedString.zig b/src/browser/webapi/svg/AnimatedString.zig
index d53de59bc..2ad187d58 100644
--- a/src/browser/webapi/svg/AnimatedString.zig
+++ b/src/browser/webapi/svg/AnimatedString.zig
@@ -67,8 +67,20 @@ pub fn getAnimVal(self: *const AnimatedString) []const u8 {
fn attributeName(self: *const AnimatedString) String {
return switch (self._kind) {
- .href => comptime .wrap("href"),
.class => comptime .wrap("class"),
+ .href => {
+ const href: String = comptime .wrap("href");
+ if (self._element.hasAttributeSafe(href)) {
+ return href;
+ }
+
+ const xlink_href: String = comptime .wrap("xlink:href");
+ if (self._element.hasAttributeSafe(xlink_href)) {
+ // legacy attribute, returned if it exists and href doens't
+ return xlink_href;
+ }
+ return href;
+ },
};
}