From 152f1eb8eb51d92d9619121a68e9f97be2fb5902 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 11 Jul 2026 13:28:13 +0200 Subject: [PATCH] webapi: script-created documents are UTF-8; URLs use the document encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the last failing subtest of WPT /dom/nodes/DOMImplementation-createHTMLDocument.html ("URL parsing"): resolving `a.href = "http://example.org/?รค"` on an anchor inside a createHTMLDocument() document returned "?%E4" instead of "?%C3%A4". Two spec violations combined: - Per DOM, documents synthesized by script (createHTMLDocument, createDocument, new Document()) have the UTF-8 encoding. We had no per-document encoding at all: document.characterSet always reflected the frame's charset, so the new document inherited windows-1252 from the test page (which uses that encoding deliberately to catch this). Document gains a _charset override (same pattern as _content_type), set to UTF-8 in DOMImplementation.createHTMLDocument/createDocument and the Document constructor. - Node.resolveURL encoded the query string with the frame's charset. Per the URL/HTML specs the query percent-encoding uses the encoding of the element's node document, so it now resolves the owning document and uses its encoding, falling back to the frame's. Coverage: - /dom/nodes/DOMImplementation-createHTMLDocument.html 12/13 -> 13/13 (fully green) - /dom/nodes/Document-constructor.html 3/5 -> 4/5 Co-Authored-By: Claude Fable 5 --- src/browser/webapi/DOMImplementation.zig | 2 ++ src/browser/webapi/Document.zig | 15 +++++++++++++-- src/browser/webapi/Node.zig | 6 +++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index 1e0a44b69..cce49bd0e 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -50,6 +50,7 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString const document = (try frame._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument(); document._ready_state = .complete; document._url = "about:blank"; + document._charset = "UTF-8"; { const doctype = try frame._factory.node(DocumentType{ @@ -101,6 +102,7 @@ pub fn createDocument(_: *const DOMImplementation, namespace_nullable: js.Nullab // Create XML Document const document = (try frame._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument(); document._url = "about:blank"; + document._charset = "UTF-8"; // Per spec the content type depends on the requested namespace. document._content_type = blk: { const ns = namespace_ orelse break :blk "application/xml"; diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index b34de50ba..ec32cdc3b 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -56,6 +56,9 @@ _frame: ?*Frame = null, _url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank) // content type override for documents created via DOMImplementation.createDocument _content_type: ?[]const u8 = null, +// encoding override: documents synthesized by script (createHTMLDocument, +// createDocument) are UTF-8 regardless of the frame's encoding +_charset: ?[]const u8 = null, _ready_state: ReadyState = .loading, _current_script: ?*Element.Html.Script = null, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, @@ -159,6 +162,14 @@ pub fn setLocation(self: *Document, url: [:0]const u8) !void { return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = frame }); } +pub fn getCharset(self: *const Document) []const u8 { + if (self._charset) |charset| { + return charset; + } + const doc_frame = self._frame orelse return "UTF-8"; + return doc_frame.charset; +} + pub fn getContentType(self: *const Document) []const u8 { if (self._content_type) |content_type| { return content_type; @@ -1417,6 +1428,7 @@ pub const JsApi = struct { return frame._factory.node(Document{ ._proto = undefined, ._type = .generic, + ._charset = "UTF-8", }); } @@ -1508,8 +1520,7 @@ pub const JsApi = struct { pub const compatMode = bridge.property("CSS1Compat", .{ .template = false }); fn getCharacterSet(self: *const Document) []const u8 { - const doc_frame = self._frame orelse return "UTF-8"; - return doc_frame.charset; + return self.getCharset(); } pub const referrer = bridge.property("", .{ .template = false }); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 2151e1d14..bd0157a6f 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -582,7 +582,11 @@ pub const ResolveURLOpts = struct { pub fn resolveURL(self: *const Node, url: anytype, frame: *Frame, opts: ResolveURLOpts) ![:0]const u8 { const owner_frame = self.ownerFrame(frame); const allocator = opts.allocator orelse frame.call_arena; - return URL.resolve(allocator, owner_frame.base(), url, .{ .encoding = owner_frame.charset }); + // The owning document's encoding, not the frame's: script-created + // documents (e.g. createHTMLDocument) are always UTF-8. + const doc: ?*const Document = if (self._type == .document) self._type.document else self.ownerDocument(frame); + const encoding = if (doc) |d| d.getCharset() else owner_frame.charset; + return URL.resolve(allocator, owner_frame.base(), url, .{ .encoding = encoding }); } // Same as `resolveURL` but can't return `TypeError`, this is needed for multiple