fix DocumentType.remove, MutationRecord.attributeNamespace, createElementNS casing

- add ChildNode.remove() to DocumentType (flips DocumentType-remove.html)
- return null for MutationRecord.attributeNamespace on non-namespaced
  attribute mutations (flips MutationObserver-takeRecords.html)
- stop lowercasing in createElementNS per spec — only createElement
  should ASCII-lowercase for HTML namespace (flips
  Element/Document-getElementsByTagNameNS.html)
- fix getElementsByTagName to use case-insensitive matching for HTML
  namespace elements
This commit is contained in:
egrs
2026-02-20 14:46:58 +01:00
parent d38ded0f26
commit 6e1b2d50f2
5 changed files with 20 additions and 6 deletions

View File

@@ -7,9 +7,11 @@
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement); testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI); testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);
// Per spec, createElementNS does NOT lowercase — 'DIV' != 'div', so this
// creates an HTMLUnknownElement, not an HTMLDivElement.
const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV'); const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');
testing.expectEqual('DIV', htmlDiv2.tagName); testing.expectEqual('DIV', htmlDiv2.tagName);
testing.expectEqual(true, htmlDiv2 instanceof HTMLDivElement); testing.expectEqual(false, htmlDiv2 instanceof HTMLDivElement);
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI); testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT'); const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');

View File

@@ -157,8 +157,8 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element { pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
try validateElementName(name); try validateElementName(name);
const ns = Element.Namespace.parse(namespace); const ns = Element.Namespace.parse(namespace);
const normalized_name = if (ns == .html) std.ascii.lowerString(&page.buf, name) else name; // Per spec, createElementNS does NOT lowercase (unlike createElement).
const node = try page.createElementNS(ns, normalized_name, null); const node = try page.createElementNS(ns, name, null);
// Store original URI for unknown namespaces so lookupNamespaceURI can return it // Store original URI for unknown namespaces so lookupNamespaceURI can return it
if (ns == .unknown) { if (ns == .unknown) {

View File

@@ -74,6 +74,12 @@ pub fn clone(self: *const DocumentType, page: *Page) !*DocumentType {
return .init(self._name, self._public_id, self._system_id, page); return .init(self._name, self._public_id, self._system_id, page);
} }
pub fn remove(self: *DocumentType, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
_ = try parent.removeChild(node, page);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(DocumentType); pub const bridge = js.Bridge(DocumentType);
@@ -87,4 +93,5 @@ pub const JsApi = struct {
pub const name = bridge.accessor(DocumentType.getName, null, .{}); pub const name = bridge.accessor(DocumentType.getName, null, .{});
pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{}); pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{});
pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{}); pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{});
pub const remove = bridge.function(DocumentType.remove, .{});
}; };

View File

@@ -387,9 +387,9 @@ pub const MutationRecord = struct {
} }
pub fn getAttributeNamespace(self: *const MutationRecord) ?[]const u8 { pub fn getAttributeNamespace(self: *const MutationRecord) ?[]const u8 {
if (self._attribute_name != null) { _ = self;
return "http://www.w3.org/1999/xhtml"; // Non-namespaced attribute mutations return null. Full namespace tracking
} // for setAttributeNS mutations is not yet implemented.
return null; return null;
} }

View File

@@ -225,8 +225,13 @@ pub fn NodeLive(comptime mode: Mode) type {
// If we're in `tag_name` mode, then the tag_name isn't // If we're in `tag_name` mode, then the tag_name isn't
// a known tag. It could be a custom element, heading, or // a known tag. It could be a custom element, heading, or
// any generic element. Compare against the element's tag name. // any generic element. Compare against the element's tag name.
// Per spec, getElementsByTagName is case-insensitive for HTML
// namespace elements, case-sensitive for others.
const el = node.is(Element) orelse return false; const el = node.is(Element) orelse return false;
const element_tag = el.getTagNameLower(); const element_tag = el.getTagNameLower();
if (el._namespace == .html) {
return std.ascii.eqlIgnoreCase(element_tag, self._filter.str());
}
return std.mem.eql(u8, element_tag, self._filter.str()); return std.mem.eql(u8, element_tag, self._filter.str());
}, },
.tag_name_ns => { .tag_name_ns => {