webapi: DOMImplementation owns its document; validate doctype names

Fixes 81 failing tests in WPT
/dom/nodes/DOMImplementation-createDocumentType.html (1/82 -> 82/82):

- DOMImplementation was an empty singleton-style object with no link to
  its document, so a doctype created through another document's
  implementation (e.g. createHTMLDocument().implementation) reported
  the frame's main document as its ownerDocument. The implementation
  object now stores its associated document and registers created
  doctypes in the frame's node-owner-document map, following the
  createDocumentFragment pattern.
- createDocumentType now validates the qualified name against the
  doctype name production (no ASCII whitespace or '>'), throwing
  InvalidCharacterError.

Coverage: /dom/nodes/DOMImplementation-createDocumentType.html 1/82 ->
82/82.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-10 21:50:31 +02:00
parent b698eab269
commit e586e9013f
2 changed files with 20 additions and 5 deletions

View File

@@ -23,10 +23,26 @@ const Document = @import("Document.zig");
const DocumentType = @import("DocumentType.zig");
const DOMImplementation = @This();
_pad: bool = false,
pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType {
return DocumentType.init(qualified_name, public_id, system_id, frame);
// The document this implementation object belongs to: nodes created through
// it are owned by that document, not necessarily the frame's main document.
_document: *Document,
pub fn createDocumentType(self: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, frame: *Frame) !*DocumentType {
// Per spec, qualifiedName must match the doctype name production: any
// characters except ASCII whitespace or '>'.
for (qualified_name) |c| {
switch (c) {
'\t', '\n', 0x0C, '\r', ' ', '>' => return error.InvalidCharacterError,
else => {},
}
}
const doctype = try DocumentType.init(qualified_name, public_id, system_id, frame);
if (self._document != frame.document) {
try frame.setNodeOwnerDocument(doctype.asNode(), self._document);
}
return doctype;
}
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, frame: *Frame) !*Document {
@@ -98,7 +114,6 @@ pub const JsApi = struct {
pub const name = "DOMImplementation";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{});

View File

@@ -418,7 +418,7 @@ pub fn querySelectorAll(self: *Document, input: String, frame: *Frame) !*Selecto
pub fn getImplementation(self: *Document, frame: *Frame) !*DOMImplementation {
if (self._implementation) |impl| return impl;
const impl = try frame._factory.create(DOMImplementation{});
const impl = try frame._factory.create(DOMImplementation{ ._document = self });
self._implementation = impl;
return impl;
}