webapi: script-created documents are UTF-8; URLs use the document encoding

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 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-11 13:28:13 +02:00
committed by Karl Seguin
parent fd174986cf
commit 152f1eb8eb
3 changed files with 20 additions and 3 deletions

View File

@@ -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";

View File

@@ -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 });

View File

@@ -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