From c16c15bedfff082beefe7a64b33f9bf0a37c23af Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 11 May 2026 14:29:47 +0800 Subject: [PATCH] Various small DOM fixes, WPT driven 1. Implement document.adoptNode (we were removing from the existing document, but not adding to the new document) 2. Document.url should use the document's frame, falling back to the execution frame 3. Move HTMLDocument.location to Document.location 4. DOMImplementation.createDocument uses a more appropriate default namespace (xml -> null) 5. Map querySelector functions to DOMException-safe errors. The Selector returns specific errors, but for the DOM apis (document.querySelector, df.querySelectorAll, elem.matches, etc...) these largely all map to SyntaxError --- src/browser/tests/document/document.html | 47 +++++++++++++++++++ .../element/attribute_value_escapes.html | 2 +- .../tests/element/selector_invalid.html | 24 +++++----- src/browser/tests/page/meta.html | 3 +- src/browser/webapi/DOMImplementation.zig | 2 +- src/browser/webapi/Document.zig | 26 ++++++++-- src/browser/webapi/DocumentFragment.zig | 4 +- src/browser/webapi/Element.zig | 6 +-- src/browser/webapi/HTMLDocument.zig | 10 ---- src/browser/webapi/Node.zig | 2 +- src/browser/webapi/selector/Selector.zig | 16 +++++++ 11 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index ede2b507..eb69f5d8 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -380,6 +380,53 @@ testing.expectEqual(0, nd.childElementCount); + + diff --git a/src/browser/tests/element/selector_invalid.html b/src/browser/tests/element/selector_invalid.html index c0d16d59..7112ab85 100644 --- a/src/browser/tests/element/selector_invalid.html +++ b/src/browser/tests/element/selector_invalid.html @@ -10,9 +10,9 @@ const container = $('#container'); // Empty functional pseudo-classes should error - testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':has()')); - testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':not()')); - testing.expectError("Error: InvalidPseudoClass", () => container.querySelector(':lang()')); + testing.expectError("SyntaxError", () => container.querySelector(':has()')); + testing.expectError("SyntaxError", () => container.querySelector(':not()')); + testing.expectError("SyntaxError", () => container.querySelector(':lang()')); } @@ -21,9 +21,9 @@ const container = $('#container'); // Invalid nth patterns - testing.expectError("Error: InvalidNthPattern", () => container.querySelector(':nth-child(foo)')); - testing.expectError("Error: InvalidNthPattern", () => container.querySelector(':nth-child(-)')); - testing.expectError("Error: InvalidNthPattern", () => container.querySelector(':nth-child(+)')); + testing.expectError("SyntaxError", () => container.querySelector(':nth-child(foo)')); + testing.expectError("SyntaxError", () => container.querySelector(':nth-child(-)')); + testing.expectError("SyntaxError", () => container.querySelector(':nth-child(+)')); } @@ -32,9 +32,9 @@ const container = $('#container'); // Unknown pseudo-classes - testing.expectError("Error: UnknownPseudoClass", () => container.querySelector(':unknown')); - testing.expectError("Error: UnknownPseudoClass", () => container.querySelector(':not-a-real-pseudo')); - testing.expectError("Error: UnknownPseudoClass", () => container.querySelector(':fake(test)')); + testing.expectError("SyntaxError", () => container.querySelector(':unknown')); + testing.expectError("SyntaxError", () => container.querySelector(':not-a-real-pseudo')); + testing.expectError("SyntaxError", () => container.querySelector(':fake(test)')); } @@ -53,8 +53,8 @@ const container = $('#container'); // Combinators with nothing after - testing.expectError("Error: InvalidSelector", () => container.querySelector('p >')); - testing.expectError("Error: InvalidSelector", () => container.querySelector('p +')); - testing.expectError("Error: InvalidSelector", () => container.querySelector('p ~')); + testing.expectError("SyntaxError", () => container.querySelector('p >')); + testing.expectError("SyntaxError", () => container.querySelector('p +')); + testing.expectError("SyntaxError", () => container.querySelector('p ~')); } diff --git a/src/browser/tests/page/meta.html b/src/browser/tests/page/meta.html index 3c03f403..98fb1688 100644 --- a/src/browser/tests/page/meta.html +++ b/src/browser/tests/page/meta.html @@ -30,7 +30,8 @@ testing.expectEqual('undefined', typeof plainDoc.scripts); testing.expectEqual('undefined', typeof plainDoc.links); testing.expectEqual('undefined', typeof plainDoc.forms); - testing.expectEqual('undefined', typeof plainDoc.location); + // location lives on Document (returns null for non-HTMLDocument). + testing.expectEqual(null, plainDoc.location); // Both should have common Document properties testing.expectEqual('string', typeof document.URL); diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index 777a9571..280db6a8 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -78,7 +78,7 @@ pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qual // Create and append root element if qualified_name provided if (qualified_name) |qname| { if (qname.len > 0) { - const namespace = if (namespace_) |ns| Node.Element.Namespace.parse(ns) else .xml; + const namespace = Node.Element.Namespace.parse(namespace_); const root = try frame.createElementNS(namespace, qname, null); _ = try document.asNode().appendChild(root, frame); } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 747e08c2..95c57793 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -119,7 +119,18 @@ pub fn asEventTarget(self: *Document) *@import("EventTarget.zig") { } pub fn getURL(self: *const Document, frame: *const Frame) [:0]const u8 { - return self._url orelse frame.url; + return self._url orelse (self._frame orelse frame).url; +} + +pub fn getLocation(self: *const Document) ?*Location { + if (self._type != .html) return null; + const doc_frame = self._frame orelse return null; + return doc_frame.window._location; +} + +pub fn setLocation(self: *Document, url: [:0]const u8, frame: *Frame) !void { + if (self._type != .html) return; + return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._frame }); } pub fn getContentType(self: *const Document) []const u8 { @@ -277,11 +288,11 @@ pub fn getSelection(self: *Document) *Selection { } pub fn querySelector(self: *Document, input: String, frame: *Frame) !?*Element { - return Selector.querySelector(self.asNode(), input.str(), frame); + return Selector.querySelector(self.asNode(), input.str(), frame) catch |err| Selector.mapErrorToDOM(err); } pub fn querySelectorAll(self: *Document, input: String, frame: *Frame) !*Selector.List { - return Selector.querySelectorAll(self.asNode(), input.str(), frame); + return Selector.querySelectorAll(self.asNode(), input.str(), frame) catch |err| Selector.mapErrorToDOM(err); } pub fn getImplementation(self: *Document, frame: *Frame) !*DOMImplementation { @@ -465,15 +476,21 @@ pub fn getFonts(self: *Document, frame: *Frame) !*FontFaceSet { return fonts; } -pub fn adoptNode(_: *const Document, node: *Node, frame: *Frame) !*Node { +pub fn adoptNode(self: *Document, node: *Node, frame: *Frame) !*Node { if (node._type == .document) { return error.NotSupported; } + const old_owner = node.ownerDocument(frame) orelse frame.document; + if (node._parent) |parent| { frame.removeNode(parent, node, .{ .will_be_reconnected = false }); } + if (old_owner != self) { + try frame.adoptNodeTree(node, old_owner, self); + } + return node; } @@ -1029,6 +1046,7 @@ pub const JsApi = struct { pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{}); pub const URL = bridge.accessor(Document.getURL, null, .{}); + pub const location = bridge.accessor(Document.getLocation, Document.setLocation, .{}); pub const documentURI = bridge.accessor(Document.getURL, null, .{}); pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{}); pub const scrollingElement = bridge.accessor(Document.getDocumentElement, null, .{}); diff --git a/src/browser/webapi/DocumentFragment.zig b/src/browser/webapi/DocumentFragment.zig index 186bc68a..b55050f2 100644 --- a/src/browser/webapi/DocumentFragment.zig +++ b/src/browser/webapi/DocumentFragment.zig @@ -84,11 +84,11 @@ pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element { } pub fn querySelector(self: *DocumentFragment, selector: []const u8, frame: *Frame) !?*Element { - return Selector.querySelector(self.asNode(), selector, frame); + return Selector.querySelector(self.asNode(), selector, frame) catch |err| Selector.mapErrorToDOM(err); } pub fn querySelectorAll(self: *DocumentFragment, input: []const u8, frame: *Frame) !*Selector.List { - return Selector.querySelectorAll(self.asNode(), input, frame); + return Selector.querySelectorAll(self.asNode(), input, frame) catch |err| Selector.mapErrorToDOM(err); } pub fn getChildren(self: *DocumentFragment, frame: *Frame) !collections.NodeLive(.child_elements) { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 4de1a732..058875a6 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1071,15 +1071,15 @@ pub fn getChildElementCount(self: *Element) usize { } pub fn matches(self: *Element, selector: []const u8, frame: *Frame) !bool { - return Selector.matches(self, selector, frame); + return Selector.matches(self, selector, frame) catch |err| Selector.mapErrorToDOM(err); } pub fn querySelector(self: *Element, selector: []const u8, frame: *Frame) !?*Element { - return Selector.querySelector(self.asNode(), selector, frame); + return Selector.querySelector(self.asNode(), selector, frame) catch |err| Selector.mapErrorToDOM(err); } pub fn querySelectorAll(self: *Element, input: []const u8, frame: *Frame) !*Selector.List { - return Selector.querySelectorAll(self.asNode(), input, frame); + return Selector.querySelectorAll(self.asNode(), input, frame) catch |err| Selector.mapErrorToDOM(err); } pub fn getAnimations(_: *const Element) []*Animation { diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 41782cc8..19e462a1 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -196,15 +196,6 @@ pub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script { return self._proto._current_script; } -pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") { - const frame = self._proto._frame orelse return null; - return frame.window._location; -} - -pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, frame: *Frame) !void { - return frame.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._frame }); -} - pub fn getDir(self: *HTMLDocument) []const u8 { const el = self._proto.getDocumentElement() orelse return ""; const html = el.is(Element.Html) orelse return ""; @@ -311,7 +302,6 @@ pub const JsApi = struct { pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{}); pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{}); pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{}); - pub const location = bridge.accessor(HTMLDocument.getLocation, HTMLDocument.setLocation, .{}); pub const all = bridge.accessor(HTMLDocument.getAll, null, .{}); pub const cookie = bridge.accessor(HTMLDocument.getCookie, HTMLDocument.setCookie, .{}); pub const doctype = bridge.accessor(HTMLDocument.getDocType, null, .{}); diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 7df1fd6a..c26411ed 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -166,7 +166,7 @@ pub fn findAdjacentNodes(self: *Node, position: []const u8) !struct { *Node, ?*N // Returned if: // * position is not one of the four listed values. // * The input is XML that is not well-formed. - return error.Syntax; + return error.SyntaxError; } pub fn firstChild(self: *const Node) ?*Node { diff --git a/src/browser/webapi/selector/Selector.zig b/src/browser/webapi/selector/Selector.zig index 2591ce6c..7322e02e 100644 --- a/src/browser/webapi/selector/Selector.zig +++ b/src/browser/webapi/selector/Selector.zig @@ -28,6 +28,22 @@ pub const List = @import("List.zig"); const String = lp.String; const Allocator = std.mem.Allocator; +// translate a Selector error to a DOMException known type. +pub fn mapErrorToDOM(err: anyerror) anyerror { + return switch (err) { + error.InvalidSelector, + error.InvalidAttributeSelector, + error.InvalidIDSelector, + error.InvalidClassSelector, + error.UnknownPseudoClass, + error.InvalidTagSelector, + error.InvalidPseudoClass, + error.InvalidNthPattern, + => error.SyntaxError, + else => err, + }; +} + pub fn parseLeaky(arena: Allocator, input: []const u8) !Parsed { if (input.len == 0) { return error.SyntaxError;